feat: add SYSTEM_OWNER role (#6765)

* define roles and permissions

* support system user memberships

* don't limit system users

* cleanup permissions

* restrict memberships to aggregates

* default to SYSTEM_OWNER

* update unit tests

* test: system user token test (#6778)

* update unit tests

* refactor: make authz testable

* move session constants

* cleanup

* comment

* comment

* decode member type string to enum (#6780)

* decode member type string to enum

* handle all membership types

* decode enums where necessary

* decode member type in steps config

* update system api docs

* add technical advisory

* tweak docs a bit

* comment in comment

* lint

* extract token from Bearer header prefix

* review changes

* fix tests

* fix: add fix for activityhandler

* add isSystemUser

* remove IsSystemUser from activity info

* fix: add fix for activityhandler

---------

Co-authored-by: Stefan Benz <stefan@caos.ch>
This commit is contained in:
Elio Bischof
2023-10-25 17:10:45 +02:00
committed by GitHub
parent c8b9b0ac75
commit 4980cd6a0c
34 changed files with 959 additions and 410 deletions

View File

@@ -45,29 +45,47 @@ func (t TriggerMethod) String() string {
}
func Trigger(ctx context.Context, orgID, userID string, trigger TriggerMethod) {
triggerLog(authz.GetInstance(ctx).InstanceID(), orgID, userID, http_utils.ComposedOrigin(ctx), trigger, info.ActivityInfoFromContext(ctx))
ai := info.ActivityInfoFromContext(ctx)
triggerLog(
authz.GetInstance(ctx).InstanceID(),
orgID,
userID,
http_utils.ComposedOrigin(ctx),
trigger,
ai.Method,
ai.Path,
ai.RequestMethod,
authz.GetCtxData(ctx).SystemMemberships != nil,
)
}
func TriggerWithContext(ctx context.Context, trigger TriggerMethod) {
data := authz.GetCtxData(ctx)
ai := info.ActivityInfoFromContext(ctx)
// if GRPC call, path is prefilled with the grpc fullmethod and method is empty
if ai.Method == "" {
ai.Method = ai.Path
ai.Path = ""
}
triggerLog(authz.GetInstance(ctx).InstanceID(), data.OrgID, data.UserID, http_utils.ComposedOrigin(ctx), trigger, ai)
// GRPC call the method is contained in the HTTP request path
method := ai.Path
triggerLog(
authz.GetInstance(ctx).InstanceID(),
authz.GetCtxData(ctx).OrgID,
authz.GetCtxData(ctx).UserID,
http_utils.ComposedOrigin(ctx),
trigger,
method,
"",
ai.RequestMethod,
authz.GetCtxData(ctx).SystemMemberships != nil,
)
}
func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, ai *info.ActivityInfo) {
func triggerLog(instanceID, orgID, userID, domain string, trigger TriggerMethod, method, path, requestMethod string, isSystemUser bool) {
logging.WithFields(
"instance", instanceID,
"org", orgID,
"user", userID,
"domain", domain,
"trigger", trigger.String(),
"method", ai.Method,
"path", ai.Path,
"requestMethod", ai.RequestMethod,
"method", method,
"path", path,
"requestMethod", requestMethod,
"isSystemUser", isSystemUser,
).Info(Activity)
}

View File

@@ -28,7 +28,7 @@ import (
type API struct {
port uint16
grpcServer *grpc.Server
verifier *internal_authz.TokenVerifier
verifier internal_authz.APITokenVerifier
health healthCheck
router *mux.Router
http1HostName string
@@ -47,7 +47,7 @@ func New(
port uint16,
router *mux.Router,
queries *query.Queries,
verifier *internal_authz.TokenVerifier,
verifier internal_authz.APITokenVerifier,
authZ internal_authz.Config,
tlsConfig *tls.Config, http2HostName, http1HostName string,
accessInterceptor *http_mw.AccessInterceptor,

View File

@@ -80,7 +80,7 @@ func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, defa
http.Error(w, err.Error(), code)
}
func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
h := &Handler{
commands: commands,
errorHandler: DefaultErrorHandler,

View File

@@ -0,0 +1,44 @@
package authz
import (
"context"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
BearerPrefix = "Bearer "
)
type MembershipsResolver interface {
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
}
type authZRepo interface {
MembershipsResolver
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (string, error)
}
var _ AccessTokenVerifier = (*AccessTokenVerifierFromRepo)(nil)
type AccessTokenVerifierFromRepo struct {
authZRepo authZRepo
}
func StartAccessTokenVerifierFromRepo(authZRepo authZRepo) *AccessTokenVerifierFromRepo {
return &AccessTokenVerifierFromRepo{authZRepo: authZRepo}
}
func (a *AccessTokenVerifierFromRepo) VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
userID, agentID, clientID, prefLang, resourceOwner, err = a.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID())
return userID, clientID, agentID, prefLang, resourceOwner, err
}
type client struct {
name string
}

View File

@@ -2,19 +2,17 @@ package authz
import (
"context"
"sync"
"testing"
"github.com/zitadel/zitadel/internal/errors"
)
func Test_VerifyAccessToken(t *testing.T) {
func Test_extractBearerToken(t *testing.T) {
type args struct {
ctx context.Context
token string
verifier *TokenVerifier
method string
verifier AccessTokenVerifier
}
tests := []struct {
name string
@@ -42,23 +40,16 @@ func Test_VerifyAccessToken(t *testing.T) {
args: args{
ctx: context.Background(),
token: "Bearer AUTH",
verifier: &TokenVerifier{
authZRepo: &testVerifier{memberships: []*Membership{}},
clients: func() sync.Map {
m := sync.Map{}
m.Store("service", &client{name: "name"})
return m
}(),
authMethods: MethodMapping{"/service/method": Option{Permission: "authenticated"}},
},
method: "/service/method",
verifier: AccessTokenVerifierFunc(func(context.Context, string) (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, _, _, _, err := verifyAccessToken(tt.args.ctx, tt.args.token, tt.args.verifier, tt.args.method)
_, err := extractBearerToken(tt.args.token)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}

View File

@@ -0,0 +1,69 @@
package authz
import (
"context"
"sync"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
// TODO: Define interfaces where they are accepted
type APITokenVerifier interface {
AccessTokenVerifier
SystemTokenVerifier
RegisterServer(appName, methodPrefix string, mappings MethodMapping)
CheckAuthMethod(method string) (Option, bool)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error)
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error)
}
type ApiTokenVerifier struct {
AccessTokenVerifier
SystemTokenVerifier
authZRepo authZRepo
clients sync.Map
authMethods MethodMapping
}
func StartAPITokenVerifier(authZRepo authZRepo, accessTokenVerifier AccessTokenVerifier, systemTokenVerifier SystemTokenVerifier) *ApiTokenVerifier {
return &ApiTokenVerifier{
authZRepo: authZRepo,
SystemTokenVerifier: systemTokenVerifier,
AccessTokenVerifier: accessTokenVerifier,
}
}
func (v *ApiTokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) {
v.clients.Store(methodPrefix, &client{name: appName})
if v.authMethods == nil {
v.authMethods = make(map[string]Option)
}
for method, option := range mappings {
v.authMethods[method] = option
}
}
func (v *ApiTokenVerifier) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.SearchMyMemberships(ctx, orgID, shouldTriggerBulk)
}
func (v *ApiTokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
}
func (v *ApiTokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ExistsOrg(ctx, id, domain)
}
func (v *ApiTokenVerifier) CheckAuthMethod(method string) (Option, bool) {
authOpt, ok := v.authMethods[method]
return authOpt, ok
}

View File

@@ -19,11 +19,11 @@ const (
// - the organisation (**either** provided by ID or verified domain) exists
// - the user is permitted to call the requested endpoint (permission option in proto)
// it will pass the [CtxData] and permission of the user into the ctx [context.Context]
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, orgDomain string, verifier APITokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, method)
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier)
if err != nil {
return nil, err
}

View File

@@ -1,12 +1,15 @@
//go:generate enumer -type MemberType -trimprefix MemberType
package authz
import (
"context"
"errors"
"strings"
"github.com/zitadel/zitadel/internal/api/grpc"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/errors"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
@@ -26,10 +29,11 @@ type CtxData struct {
AgentID string
PreferredLanguage string
ResourceOwner string
SystemMemberships Memberships
}
func (ctxData CtxData) IsZero() bool {
return ctxData.UserID == "" || ctxData.OrgID == ""
return ctxData.UserID == "" || ctxData.OrgID == "" && ctxData.SystemMemberships == nil
}
type Grants []*Grant
@@ -54,29 +58,68 @@ type MemberType int32
const (
MemberTypeUnspecified MemberType = iota
MemberTypeOrganisation
MemberTypeOrganization
MemberTypeProject
MemberTypeProjectGrant
MemberTypeIam
MemberTypeIAM
MemberTypeSystem
)
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t *TokenVerifier, method string) (_ CtxData, err error) {
type TokenVerifier interface {
ExistsOrg(ctx context.Context, id, domain string) (string, error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
AccessTokenVerifier
SystemTokenVerifier
}
type AccessTokenVerifier interface {
VerifyAccessToken(ctx context.Context, token string) (userID, clientID, agentID, prefLan, resourceOwner string, err error)
}
// AccessTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a AccessTokenVerifier.
type AccessTokenVerifierFunc func(context.Context, string) (string, string, string, string, string, error)
func (a AccessTokenVerifierFunc) VerifyAccessToken(ctx context.Context, token string) (string, string, string, string, string, error) {
return a(ctx, token)
}
type SystemTokenVerifier interface {
VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error)
}
// SystemTokenVerifierFunc implements the SystemTokenVerifier interface so that a function can be used as a SystemTokenVerifier.
type SystemTokenVerifierFunc func(context.Context, string, string) (Memberships, string, error)
func (s SystemTokenVerifierFunc) VerifySystemToken(ctx context.Context, token string, orgID string) (Memberships, string, error) {
return s(ctx, token, orgID)
}
func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier) (_ CtxData, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
userID, clientID, agentID, prefLang, resourceOwner, err := verifyAccessToken(ctx, token, t, method)
tokenWOBearer, err := extractBearerToken(token)
if err != nil {
return CtxData{}, err
}
if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") {
return CtxData{UserID: userID}, nil
userID, clientID, agentID, prefLang, resourceOwner, err := t.VerifyAccessToken(ctx, tokenWOBearer)
var sysMemberships Memberships
if err != nil && !zitadel_errors.IsUnauthenticated(err) {
return CtxData{}, err
}
if err != nil {
var sysTokenErr error
sysMemberships, userID, sysTokenErr = t.VerifySystemToken(ctx, tokenWOBearer, orgID)
err = errors.Join(err, sysTokenErr)
if sysTokenErr != nil || sysMemberships == nil {
return CtxData{}, err
}
}
var projectID string
var origins []string
if clientID != "" {
projectID, origins, err = t.ProjectIDAndOriginsByClientID(ctx, clientID)
if err != nil {
return CtxData{}, errors.ThrowPermissionDenied(err, "AUTH-GHpw2", "could not read projectid by clientid")
return CtxData{}, zitadel_errors.ThrowPermissionDenied(err, "AUTH-GHpw2", "could not read projectid by clientid")
}
// We used to check origins for every token, but service users shouldn't be used publicly (native app / SPA).
// Therefore, mostly won't send an origin and aren't able to configure them anyway.
@@ -88,21 +131,22 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain st
if orgID == "" && orgDomain == "" {
orgID = resourceOwner
}
verifiedOrgID, err := t.ExistsOrg(ctx, orgID, orgDomain)
if err != nil {
return CtxData{}, errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
// System API calls dont't have a resource owner
if orgID != "" {
orgID, err = t.ExistsOrg(ctx, orgID, orgDomain)
if err != nil {
return CtxData{}, zitadel_errors.ThrowPermissionDenied(nil, "AUTH-Bs7Ds", "Organisation doesn't exist")
}
}
return CtxData{
UserID: userID,
OrgID: verifiedOrgID,
OrgID: orgID,
ProjectID: projectID,
AgentID: agentID,
PreferredLanguage: prefLang,
ResourceOwner: resourceOwner,
SystemMemberships: sysMemberships,
}, nil
}
func SetCtxData(ctx context.Context, ctxData CtxData) context.Context {
@@ -119,11 +163,6 @@ func GetRequestPermissionsFromCtx(ctx context.Context) []string {
return ctxPermission
}
func GetAllPermissionsFromCtx(ctx context.Context) []string {
ctxPermission, _ := ctx.Value(allPermissionsKey).([]string)
return ctxPermission
}
func checkOrigin(ctx context.Context, origins []string) error {
origin := grpc.GetGatewayHeader(ctx, http_util.Origin)
if origin == "" {
@@ -135,5 +174,13 @@ func checkOrigin(ctx context.Context, origins []string) error {
if http_util.IsOriginAllowed(origins, origin) {
return nil
}
return errors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed")
return zitadel_errors.ThrowPermissionDenied(nil, "AUTH-DZG21", "Errors.OriginNotAllowed")
}
func extractBearerToken(token string) (part string, err error) {
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", zitadel_errors.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
}
return parts[1], nil
}

View File

@@ -0,0 +1,94 @@
// Code generated by "enumer -type MemberType -trimprefix MemberType"; DO NOT EDIT.
package authz
import (
"fmt"
"strings"
)
const _MemberTypeName = "UnspecifiedOrganizationProjectProjectGrantIAMSystem"
var _MemberTypeIndex = [...]uint8{0, 11, 23, 30, 42, 45, 51}
const _MemberTypeLowerName = "unspecifiedorganizationprojectprojectgrantiamsystem"
func (i MemberType) String() string {
if i < 0 || i >= MemberType(len(_MemberTypeIndex)-1) {
return fmt.Sprintf("MemberType(%d)", i)
}
return _MemberTypeName[_MemberTypeIndex[i]:_MemberTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _MemberTypeNoOp() {
var x [1]struct{}
_ = x[MemberTypeUnspecified-(0)]
_ = x[MemberTypeOrganization-(1)]
_ = x[MemberTypeProject-(2)]
_ = x[MemberTypeProjectGrant-(3)]
_ = x[MemberTypeIAM-(4)]
_ = x[MemberTypeSystem-(5)]
}
var _MemberTypeValues = []MemberType{MemberTypeUnspecified, MemberTypeOrganization, MemberTypeProject, MemberTypeProjectGrant, MemberTypeIAM, MemberTypeSystem}
var _MemberTypeNameToValueMap = map[string]MemberType{
_MemberTypeName[0:11]: MemberTypeUnspecified,
_MemberTypeLowerName[0:11]: MemberTypeUnspecified,
_MemberTypeName[11:23]: MemberTypeOrganization,
_MemberTypeLowerName[11:23]: MemberTypeOrganization,
_MemberTypeName[23:30]: MemberTypeProject,
_MemberTypeLowerName[23:30]: MemberTypeProject,
_MemberTypeName[30:42]: MemberTypeProjectGrant,
_MemberTypeLowerName[30:42]: MemberTypeProjectGrant,
_MemberTypeName[42:45]: MemberTypeIAM,
_MemberTypeLowerName[42:45]: MemberTypeIAM,
_MemberTypeName[45:51]: MemberTypeSystem,
_MemberTypeLowerName[45:51]: MemberTypeSystem,
}
var _MemberTypeNames = []string{
_MemberTypeName[0:11],
_MemberTypeName[11:23],
_MemberTypeName[23:30],
_MemberTypeName[30:42],
_MemberTypeName[42:45],
_MemberTypeName[45:51],
}
// MemberTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func MemberTypeString(s string) (MemberType, error) {
if val, ok := _MemberTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _MemberTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to MemberType values", s)
}
// MemberTypeValues returns all values of the enum
func MemberTypeValues() []MemberType {
return _MemberTypeValues
}
// MemberTypeStrings returns a slice of all String values of the enum
func MemberTypeStrings() []string {
strs := make([]string, len(_MemberTypeNames))
copy(strs, _MemberTypeNames)
return strs
}
// IsAMemberType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i MemberType) IsAMemberType() bool {
for _, v := range _MemberTypeValues {
if i == v {
return true
}
}
return false
}

View File

@@ -30,6 +30,11 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi
return nil, nil, errors.ThrowUnauthenticated(nil, "AUTH-rKLWEH", "context missing")
}
if ctxData.SystemMemberships != nil {
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings)
return requestedPermissions, allPermissions, nil
}
ctx = context.WithValue(ctx, dataKey, ctxData)
memberships, err := resolver.SearchMyMemberships(ctx, orgID, false)
if err != nil {

View File

@@ -7,33 +7,6 @@ import (
caos_errs "github.com/zitadel/zitadel/internal/errors"
)
func getTestCtx(userID, orgID string) context.Context {
return context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID})
}
type testVerifier struct {
memberships []*Membership
}
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
return "userID", "agentID", "clientID", "de", "orgID", nil
}
func (v *testVerifier) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*Membership, error) {
return v.memberships, nil
}
func (v *testVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *testVerifier) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil
}
func (v *testVerifier) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "clientID", "projectID", nil
}
func equalStringArray(a, b []string) bool {
if len(a) != len(b) {
return false
@@ -46,12 +19,18 @@ func equalStringArray(a, b []string) bool {
return true
}
type membershipsResolverFunc func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
func (m membershipsResolverFunc) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return m(ctx, orgID, shouldTriggerBulk)
}
func Test_GetUserPermissions(t *testing.T) {
type args struct {
ctxData CtxData
verifier *TokenVerifier
requiredPerm string
authConfig Config
ctxData CtxData
membershipsResolver MembershipsResolver
requiredPerm string
authConfig Config
}
tests := []struct {
name string
@@ -64,11 +43,9 @@ func Test_GetUserPermissions(t *testing.T) {
name: "Empty Context",
args: args{
ctxData: CtxData{},
verifier: Start(&testVerifier{memberships: []*Membership{
{
Roles: []string{"ORG_OWNER"},
},
}}, "", nil),
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{{Roles: []string{"ORG_OWNER"}}}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
@@ -90,8 +67,10 @@ func Test_GetUserPermissions(t *testing.T) {
{
name: "No Grants",
args: args{
ctxData: CtxData{},
verifier: Start(&testVerifier{memberships: []*Membership{}}, "", nil),
ctxData: CtxData{},
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
@@ -112,14 +91,16 @@ func Test_GetUserPermissions(t *testing.T) {
name: "Get Permissions",
args: args{
ctxData: CtxData{UserID: "userID", OrgID: "orgID"},
verifier: Start(&testVerifier{memberships: []*Membership{
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIam,
Roles: []string{"IAM_OWNER"},
},
}}, "", nil),
membershipsResolver: membershipsResolverFunc(func(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error) {
return []*Membership{
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIAM,
Roles: []string{"IAM_OWNER"},
},
}, nil
}),
requiredPerm: "project.read",
authConfig: Config{
RolePermissionMappings: []RoleMapping{
@@ -139,7 +120,7 @@ func Test_GetUserPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, perms, err := getUserPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
_, perms, err := getUserPermissions(context.Background(), tt.args.membershipsResolver, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
@@ -176,7 +157,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
},
@@ -204,7 +185,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
},
@@ -232,13 +213,13 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "1",
ObjectID: "1",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
{
AggregateID: "IAM",
ObjectID: "IAM",
MemberType: MemberTypeIam,
MemberType: MemberTypeIAM,
Roles: []string{"IAM_OWNER"},
},
},
@@ -266,7 +247,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
{
AggregateID: "2",
ObjectID: "2",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
{
@@ -327,7 +308,7 @@ func Test_MapMembershipToPerm(t *testing.T) {
membership: &Membership{
AggregateID: "Org",
ObjectID: "Org",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
authConfig: Config{
@@ -355,7 +336,7 @@ func Test_MapMembershipToPerm(t *testing.T) {
membership: &Membership{
AggregateID: "Org",
ObjectID: "Org",
MemberType: MemberTypeOrganisation,
MemberType: MemberTypeOrganization,
Roles: []string{"ORG_OWNER"},
},
authConfig: Config{

View File

@@ -0,0 +1,32 @@
package authz
import (
"context"
"encoding/base64"
"fmt"
"github.com/zitadel/zitadel/internal/crypto"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
SessionTokenPrefix = "sess_"
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
if err != nil {
return err
}
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
token, err := algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
spanPasswordComparison.EndWithError(err)
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
return zitadel_errors.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
}
return nil
}
}

View File

@@ -0,0 +1,117 @@
package authz
import (
"context"
"crypto/rsa"
"errors"
"os"
"sync"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/crypto"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
)
var _ SystemTokenVerifier = (*SystemTokenVerifierFromConfig)(nil)
type SystemTokenVerifierFromConfig struct {
systemJWTProfile *op.JWTProfileVerifier
systemUsers map[string]Memberships
}
func StartSystemTokenVerifierFromConfig(issuer string, keys map[string]*SystemAPIUser) (*SystemTokenVerifierFromConfig, error) {
systemUsers := make(map[string]Memberships, len(keys))
for userID, key := range keys {
if len(key.Memberships) == 0 {
systemUsers[userID] = Memberships{{MemberType: MemberTypeSystem, Roles: []string{"SYSTEM_OWNER"}}}
continue
}
for _, membership := range key.Memberships {
switch membership.MemberType {
case MemberTypeSystem, MemberTypeIAM, MemberTypeOrganization:
systemUsers[userID] = key.Memberships
case MemberTypeUnspecified, MemberTypeProject, MemberTypeProjectGrant:
return nil, errors.New("for system users, only the membership types System, IAM and Organization are supported")
default:
return nil, errors.New("unknown membership type")
}
}
}
return &SystemTokenVerifierFromConfig{
systemJWTProfile: op.NewJWTProfileVerifier(
&systemJWTStorage{
keys: keys,
cachedKeys: make(map[string]*rsa.PublicKey),
},
issuer,
1*time.Hour,
time.Second,
),
systemUsers: systemUsers,
}, nil
}
func (s *SystemTokenVerifierFromConfig) VerifySystemToken(ctx context.Context, token string, orgID string) (matchingMemberships Memberships, userID string, err error) {
jwtReq, err := op.VerifyJWTAssertion(ctx, token, s.systemJWTProfile)
if err != nil {
return nil, "", err
}
systemUserMemberships, ok := s.systemUsers[jwtReq.Subject]
if !ok {
return nil, "", zitadel_errors.ThrowPermissionDenied(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
}
matchingMemberships = make(Memberships, 0, len(systemUserMemberships))
for _, membership := range systemUserMemberships {
if membership.MemberType == MemberTypeSystem ||
membership.MemberType == MemberTypeIAM && GetInstance(ctx).InstanceID() == membership.AggregateID ||
membership.MemberType == MemberTypeOrganization && orgID == membership.AggregateID {
matchingMemberships = append(matchingMemberships, membership)
}
}
return matchingMemberships, jwtReq.Subject, nil
}
type systemJWTStorage struct {
keys map[string]*SystemAPIUser
mutex sync.Mutex
cachedKeys map[string]*rsa.PublicKey
}
type SystemAPIUser struct {
Path string // if a path is specified, the key will be read from that path
KeyData []byte // else you can also specify the data directly in the KeyData
Memberships Memberships
}
func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) {
if s.Path != "" {
var err error
s.KeyData, err = os.ReadFile(s.Path)
if err != nil {
return nil, zitadel_errors.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound")
}
}
return crypto.BytesToPublicKey(s.KeyData)
}
func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) {
cachedKey, ok := s.cachedKeys[userID]
if ok {
return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil
}
key, ok := s.keys[userID]
if !ok {
return nil, zitadel_errors.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound")
}
s.mutex.Lock()
defer s.mutex.Unlock()
publicKey, err := key.readKey()
if err != nil {
return nil, err
}
s.cachedKeys[userID] = publicKey
return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil
}

View File

@@ -1,188 +0,0 @@
package authz
import (
"context"
"crypto/rsa"
"encoding/base64"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/crypto"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
BearerPrefix = "Bearer "
SessionTokenPrefix = "sess_"
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
type TokenVerifier struct {
authZRepo authZRepo
clients sync.Map
authMethods MethodMapping
systemJWTProfile *op.JWTProfileVerifier
}
type MembershipsResolver interface {
SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) ([]*Membership, error)
}
type authZRepo interface {
MembershipsResolver
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, id, domain string) (string, error)
}
func Start(authZRepo authZRepo, issuer string, keys map[string]*SystemAPIUser) (v *TokenVerifier) {
return &TokenVerifier{
authZRepo: authZRepo,
systemJWTProfile: op.NewJWTProfileVerifier(
&systemJWTStorage{
keys: keys,
cachedKeys: make(map[string]*rsa.PublicKey),
},
issuer,
1*time.Hour,
time.Second,
),
}
}
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) {
if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") {
userID, err := v.verifySystemToken(ctx, token)
if err != nil {
return "", "", "", "", "", err
}
return userID, "", "", "", "", nil
}
userID, agentID, clientID, prefLang, resourceOwner, err = v.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID())
return userID, clientID, agentID, prefLang, resourceOwner, err
}
func (v *TokenVerifier) verifySystemToken(ctx context.Context, token string) (string, error) {
jwtReq, err := op.VerifyJWTAssertion(ctx, token, v.systemJWTProfile)
if err != nil {
return "", err
}
return jwtReq.Subject, nil
}
type systemJWTStorage struct {
keys map[string]*SystemAPIUser
mutex sync.Mutex
cachedKeys map[string]*rsa.PublicKey
}
type SystemAPIUser struct {
Path string //if a path is specified, the key will be read from that path
KeyData []byte //else you can also specify the data directly in the KeyData
}
func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) {
if s.Path != "" {
var err error
s.KeyData, err = os.ReadFile(s.Path)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound")
}
}
return crypto.BytesToPublicKey(s.KeyData)
}
func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) {
cachedKey, ok := s.cachedKeys[userID]
if ok {
return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil
}
key, ok := s.keys[userID]
if !ok {
return nil, caos_errs.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound")
}
defer s.mutex.Unlock()
s.mutex.Lock()
publicKey, err := key.readKey()
if err != nil {
return nil, err
}
s.cachedKeys[userID] = publicKey
return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil
}
type client struct {
id string
projectID string
name string
}
func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) {
v.clients.Store(methodPrefix, &client{name: appName})
if v.authMethods == nil {
v.authMethods = make(map[string]Option)
}
for method, option := range mappings {
v.authMethods[method] = option
}
}
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string, shouldTriggerBulk bool) (_ []*Membership, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.SearchMyMemberships(ctx, orgID, shouldTriggerBulk)
}
func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
}
func (v *TokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.ExistsOrg(ctx, id, domain)
}
func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) {
authOpt, ok := v.authMethods[method]
return authOpt, ok
}
func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, method string) (userID, clientID, agentID, prefLan, resourceOwner string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
}
return t.VerifyAccessToken(ctx, parts[1], method)
}
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
if err != nil {
return err
}
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
var token string
token, err = algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
spanPasswordComparison.EndWithError(err)
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
return caos_errs.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
}
return nil
}
}

View File

@@ -77,5 +77,9 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
}
func (s *Server) GatewayPathPrefix() string {
return GatewayPathPrefix()
}
func GatewayPathPrefix() string {
return "/auth/v1"
}

View File

@@ -13,13 +13,13 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor {
func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return authorize(ctx, req, info, handler, verifier, authConfig)
}
}
func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier *authz.TokenVerifier, authConfig authz.Config) (_ interface{}, err error) {
func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) {
authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod)
if !needsToken {
return handler(ctx, req)

View File

@@ -2,6 +2,7 @@ package middleware
import (
"context"
"errors"
"reflect"
"testing"
@@ -9,44 +10,54 @@ import (
"google.golang.org/grpc/metadata"
"github.com/zitadel/zitadel/internal/api/authz"
zitadel_errors "github.com/zitadel/zitadel/internal/errors"
)
var (
mockMethods = authz.MethodMapping{
"need.authentication": authz.Option{
Permission: "authenticated",
},
}
)
const anAPIRole = "AN_API_ROLE"
type verifierMock struct{}
type authzRepoMock struct{}
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}
func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) {
return nil, nil
func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) {
return authz.Memberships{{
MemberType: authz.MemberTypeOrganization,
AggregateID: orgID,
Roles: []string{anAPIRole},
}}, nil
}
func (v *verifierMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil
}
func (v *verifierMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil
}
func (v *verifierMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil
}
var (
accessTokenOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) {
return "user1", "", "", "", "org1", nil
})
accessTokenNOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) {
return "", "", "", "", "", zitadel_errors.ThrowUnauthenticated(nil, "TEST-fQHDI", "unauthenticaded")
})
systemTokenNOK = authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) {
return nil, "", errors.New("system token error")
})
)
func Test_authorize(t *testing.T) {
type args struct {
ctx context.Context
req interface{}
info *grpc.UnaryServerInfo
handler grpc.UnaryHandler
verifier *authz.TokenVerifier
authConfig authz.Config
authMethods authz.MethodMapping
ctx context.Context
req interface{}
info *grpc.UnaryServerInfo
handler grpc.UnaryHandler
verifier func() authz.APITokenVerifier
authConfig authz.Config
}
type res struct {
want interface{}
@@ -64,12 +75,11 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/no/token/needed"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{})
return verifier
}(),
authMethods: mockMethods,
},
},
res{
&mockReq{},
@@ -83,13 +93,12 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
return verifier
}(),
authConfig: authz.Config{},
authMethods: mockMethods,
},
authConfig: authz.Config{},
},
res{
nil,
@@ -103,13 +112,12 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
return verifier
}(),
authConfig: authz.Config{},
authMethods: mockMethods,
},
authConfig: authz.Config{},
},
res{
nil,
@@ -123,13 +131,118 @@ func Test_authorize(t *testing.T) {
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() *authz.TokenVerifier {
verifier := authz.Start(&verifierMock{}, "", nil)
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}})
return verifier
}(),
authConfig: authz.Config{},
authMethods: mockMethods,
},
authConfig: authz.Config{},
},
res{
&mockReq{},
false,
},
},
{
"permission denied error",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: anAPIRole,
Permissions: []string{"to.do.something.else"},
}},
},
},
res{
nil,
true,
},
},
{
"permission ok",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK)
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: anAPIRole,
Permissions: []string{"to.do.something"},
}},
},
},
res{
&mockReq{},
false,
},
},
{
"system token permission denied error",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) {
return authz.Memberships{{
MemberType: authz.MemberTypeSystem,
Roles: []string{"A_SYSTEM_ROLE"},
}}, "systemuser", nil
}))
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: "A_SYSTEM_ROLE",
Permissions: []string{"to.do.something.else"},
}},
},
},
res{
nil,
true,
},
},
{
"system token permission denied error",
args{
ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorization", "Bearer token")),
req: &mockReq{},
info: mockInfo("/need/authentication"),
handler: emptyMockHandler,
verifier: func() authz.APITokenVerifier {
verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) {
return authz.Memberships{{
MemberType: authz.MemberTypeSystem,
Roles: []string{"A_SYSTEM_ROLE"},
}}, "systemuser", nil
}))
verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}})
return verifier
},
authConfig: authz.Config{
RolePermissionMappings: []authz.RoleMapping{{
Role: "A_SYSTEM_ROLE",
Permissions: []string{"to.do.something"},
}},
},
},
res{
&mockReq{},
@@ -139,7 +252,7 @@ func Test_authorize(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier, tt.args.authConfig)
got, err := authorize(tt.args.ctx, tt.args.req, tt.args.info, tt.args.handler, tt.args.verifier(), tt.args.authConfig)
if (err != nil) != tt.res.wantErr {
t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr)
return

View File

@@ -28,7 +28,9 @@ func QuotaExhaustedInterceptor(svc *logstore.Service[*record.AccessLog], ignoreS
// The auth interceptor will ensure that only authorized or public requests are allowed.
// So if there's no authorization context, we don't need to check for limitation
if authz.GetCtxData(ctx).IsZero() {
// Also, we don't limit calls with system user tokens
ctxData := authz.GetCtxData(ctx)
if ctxData.IsZero() || ctxData.SystemMemberships != nil {
return handler(ctx, req)
}

View File

@@ -35,7 +35,7 @@ type WithGatewayPrefix interface {
}
func CreateServer(
verifier *authz.TokenVerifier,
verifier authz.APITokenVerifier,
authConfig authz.Config,
queries *query.Queries,
hostHeaderName string,

View File

@@ -2,31 +2,13 @@ package middleware
import (
"net/http"
"strings"
"github.com/zitadel/zitadel/internal/api/info"
)
func ActivityHandler(handlerPrefixes []string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
activityInfo := info.ActivityInfoFromContext(ctx)
hasPrefix := false
// only add path to context if handler is called
for _, prefix := range handlerPrefixes {
if strings.HasPrefix(r.URL.Path, prefix) {
activityInfo.SetPath(r.URL.Path)
hasPrefix = true
break
}
}
// last call is with grpc method as path
if !hasPrefix {
activityInfo.SetMethod(r.URL.Path)
}
ctx = activityInfo.SetRequestMethod(r.Method).IntoContext(ctx)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func ActivityHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := info.ActivityInfoFromContext(r.Context()).SetPath(r.URL.Path).SetRequestMethod(r.Method).IntoContext(r.Context())
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -11,11 +11,11 @@ import (
)
type AuthInterceptor struct {
verifier *authz.TokenVerifier
verifier authz.APITokenVerifier
authConfig authz.Config
}
func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) *AuthInterceptor {
func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) *AuthInterceptor {
return &AuthInterceptor{
verifier: verifier,
authConfig: authConfig,
@@ -48,7 +48,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.HandlerFunc) http.HandlerFunc {
type httpReq struct{}
func authorize(r *http.Request, verifier *authz.TokenVerifier, authConfig authz.Config) (_ context.Context, err error) {
func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig authz.Config) (_ context.Context, err error) {
ctx := r.Context()
authOpt, needsToken := verifier.CheckAuthMethod(r.Method + ":" + r.RequestURI)
if !needsToken {

View File

@@ -51,7 +51,7 @@ func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context, orgID
func userMembershipToMembership(membership *query.Membership) *authz.Membership {
if membership.IAM != nil {
return &authz.Membership{
MemberType: authz.MemberTypeIam,
MemberType: authz.MemberTypeIAM,
AggregateID: membership.IAM.IAMID,
ObjectID: membership.IAM.IAMID,
Roles: membership.Roles,
@@ -59,7 +59,7 @@ func userMembershipToMembership(membership *query.Membership) *authz.Membership
}
if membership.Org != nil {
return &authz.Membership{
MemberType: authz.MemberTypeOrganisation,
MemberType: authz.MemberTypeOrganization,
AggregateID: membership.Org.OrgID,
ObjectID: membership.Org.OrgID,
Roles: membership.Roles,

View File

@@ -4,11 +4,10 @@ import (
"reflect"
"github.com/mitchellh/mapstructure"
"github.com/zitadel/zitadel/internal/domain"
"golang.org/x/exp/constraints"
)
func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType {
func EnumHookFunc[T constraints.Integer](resolve func(string) (T, error)) mapstructure.DecodeHookFuncType {
return func(
f reflect.Type,
t reflect.Type,
@@ -17,11 +16,9 @@ func StringToFeatureHookFunc() mapstructure.DecodeHookFuncType {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(domain.FeatureUnspecified) {
if t != reflect.TypeOf(T(0)) {
return data, nil
}
return domain.FeatureString(data.(string))
return resolve(data.(string))
}
}