From 4980cd6a0c9a1a58f3657326a28ce667350c196f Mon Sep 17 00:00:00 2001 From: Elio Bischof Date: Wed, 25 Oct 2023 17:10:45 +0200 Subject: [PATCH] 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 --- cmd/defaults.yaml | 45 +++- cmd/ready/config.go | 5 +- cmd/setup/config.go | 6 +- cmd/start/config.go | 4 +- cmd/start/config_test.go | 84 ++++++++ cmd/start/start.go | 9 +- .../integrate/access-zitadel-system-api.md | 29 ++- docs/docs/support/advisory/a10007.md | 63 ++++++ docs/docs/support/technical_advisory.mdx | 12 ++ internal/activity/activity.go | 42 ++-- internal/api/api.go | 4 +- internal/api/assets/asset.go | 2 +- internal/api/authz/access_token.go | 44 ++++ .../{token_test.go => access_token_test.go} | 21 +- internal/api/authz/api_token_verifier.go | 69 +++++++ internal/api/authz/authorization.go | 4 +- internal/api/authz/context.go | 93 ++++++--- internal/api/authz/membertype_enumer.go | 94 +++++++++ internal/api/authz/permissions.go | 5 + internal/api/authz/permissions_test.go | 89 ++++---- internal/api/authz/session_token.go | 32 +++ internal/api/authz/system_token.go | 117 +++++++++++ internal/api/authz/token.go | 188 ----------------- internal/api/grpc/auth/server.go | 4 + .../server/middleware/auth_interceptor.go | 4 +- .../middleware/auth_interceptor_test.go | 195 ++++++++++++++---- .../server/middleware/quota_interceptor.go | 4 +- internal/api/grpc/server/server.go | 2 +- .../http/middleware/activity_interceptor.go | 28 +-- .../api/http/middleware/auth_interceptor.go | 6 +- .../eventstore/user_membership.go | 4 +- internal/config/hook/{feature.go => enum.go} | 11 +- proto/zitadel/admin.proto | 4 + proto/zitadel/system.proto | 46 +++-- 34 files changed, 959 insertions(+), 410 deletions(-) create mode 100644 cmd/start/config_test.go create mode 100644 docs/docs/support/advisory/a10007.md create mode 100644 internal/api/authz/access_token.go rename internal/api/authz/{token_test.go => access_token_test.go} (65%) create mode 100644 internal/api/authz/api_token_verifier.go create mode 100644 internal/api/authz/membertype_enumer.go create mode 100644 internal/api/authz/session_token.go create mode 100644 internal/api/authz/system_token.go delete mode 100644 internal/api/authz/token.go rename internal/config/hook/{feature.go => enum.go} (54%) diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 4baf021b9e..71908f7853 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -389,11 +389,27 @@ EncryptionKeys: UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID SystemAPIUsers: -# Add keys for authentication of the systemAPI here: -# you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT: +# # Add keys for authentication of the systemAPI here: +# # you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT: # - superuser: -# Path: /path/to/superuser/key.pem # you can provide the key either by reference with the path +# Path: /path/to/superuser/ey.pem # you can provide the key either by reference with the path +# Memberships: +# # MemberType System allows the user to access all APIs for all instances or organizations +# - MemberType: System +# Roles: +# - "SYSTEM_OWNER" +# # Actually, we don't recommend adding IAM_OWNER and ORG_OWNER to the System membership, as this basically enables god mode for the system user +# - "IAM_OWNER" +# - "ORG_OWNER" +# # MemberType IAM and Organization let you restrict access to a specific instance or organization by specifying the AggregateID +# - MemberType: IAM +# Roles: "IAM_OWNER" +# AggregateID: "123456789012345678" +# - MemberType: Organization +# Roles: "ORG_OWNER" +# AggregateID: "123456789012345678" # - superuser2: +# # If no memberships are specified, the user has a membership of type System with the role "SYSTEM_OWNER" # KeyData: # or you can directly embed it as base64 encoded value #TODO: remove as soon as possible @@ -841,6 +857,29 @@ AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION InternalAuthZ: RolePermissionMappings: + - Role: "SYSTEM_OWNER" + Permissions: + - "system.instance.read" + - "system.instance.write" + - "system.instance.delete" + - "system.domain.read" + - "system.domain.write" + - "system.domain.delete" + - "system.debug.read" + - "system.debug.write" + - "system.debug.delete" + - "system.feature.write" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + - Role: "SYSTEM_OWNER_VIEWER" + Permissions: + - "system.instance.read" + - "system.domain.read" + - "system.debug.read" + - "system.iam.member.read" - Role: "IAM_OWNER" Permissions: - "iam.read" diff --git a/cmd/ready/config.go b/cmd/ready/config.go index b9137519ee..ea4c1290de 100644 --- a/cmd/ready/config.go +++ b/cmd/ready/config.go @@ -7,7 +7,9 @@ import ( "github.com/spf13/viper" "github.com/zitadel/logging" + internal_authz "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/config/hook" + "github.com/zitadel/zitadel/internal/domain" ) type Config struct { @@ -23,7 +25,8 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), + hook.EnumHookFunc(internal_authz.MemberTypeString), )), ) logging.OnError(err).Fatal("unable to read default config") diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 4ff7c0232b..61930baecf 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -15,6 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/query/projection" @@ -45,7 +46,8 @@ func MustNewConfig(v *viper.Viper) *Config { mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), database.DecodeHook, - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), + hook.EnumHookFunc(authz.MemberTypeString), )), ) logging.OnError(err).Fatal("unable to read default config") @@ -101,7 +103,7 @@ func MustNewSteps(v *viper.Viper) *Steps { mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), )), ) logging.OnError(err).Fatal("unable to read steps") diff --git a/cmd/start/config.go b/cmd/start/config.go index 6ef844feed..a682e09efd 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -24,6 +24,7 @@ import ( "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/logstore" @@ -92,7 +93,8 @@ func MustNewConfig(v *viper.Viper) *Config { database.DecodeHook, actions.HTTPConfigDecodeHook, systemAPIUsersDecodeHook, - hook.StringToFeatureHookFunc(), + hook.EnumHookFunc(domain.FeatureString), + hook.EnumHookFunc(internal_authz.MemberTypeString), )), ) logging.OnError(err).Fatal("unable to read config") diff --git a/cmd/start/config_test.go b/cmd/start/config_test.go new file mode 100644 index 0000000000..cfbf877ab5 --- /dev/null +++ b/cmd/start/config_test.go @@ -0,0 +1,84 @@ +package start + +import ( + "reflect" + "strings" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/actions" + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" +) + +func TestMustNewConfig(t *testing.T) { + type args struct { + yaml string + } + tests := []struct { + name string + args args + want *Config + }{{ + name: "features ok", + args: args{yaml: ` +DefaultInstance: + Features: + - FeatureLoginDefaultOrg: true +`}, + want: &Config{ + DefaultInstance: command.InstanceSetup{ + Features: map[domain.Feature]any{ + domain.FeatureLoginDefaultOrg: true, + }, + }, + }, + }, { + name: "membership types ok", + args: args{yaml: ` +SystemAPIUsers: +- superuser: + Memberships: + - MemberType: System + - MemberType: Organization + - MemberType: IAM +`}, + want: &Config{ + SystemAPIUsers: map[string]*authz.SystemAPIUser{ + "superuser": { + Memberships: authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + }, { + MemberType: authz.MemberTypeOrganization, + }, { + MemberType: authz.MemberTypeIAM, + }}, + }, + }, + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := viper.New() + v.SetConfigType("yaml") + err := v.ReadConfig(strings.NewReader(`Log: + Level: info +Actions: + HTTP: + DenyList: [] +` + tt.args.yaml)) + require.NoError(t, err) + tt.want.Log = &logging.Config{Level: "info"} + tt.want.Actions = &actions.Config{HTTP: actions.HTTPConfig{DenyList: []actions.AddressChecker{}}} + require.NoError(t, tt.want.Log.SetLogger()) + got := MustNewConfig(v) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MustNewConfig() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/start/start.go b/cmd/start/start.go index 9da5c114ff..5ed358086b 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -320,8 +320,13 @@ func startAPIs( // always set the origin in the context if available in the http headers, no matter for what protocol router.Use(middleware.OriginHandler) // adds used HTTPPathPattern and RequestMethod to context - router.Use(middleware.ActivityHandler(append(oidcPrefixes, saml.HandlerPrefix, admin.GatewayPathPrefix(), management.GatewayPathPrefix()))) - verifier := internal_authz.Start(repo, http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) + router.Use(middleware.ActivityHandler) + systemTokenVerifier, err := internal_authz.StartSystemTokenVerifierFromConfig(http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), config.SystemAPIUsers) + if err != nil { + return err + } + accessTokenVerifer := internal_authz.StartAccessTokenVerifierFromRepo(repo) + verifier := internal_authz.StartAPITokenVerifier(repo, accessTokenVerifer, systemTokenVerifier) tlsConfig, err := config.TLS.Config() if err != nil { return err diff --git a/docs/docs/guides/integrate/access-zitadel-system-api.md b/docs/docs/guides/integrate/access-zitadel-system-api.md index 08bef3656c..821dbc0d3c 100644 --- a/docs/docs/guides/integrate/access-zitadel-system-api.md +++ b/docs/docs/guides/integrate/access-zitadel-system-api.md @@ -49,6 +49,33 @@ SystemAPIUsers: KeyData: ``` +You can define memberships for the user as well: + +```yaml +SystemAPIUsers: + - system-user-1: + Path: /system-user-1.pub + Memberships: + # MemberType System allows the user to access all APIs for all instances or organizations + - MemberType: System + Roles: + - "SYSTEM_OWNER" + - "IAM_OWNER" + - "ORG_OWNER" + # MemberType IAM and Organization let you restrict access to a specific instance or organization by specifying the AggregateID + - MemberType: IAM + Roles: "IAM_OWNER" + AggregateID: "123456789012345678" + - MemberType: Organization + Roles: "ORG_OWNER" + AggregateID: "123456789012345678" + - superuser2: + # If no memberships are specified, the user has a membership of type System with the role "SYSTEM_OWNER" + KeyData: # or you can directly embed it as base64 encoded value +``` + +If you don't specify any memberships, you are allowed to access the whole [ZITADEL System API](/apis/resources/system). + ## Generate JWT Similar to the OAuth 2.0 JWT Profile, we will create and sign a JWT. For this API, the JWT will not be used to authenticate against ZITADEL Authorization Server, but rather directly to the API itself. @@ -145,8 +172,6 @@ You should get a successful response with a `totalResult` number of 1 and the de } ``` -With this token you are allowed to access the whole [ZITADEL System API](/apis/resources/system). - ## Summary * Create an RSA keypair diff --git a/docs/docs/support/advisory/a10007.md b/docs/docs/support/advisory/a10007.md new file mode 100644 index 0000000000..66a9d8eb5a --- /dev/null +++ b/docs/docs/support/advisory/a10007.md @@ -0,0 +1,63 @@ +--- +title: Technical Advisory 10007 +--- + +## Date and Version + +Version: Upcoming + +Date: Upcoming + +## Affected Users + +This advisory applies to self-hosted ZITADEL installations with custom roles to permissions mappings in the *InternalAuthZ.RolePermissionMappings* configuration section. + +## Description + +In upcoming ZITADEL versions, RBAC also applies to [system users defined in the ZITADEL runtime configuration](/guides/integrate/access-zitadel-system-api#runtime-configuration). +This enables fine grained access control to the system API as well as other APIs for system users. +ZITADEL defines the new default roles *SYSTEM_OWNER* and *SYSTEM_OWNER_VIEWER*. +System users without any memberships defined in the configuration will be assigned the *SYSTEM_OWNER* role. +**Self-hosting users who define their own custom mapping at the *InternalAuthZ.RolePermissionMappings* configuration section**, have to define the *SYSTEM_OWNER* role in their configuration too to be able to access the system API with the default system user membership. + +## Statement + +This change is tracked in the following PR: [feat: add SYSTEM_OWNER role](https://github.com/zitadel/zitadel/pull/6765). +As soon as the release version is published, we will include the version here. + +## Mitigation + +If you have a custom role mapping configured, make sure you configure the new role *SYSTEM_OWNER* before migrating to upcoming ZITADEL versions. +As a reference, these are the default mappings: + +```yaml +InternalAuthZ: + RolePermissionMappings: + - Role: "SYSTEM_OWNER" + Permissions: + - "system.instance.read" + - "system.instance.write" + - "system.instance.delete" + - "system.domain.read" + - "system.domain.write" + - "system.domain.delete" + - "system.debug.read" + - "system.debug.write" + - "system.debug.delete" + - "system.feature.write" + - "system.limits.write" + - "system.limits.delete" + - "system.quota.write" + - "system.quota.delete" + - "system.iam.member.read" + - Role: "SYSTEM_OWNER_VIEWER" + Permissions: + - "system.instance.read" + - "system.domain.read" + - "system.debug.read" +... +``` + +## Impact + +If the system users don't have the correct memberships and roles which resolve to permissions, the system users lose access to the system API. diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index a359b2ee83..28eb02a2d1 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -130,6 +130,18 @@ We understand that these advisories may include breaking changes, and we aim to 2.39.0 Calendar week 41/42 2023 + + + A-10006 + + Additional grant to cockroach database user + Breaking Behaviour Change + + Upcoming Versions require the SYSTEM_OWNER role to be available in the permission role mappings. Self-hosting ZITADEL users who define custom permission role mappings need to make sure their system users don't lose access to the system API. + + Upcoming + Upcoming + ## Subscribe to our Mailing List diff --git a/internal/activity/activity.go b/internal/activity/activity.go index 4ceb681cf7..2bea722414 100644 --- a/internal/activity/activity.go +++ b/internal/activity/activity.go @@ -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) } diff --git a/internal/api/api.go b/internal/api/api.go index 9952863eb7..aaa3cd66d3 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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, diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 9b73a93e7c..16ec99c41f 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -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, diff --git a/internal/api/authz/access_token.go b/internal/api/authz/access_token.go new file mode 100644 index 0000000000..de7634c36d --- /dev/null +++ b/internal/api/authz/access_token.go @@ -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 +} diff --git a/internal/api/authz/token_test.go b/internal/api/authz/access_token_test.go similarity index 65% rename from internal/api/authz/token_test.go rename to internal/api/authz/access_token_test.go index 04e104ec2b..54f3c6518c 100644 --- a/internal/api/authz/token_test.go +++ b/internal/api/authz/access_token_test.go @@ -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) } diff --git a/internal/api/authz/api_token_verifier.go b/internal/api/authz/api_token_verifier.go new file mode 100644 index 0000000000..4e25a30aac --- /dev/null +++ b/internal/api/authz/api_token_verifier.go @@ -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 +} diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index 2db8f14ad4..ad55ab976c 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -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 } diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index a916dfcc8a..0aa22943dd 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -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 } diff --git a/internal/api/authz/membertype_enumer.go b/internal/api/authz/membertype_enumer.go new file mode 100644 index 0000000000..5de4c92292 --- /dev/null +++ b/internal/api/authz/membertype_enumer.go @@ -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 +} diff --git a/internal/api/authz/permissions.go b/internal/api/authz/permissions.go index a7f26b2d0b..8400efe8ff 100644 --- a/internal/api/authz/permissions.go +++ b/internal/api/authz/permissions.go @@ -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 { diff --git a/internal/api/authz/permissions_test.go b/internal/api/authz/permissions_test.go index 41b1ff2b8f..a500f6a4fb 100644 --- a/internal/api/authz/permissions_test.go +++ b/internal/api/authz/permissions_test.go @@ -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{ diff --git a/internal/api/authz/session_token.go b/internal/api/authz/session_token.go new file mode 100644 index 0000000000..1691828513 --- /dev/null +++ b/internal/api/authz/session_token.go @@ -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 + } +} diff --git a/internal/api/authz/system_token.go b/internal/api/authz/system_token.go new file mode 100644 index 0000000000..08d7bde664 --- /dev/null +++ b/internal/api/authz/system_token.go @@ -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 +} diff --git a/internal/api/authz/token.go b/internal/api/authz/token.go deleted file mode 100644 index e5b34af9a2..0000000000 --- a/internal/api/authz/token.go +++ /dev/null @@ -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 - } -} diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index 015c8ce83f..44a6172768 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -77,5 +77,9 @@ func (s *Server) RegisterGateway() server.RegisterGatewayFunc { } func (s *Server) GatewayPathPrefix() string { + return GatewayPathPrefix() +} + +func GatewayPathPrefix() string { return "/auth/v1" } diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index d2a81203ea..21c9d2e726 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -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) diff --git a/internal/api/grpc/server/middleware/auth_interceptor_test.go b/internal/api/grpc/server/middleware/auth_interceptor_test.go index 6e22d1352b..c644ab2e31 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor_test.go +++ b/internal/api/grpc/server/middleware/auth_interceptor_test.go @@ -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 diff --git a/internal/api/grpc/server/middleware/quota_interceptor.go b/internal/api/grpc/server/middleware/quota_interceptor.go index cfcdcedb9f..be6e4d6e4d 100644 --- a/internal/api/grpc/server/middleware/quota_interceptor.go +++ b/internal/api/grpc/server/middleware/quota_interceptor.go @@ -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) } diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 96c8066d1e..2173c36560 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -35,7 +35,7 @@ type WithGatewayPrefix interface { } func CreateServer( - verifier *authz.TokenVerifier, + verifier authz.APITokenVerifier, authConfig authz.Config, queries *query.Queries, hostHeaderName string, diff --git a/internal/api/http/middleware/activity_interceptor.go b/internal/api/http/middleware/activity_interceptor.go index 7cba3db421..7f15a7c319 100644 --- a/internal/api/http/middleware/activity_interceptor.go +++ b/internal/api/http/middleware/activity_interceptor.go @@ -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)) + }) } diff --git a/internal/api/http/middleware/auth_interceptor.go b/internal/api/http/middleware/auth_interceptor.go index 047ee77541..c327d8c846 100644 --- a/internal/api/http/middleware/auth_interceptor.go +++ b/internal/api/http/middleware/auth_interceptor.go @@ -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 { diff --git a/internal/authz/repository/eventsourcing/eventstore/user_membership.go b/internal/authz/repository/eventsourcing/eventstore/user_membership.go index d4a86a680d..06a67ca39f 100644 --- a/internal/authz/repository/eventsourcing/eventstore/user_membership.go +++ b/internal/authz/repository/eventsourcing/eventstore/user_membership.go @@ -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, diff --git a/internal/config/hook/feature.go b/internal/config/hook/enum.go similarity index 54% rename from internal/config/hook/feature.go rename to internal/config/hook/enum.go index 5eccaa5706..0196a9123e 100644 --- a/internal/config/hook/feature.go +++ b/internal/config/hook/enum.go @@ -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)) } } diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index c7a2faab6f..f750c4d27f 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -293,6 +293,10 @@ service AdminService { post: "/domains/_search"; }; + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Instance"; summary: "List Instance Domains"; diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 80fc89076d..01d0aecaf1 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -115,7 +115,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.read"; }; } @@ -126,7 +126,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.read"; }; } @@ -140,7 +140,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.write"; }; } @@ -152,7 +152,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.write"; }; } @@ -165,7 +165,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.write"; }; } @@ -177,12 +177,13 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.instance.delete"; }; } //Returns all instance members matching the request // all queries need to match (ANDed) + // Deprecated: Use the Admin APIs ListIAMMembers instead rpc ListIAMMembers(ListIAMMembersRequest) returns (ListIAMMembersResponse) { option (google.api.http) = { post: "/instances/{instance_id}/members/_search"; @@ -190,11 +191,11 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.iam.member.read"; }; } - // Checks if a domain exists + //Checks if a domain exists rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { option (google.api.http) = { post: "/domains/{domain}/_exists"; @@ -202,11 +203,13 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.read"; }; } // Returns the custom domains of an instance + //Checks if a domain exists + // Deprecated: Use the Admin APIs ListInstanceDomains on the admin API instead rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { option (google.api.http) = { post: "/instances/{instance_id}/domains/_search"; @@ -214,7 +217,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.read"; }; } @@ -226,7 +229,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.write"; }; } @@ -237,7 +240,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.delete"; }; } @@ -249,7 +252,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.domain.write"; }; } @@ -263,7 +266,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.read"; }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -287,7 +290,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.write"; }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -311,7 +314,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.read"; }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -336,9 +339,8 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.debug.delete"; }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "failed events"; responses: { @@ -375,7 +377,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.quota.write"; }; } @@ -392,7 +394,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.quota.write"; }; } @@ -407,7 +409,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.quota.delete"; }; } @@ -419,7 +421,7 @@ service SystemService { }; option (zitadel.v1.auth_option) = { - permission: "authenticated"; + permission: "system.feature.write"; }; }