From e4102ebd207bd337df72b2cd7217cb4bfcc40e00 Mon Sep 17 00:00:00 2001 From: Iraq <66622793+kkrime@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:15:17 +0000 Subject: [PATCH] feat(Authz): system user support for permission check v2 (#9640) # Which Problems Are Solved For permissoin check v2, we currently check the human users permissions from the DB, however for system users (from the config, which would not be in the DB), their permissions are not in the DB, but are in a config file like defaults.yaml. - The current check involves **all** users (human, machine) in the DB and excludes only system users - It's redundant to mention twice that system users are not in the DB. ## How the Problems Are Solved - Can you please expand a bit on your implementation? For example: - what is the precedence of the permissions being applied. - which role-permission configuration is used for the system users (should be runtime, but currently is DB, see my other comment) - Mention details such as "roles are loaded once per request and cached", which you did in a separate comment. The way the permissions work are based on the following: https://zitadel.slack.com/archives/C087ADF8LRX/p1742207808062949?thread_ts=1742206770.965909&cid=C087ADF8LRX # Additional Changes **Important to note** from this point onwards, System Users permissions will be picked up from `SystemAuthz` (added to `default.yaml`) **NOT** `InternalAuthz` - Closes https://github.com/zitadel/zitadel/issues/9189 --- cmd/defaults.yaml | 292 ++++++++++++++++++ cmd/mirror/projections.go | 5 +- cmd/setup/52.go | 37 +++ cmd/setup/52/01-get-permissions-from-JSON.sql | 43 +++ cmd/setup/52/02-permitted_orgs_function.sql | 144 +++++++++ cmd/setup/config.go | 14 +- cmd/setup/setup.go | 6 +- cmd/start/config.go | 13 +- cmd/start/start.go | 14 +- .../zitadel-apis/access-zitadel-system-api.md | 2 + internal/api/api.go | 11 +- internal/api/assets/asset.go | 10 +- internal/api/authz/authorization.go | 51 ++- internal/api/authz/context.go | 4 +- internal/api/authz/permissions.go | 8 +- internal/api/authz/permissions_test.go | 2 +- .../server/middleware/auth_interceptor.go | 8 +- .../middleware/auth_interceptor_test.go | 5 +- internal/api/grpc/server/server.go | 3 +- .../user/v2/integration_test/query_test.go | 95 ++++++ .../user/v2/integration_test/user_test.go | 16 +- .../api/http/middleware/auth_interceptor.go | 20 +- internal/id/sonyflake.go | 4 +- internal/integration/config.go | 9 +- .../system-user-with-no-permissions.pem | 28 ++ internal/integration/config/zitadel.yaml | 7 + internal/integration/system.go | 27 +- internal/query/permission.go | 55 +++- internal/query/user.go | 15 +- 29 files changed, 854 insertions(+), 94 deletions(-) create mode 100644 cmd/setup/52.go create mode 100644 cmd/setup/52/01-get-permissions-from-JSON.sql create mode 100644 cmd/setup/52/02-permitted_orgs_function.sql create mode 100644 internal/integration/config/system-user-with-no-permissions.pem diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index f85033069a..30e037c80f 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -1713,6 +1713,298 @@ InternalAuthZ: - "user.grant.read" - "user.membership.read" +SystemAuthZ: + 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.read" + - "system.feature.write" + - "system.feature.delete" + - "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.feature.read" + - "system.iam.member.read" + - Role: "IAM_OWNER" + Permissions: + - "iam.read" + - "iam.write" + - "iam.policy.read" + - "iam.policy.write" + - "iam.policy.delete" + - "iam.member.read" + - "iam.member.write" + - "iam.member.delete" + - "iam.idp.read" + - "iam.idp.write" + - "iam.idp.delete" + - "iam.action.read" + - "iam.action.write" + - "iam.action.delete" + - "iam.flow.read" + - "iam.flow.write" + - "iam.flow.delete" + - "iam.feature.read" + - "iam.feature.write" + - "iam.feature.delete" + - "iam.restrictions.read" + - "iam.restrictions.write" + - "iam.web_key.write" + - "iam.web_key.delete" + - "iam.web_key.read" + - "iam.debug.write" + - "iam.debug.read" + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "events.read" + - "milestones.read" + - "session.read" + - "session.delete" + - "action.target.read" + - "action.target.write" + - "action.target.delete" + - "action.execution.read" + - "action.execution.write" + - "userschema.read" + - "userschema.write" + - "userschema.delete" + - "session.read" + - "session.delete" + - Role: "IAM_OWNER_VIEWER" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.idp.read" + - "iam.action.read" + - "iam.flow.read" + - "iam.restrictions.read" + - "iam.feature.read" + - "iam.web_key.read" + - "iam.debug.read" + - "org.read" + - "org.member.read" + - "org.idp.read" + - "org.action.read" + - "org.flow.read" + - "org.feature.read" + - "user.read" + - "user.global.read" + - "user.grant.read" + - "user.membership.read" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.member.read" + - "events.read" + - "milestones.read" + - "action.target.read" + - "action.execution.read" + - "userschema.read" + - "session.read" + - Role: "IAM_ORG_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.create" + - "org.write" + - "org.delete" + - "org.member.read" + - "org.member.write" + - "org.member.delete" + - "org.idp.read" + - "org.idp.write" + - "org.idp.delete" + - "org.action.read" + - "org.action.write" + - "org.action.delete" + - "org.flow.read" + - "org.flow.write" + - "org.flow.delete" + - "org.feature.read" + - "org.feature.write" + - "org.feature.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "policy.read" + - "policy.write" + - "policy.delete" + - "project.read" + - "project.create" + - "project.write" + - "project.delete" + - "project.member.read" + - "project.member.write" + - "project.member.delete" + - "project.role.read" + - "project.role.write" + - "project.role.delete" + - "project.app.read" + - "project.app.write" + - "project.app.delete" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "project.grant.member.write" + - "project.grant.member.delete" + - "session.delete" + - Role: "IAM_USER_MANAGER" + Permissions: + - "org.read" + - "org.global.read" + - "org.member.read" + - "org.member.delete" + - "user.read" + - "user.global.read" + - "user.write" + - "user.delete" + - "user.grant.read" + - "user.grant.write" + - "user.grant.delete" + - "user.membership.read" + - "user.passkey.write" + - "user.feature.read" + - "user.feature.write" + - "user.feature.delete" + - "project.read" + - "project.member.read" + - "project.role.read" + - "project.app.read" + - "project.grant.read" + - "project.grant.write" + - "project.grant.delete" + - "project.grant.member.read" + - "session.delete" + - Role: "IAM_ADMIN_IMPERSONATOR" + Permissions: + - "admin.impersonation" + - "impersonation" + - Role: "IAM_END_USER_IMPERSONATOR" + Permissions: + - "impersonation" + - Role: "IAM_LOGIN_CLIENT" + Permissions: + - "iam.read" + - "iam.policy.read" + - "iam.member.read" + - "iam.member.write" + - "iam.idp.read" + - "iam.feature.read" + - "iam.restrictions.read" + - "org.read" + - "org.member.read" + - "org.member.write" + - "org.idp.read" + - "org.feature.read" + - "user.read" + - "user.write" + - "user.grant.read" + - "user.grant.write" + - "user.membership.read" + - "user.credential.write" + - "user.passkey.write" + - "user.feature.read" + - "policy.read" + - "project.read" + - "project.member.read" + - "project.member.write" + - "project.role.read" + - "project.app.read" + - "project.member.read" + - "project.member.write" + - "project.grant.read" + - "project.grant.member.read" + - "project.grant.member.write" + - "session.read" + - "session.link" + - "session.delete" + - "userschema.read" + # If a new projection is introduced it will be prefilled during the setup process (if enabled) # This can prevent serving outdated data after a version upgrade, but might require a longer setup / upgrade process: # https://zitadel.com/docs/self-hosting/manage/updating_scaling diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 14b93b52c8..66b3fb1a26 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -84,6 +84,7 @@ type ProjectionsConfig struct { ExternalDomain string ExternalSecure bool InternalAuthZ internal_authz.Config + SystemAuthZ internal_authz.Config SystemDefaults systemdefaults.SystemDefaults Telemetry *handlers.TelemetryPusherConfig Login login.Config @@ -150,7 +151,7 @@ func projections( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, @@ -187,7 +188,7 @@ func projections( keys.Target, &http.Client{}, func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) }, sessionTokenVerifier, config.OIDC.DefaultAccessTokenLifetime, diff --git a/cmd/setup/52.go b/cmd/setup/52.go new file mode 100644 index 0000000000..581a7acbab --- /dev/null +++ b/cmd/setup/52.go @@ -0,0 +1,37 @@ +package setup + +import ( + "context" + "embed" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +type InitPermittedOrgsFunction52 struct { + dbClient *database.DB +} + +//go:embed 52/*.sql +var permittedOrgsFunction52 embed.FS + +func (mig *InitPermittedOrgsFunction52) Execute(ctx context.Context, _ eventstore.Event) error { + statements, err := readStatements(permittedOrgsFunction52, "52") + if err != nil { + return err + } + for _, stmt := range statements { + logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement") + if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil { + return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err) + } + } + return nil +} + +func (*InitPermittedOrgsFunction52) String() string { + return "52_init_permitted_orgs_function" +} diff --git a/cmd/setup/52/01-get-permissions-from-JSON.sql b/cmd/setup/52/01-get-permissions-from-JSON.sql new file mode 100644 index 0000000000..b6415fa180 --- /dev/null +++ b/cmd/setup/52/01-get-permissions-from-JSON.sql @@ -0,0 +1,43 @@ +DROP FUNCTION IF EXISTS eventstore.get_system_permissions; + +CREATE OR REPLACE FUNCTION eventstore.get_system_permissions( + permissions_json JSONB + /* + [ + { + "member_type": "System", + "aggregate_id": "", + "object_id": "", + "permissions": ["iam.read", "iam.write", "iam.polic.read"] + }, + { + "member_type": "IAM", + "aggregate_id": "310716990375453665", + "object_id": "", + "permissions": ["iam.read", "iam.write", "iam.polic.read"] + } + ] + */ + , permm TEXT +) +RETURNS TABLE ( + member_type TEXT, + aggregate_id TEXT, + object_id TEXT +) + LANGUAGE 'plpgsql' +AS $$ +BEGIN + RETURN QUERY + SELECT res.member_type, res.aggregate_id, res.object_id FROM ( + SELECT + (perm)->>'member_type' AS member_type, + (perm)->>'aggregate_id' AS aggregate_id, + (perm)->>'object_id' AS object_id, + permission + FROM jsonb_array_elements(permissions_json) AS perm + CROSS JOIN jsonb_array_elements_text(perm->'permissions') AS permission) AS res + WHERE res. permission= permm; +END; +$$; + diff --git a/cmd/setup/52/02-permitted_orgs_function.sql b/cmd/setup/52/02-permitted_orgs_function.sql new file mode 100644 index 0000000000..b6f61c6225 --- /dev/null +++ b/cmd/setup/52/02-permitted_orgs_function.sql @@ -0,0 +1,144 @@ +DROP FUNCTION IF EXISTS eventstore.check_system_user_perms; + +CREATE OR REPLACE FUNCTION eventstore.check_system_user_perms( + system_user_perms JSONB + , perm TEXT + , filter_orgs TEXT + + , org_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' +AS $$ +BEGIN + + WITH found_permissions(member_type, aggregate_id, object_id ) AS ( + SELECT * FROM eventstore.get_system_permissions( + system_user_perms, + perm) + ) + + SELECT array_agg(DISTINCT o.org_id) INTO org_ids + FROM eventstore.instance_orgs o, found_permissions + WHERE + CASE WHEN (SELECT TRUE WHERE found_permissions.member_type = 'System' LIMIT 1) THEN + TRUE + WHEN (SELECT TRUE WHERE found_permissions.member_type = 'IAM' LIMIT 1) THEN + -- aggregate_id not present + CASE WHEN (SELECT TRUE WHERE '' = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'IAM' + GROUP BY member_type + LIMIT 1 + )::TEXT[])) THEN + TRUE + -- aggregate_id is present + ELSE + o.instance_id = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'IAM' + GROUP BY member_type + LIMIT 1 + )::TEXT[]) + END + WHEN (SELECT TRUE WHERE found_permissions.member_type = 'Organization' LIMIT 1) THEN + -- aggregate_id not present + CASE WHEN (SELECT TRUE WHERE '' = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'Organization' + GROUP BY member_type + LIMIT 1 + )::TEXT[])) THEN + TRUE + -- aggregate_id is present + ELSE + o.org_id = ANY ( + ( + SELECT array_agg(found_permissions.aggregate_id) + FROM found_permissions + WHERE member_type = 'Organization' + GROUP BY member_type + LIMIT 1 + )::TEXT[]) + END + END + AND + CASE WHEN filter_orgs != '' + THEN o.org_id IN (filter_orgs) + ELSE TRUE END + LIMIT 1; +END; +$$; + + +DROP FUNCTION IF EXISTS eventstore.permitted_orgs; + +CREATE OR REPLACE FUNCTION eventstore.permitted_orgs( + instanceId TEXT + , userId TEXT + , system_user_perms JSONB + , perm TEXT + , filter_orgs TEXT + + , org_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' +AS $$ +BEGIN + + -- if system user + IF system_user_perms IS NOT NULL THEN + org_ids := eventstore.check_system_user_perms(system_user_perms, perm, filter_orgs); + -- if human/machine user + ELSE + DECLARE + matched_roles TEXT[]; -- roles containing permission + BEGIN + + SELECT array_agg(rp.role) INTO matched_roles + FROM eventstore.role_permissions rp + WHERE rp.instance_id = instanceId + AND rp.permission = perm; + + -- First try if the permission was granted thru an instance-level role + DECLARE + has_instance_permission bool; + BEGIN + SELECT true INTO has_instance_permission + FROM eventstore.instance_members im + WHERE im.role = ANY(matched_roles) + AND im.instance_id = instanceId + AND im.user_id = userId + LIMIT 1; + + IF has_instance_permission THEN + -- Return all organizations or only those in filter_orgs + SELECT array_agg(o.org_id) INTO org_ids + FROM eventstore.instance_orgs o + WHERE o.instance_id = instanceId + AND CASE WHEN filter_orgs != '' + THEN o.org_id IN (filter_orgs) + ELSE TRUE END; + RETURN; + END IF; + END; + + -- Return the organizations where permission were granted thru org-level roles + SELECT array_agg(sub.org_id) INTO org_ids + FROM ( + SELECT DISTINCT om.org_id + FROM eventstore.org_members om + WHERE om.role = ANY(matched_roles) + AND om.instance_id = instanceID + AND om.user_id = userId + ) AS sub; + END; + END IF; +END; +$$; + diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 1c5e03cca3..9754494f9c 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -12,7 +12,7 @@ import ( "github.com/zitadel/zitadel/cmd/encryption" "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/cache/connector" @@ -34,7 +34,8 @@ type Config struct { Database database.Config Caches *connector.CachesConfig SystemDefaults systemdefaults.SystemDefaults - InternalAuthZ internal_authz.Config + InternalAuthZ authz.Config + SystemAuthZ authz.Config ExternalDomain string ExternalPort uint16 ExternalSecure bool @@ -53,7 +54,7 @@ type Config struct { Login login.Config WebAuthNName string Telemetry *handlers.TelemetryPusherConfig - SystemAPIUsers map[string]*internal_authz.SystemAPIUser + SystemAPIUsers map[string]*authz.SystemAPIUser } type InitProjections struct { @@ -68,12 +69,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hooks.SliceTypeStringDecode[*domain.CustomMessageText], - hooks.SliceTypeStringDecode[internal_authz.RoleMapping], - hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.SliceTypeStringDecode[authz.RoleMapping], + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], hooks.MapHTTPHeaderStringDecode, database.DecodeHook(false), actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), + hook.EnumHookFunc(authz.MemberTypeString), hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), @@ -142,6 +143,7 @@ type Steps struct { s49InitPermittedOrgsFunction *InitPermittedOrgsFunction s50IDPTemplate6UsePKCE *IDPTemplate6UsePKCE s51IDPTemplate6RootCA *IDPTemplate6RootCA + s52InitPermittedOrgsFunction *InitPermittedOrgsFunction52 } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 6d5a9357cf..1a6cafbc64 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -178,6 +178,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s49InitPermittedOrgsFunction = &InitPermittedOrgsFunction{eventstoreClient: dbClient} steps.s50IDPTemplate6UsePKCE = &IDPTemplate6UsePKCE{dbClient: dbClient} steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient} + steps.s52InitPermittedOrgsFunction = &InitPermittedOrgsFunction52{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -218,6 +219,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s49InitPermittedOrgsFunction, steps.s50IDPTemplate6UsePKCE, steps.s51IDPTemplate6RootCA, + steps.s52InitPermittedOrgsFunction, } { mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") } @@ -409,7 +411,7 @@ func startCommandsQueries( sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, 0, // not needed for projections @@ -434,7 +436,7 @@ func startCommandsQueries( authZRepo, err := authz.Start(queries, eventstoreClient, dbClient, keys.OIDC, config.ExternalSecure) logging.OnError(err).Fatal("unable to start authz repo") permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } commands, err := command.StartCommands(ctx, diff --git a/cmd/start/config.go b/cmd/start/config.go index 589086b801..e973c40479 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/internal/actions" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/saml" @@ -67,12 +67,13 @@ type Config struct { Login login.Config Console console.Config AssetStorage static_config.AssetStorageConfig - InternalAuthZ internal_authz.Config + InternalAuthZ authz.Config + SystemAuthZ authz.Config SystemDefaults systemdefaults.SystemDefaults EncryptionKeys *encryption.EncryptionKeyConfig DefaultInstance command.InstanceSetup AuditLogRetention time.Duration - SystemAPIUsers map[string]*internal_authz.SystemAPIUser + SystemAPIUsers map[string]*authz.SystemAPIUser CustomerPortal string Machine *id.Config Actions *actions.Config @@ -96,12 +97,12 @@ func MustNewConfig(v *viper.Viper) *Config { err := v.Unmarshal(config, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( hooks.SliceTypeStringDecode[*domain.CustomMessageText], - hooks.SliceTypeStringDecode[internal_authz.RoleMapping], - hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], + hooks.SliceTypeStringDecode[authz.RoleMapping], + hooks.MapTypeStringDecode[string, *authz.SystemAPIUser], hooks.MapHTTPHeaderStringDecode, database.DecodeHook(false), actions.HTTPConfigDecodeHook, - hook.EnumHookFunc(internal_authz.MemberTypeString), + hook.EnumHookFunc(authz.MemberTypeString), hooks.MapTypeStringDecode[domain.Feature, any], hooks.SliceTypeStringDecode[*command.SetQuota], hook.Base64ToBytesHookFunc(), diff --git a/cmd/start/start.go b/cmd/start/start.go index 84dda13c54..e3d84625b4 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -56,7 +56,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/system" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" - "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta" + webkey "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/idp" @@ -193,7 +193,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server sessionTokenVerifier, func(q *query.Queries) domain.PermissionCheck { return func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, &authz_es.UserMembershipRepo{Queries: q}, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } }, config.AuditLogRetention, @@ -209,7 +209,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server return fmt.Errorf("error starting authz repo: %w", err) } permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { - return internal_authz.CheckPermission(ctx, authZRepo, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) + return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) } storage, err := config.AssetStorage.NewStorage(dbClient.DB) @@ -418,7 +418,7 @@ func startAPIs( http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), ) limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) - apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) + apis, err := api.New(ctx, config.Port, router, queries, verifier, config.SystemAuthZ, config.InternalAuthZ, tlsConfig, config.ExternalDomain, append(config.InstanceHostHeaders, config.PublicHostHeaders...), limitingAccessInterceptor) if err != nil { return nil, fmt.Errorf("error creating api %w", err) } @@ -501,7 +501,7 @@ func startAPIs( } instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) - apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) + apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler)) @@ -545,7 +545,7 @@ func startAPIs( keys.User, &config.SCIM, instanceInterceptor.HandlerFuncWithError, - middleware.AuthorizationInterceptor(verifier, config.InternalAuthZ).HandlerFuncWithError)) + middleware.AuthorizationInterceptor(verifier, config.SystemAuthZ, config.InternalAuthZ).HandlerFuncWithError)) c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal) if err != nil { @@ -611,7 +611,7 @@ func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls go func() { logging.Infof("server is listening on %s", lis.Addr().String()) if tlsConfig != nil { - //we don't need to pass the files here, because we already initialized the TLS config on the server + // we don't need to pass the files here, because we already initialized the TLS config on the server errCh <- http1Server.ServeTLS(lis, "", "") } else { errCh <- http1Server.Serve(lis) diff --git a/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md b/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md index 4e2b0b9973..4b96e9bbd5 100644 --- a/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md +++ b/docs/docs/guides/integrate/zitadel-apis/access-zitadel-system-api.md @@ -16,6 +16,8 @@ To authenticate the user a self-signed JWT will be created and utilized. You can define any id for your user. This guide will assume it's `system-user-1`. +**NOTE:** system user id cannot contain capital letters + ## Generate an RSA keypair Generate an RSA private key with 2048 bit modulus: diff --git a/internal/api/api.go b/internal/api/api.go index 15d6c5b996..62d3e14b35 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -15,7 +15,7 @@ import ( healthpb "google.golang.org/grpc/health/grpc_health_v1" "google.golang.org/grpc/reflection" - internal_authz "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" http_util "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" @@ -29,7 +29,7 @@ import ( type API struct { port uint16 grpcServer *grpc.Server - verifier internal_authz.APITokenVerifier + verifier authz.APITokenVerifier health healthCheck router *mux.Router hostHeaders []string @@ -72,8 +72,9 @@ func New( port uint16, router *mux.Router, queries *query.Queries, - verifier internal_authz.APITokenVerifier, - authZ internal_authz.Config, + verifier authz.APITokenVerifier, + systemAuthz authz.Config, + authZ authz.Config, tlsConfig *tls.Config, externalDomain string, hostHeaders []string, @@ -89,7 +90,7 @@ func New( hostHeaders: hostHeaders, } - api.grpcServer = server.CreateServer(api.verifier, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) + api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig) if err != nil { return nil, err diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 57ad3710bc..8c3c51c6aa 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -94,13 +94,13 @@ func DefaultErrorHandler(translator *i18n.Translator) func(w http.ResponseWriter } } -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 { +func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, systemAuthCOnfig authz.Config, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler { translator, err := i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") h := &Handler{ commands: commands, errorHandler: DefaultErrorHandler(translator), - authInterceptor: http_mw.AuthorizationInterceptor(verifier, authConfig), + authInterceptor: http_mw.AuthorizationInterceptor(verifier, systemAuthCOnfig, authConfig), idGenerator: idGenerator, storage: storage, query: queries, @@ -129,8 +129,10 @@ func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string return ownerPath } -const maxMemory = 2 << 20 -const paramFile = "file" +const ( + maxMemory = 2 << 20 + paramFile = "file" +) func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index 2099b3e426..ea20a2438f 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -4,8 +4,11 @@ import ( "context" "fmt" "reflect" + "slices" "strings" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -16,10 +19,10 @@ const ( // CheckUserAuthorization verifies that: // - the token is active, -// - the organisation (**either** provided by ID or verified domain) exists +// - the organization (**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 APITokenVerifier, 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, systemRolePermissionMapping []RoleMapping, rolePermissionMapping []RoleMapping, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { ctx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() @@ -30,11 +33,12 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, if requiredAuthOption.Permission == authenticated { return func(parent context.Context) context.Context { + parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData) return context.WithValue(parent, dataKey, ctxData) }, nil } - requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID) + requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, systemRolePermissionMapping, rolePermissionMapping, ctxData, ctxData.OrgID) if err != nil { return nil, err } @@ -50,6 +54,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, parent = context.WithValue(parent, dataKey, ctxData) parent = context.WithValue(parent, allPermissionsKey, allPermissions) parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions) + parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData) return parent }, nil } @@ -125,3 +130,43 @@ func GetAllPermissionCtxIDs(perms []string) []string { } return ctxIDs } + +type SystemUserPermissionsDBQuery struct { + MemberType string `json:"member_type"` + AggregateID string `json:"aggregate_id"` + ObjectID string `json:"object_id"` + Permissions []string `json:"permissions"` +} + +func addGetSystemUserRolesToCtx(ctx context.Context, systemUserRoleMap []RoleMapping, ctxData CtxData) context.Context { + if len(ctxData.SystemMemberships) == 0 { + return ctx + } + systemUserPermissions := make([]SystemUserPermissionsDBQuery, len(ctxData.SystemMemberships)) + for i, systemPerm := range ctxData.SystemMemberships { + permissions := make([]string, 0, len(systemPerm.Roles)) + for _, role := range systemPerm.Roles { + permissions = append(permissions, getPermissionsFromRole(systemUserRoleMap, role)...) + } + slices.Sort(permissions) + permissions = slices.Compact(permissions) + + systemUserPermissions[i].MemberType = systemPerm.MemberType.String() + systemUserPermissions[i].AggregateID = systemPerm.AggregateID + systemUserPermissions[i].Permissions = permissions + } + return context.WithValue(ctx, systemUserRolesKey, systemUserPermissions) +} + +func GetSystemUserPermissions(ctx context.Context) []SystemUserPermissionsDBQuery { + getSystemUserRolesFuncValue := ctx.Value(systemUserRolesKey) + if getSystemUserRolesFuncValue == nil { + return nil + } + systemUserRoles, ok := getSystemUserRolesFuncValue.([]SystemUserPermissionsDBQuery) + if !ok { + logging.WithFields("Authz").Error("unable to cast []SystemUserPermissionsDBQuery") + return nil + } + return systemUserRoles +} diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index ff401f8862..d6528cd017 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -22,6 +22,7 @@ const ( dataKey key = 2 allPermissionsKey key = 3 instanceKey key = 4 + systemUserRolesKey key = 5 ) type CtxData struct { @@ -50,7 +51,8 @@ type Memberships []*Membership type Membership struct { MemberType MemberType AggregateID string - //ObjectID differs from aggregate id if object is sub of an aggregate + InstanceID string + // ObjectID differs from aggregate id if object is sub of an aggregate ObjectID string Roles []string diff --git a/internal/api/authz/permissions.go b/internal/api/authz/permissions.go index e96a7b256b..904fbbc33a 100644 --- a/internal/api/authz/permissions.go +++ b/internal/api/authz/permissions.go @@ -7,8 +7,8 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { - requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID) +func CheckPermission(ctx context.Context, resolver MembershipsResolver, systemUserRoleMapping []RoleMapping, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { + requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, systemUserRoleMapping, roleMappings, GetCtxData(ctx), orgID) if err != nil { return err } @@ -22,7 +22,7 @@ func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMapp // getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level), // and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately. -func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { +func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, systemUserRoleMappings []RoleMapping, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -31,7 +31,7 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi } if ctxData.SystemMemberships != nil { - requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings) + requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, systemUserRoleMappings) return requestedPermissions, allPermissions, nil } diff --git a/internal/api/authz/permissions_test.go b/internal/api/authz/permissions_test.go index 7919747de6..93243d0c09 100644 --- a/internal/api/authz/permissions_test.go +++ b/internal/api/authz/permissions_test.go @@ -120,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.membershipsResolver, 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, nil, 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) diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index 6eb326a59a..410b4b8abc 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.APITokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemUserPermissions authz.Config, 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) + return authorize(ctx, req, info, handler, verifier, systemUserPermissions, authConfig) } } -func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, authConfig authz.Config) (_ interface{}, err error) { +func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) (_ interface{}, err error) { authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod) if !needsToken { return handler(ctx, req) @@ -34,7 +34,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, } orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req) - ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, authConfig, authOpt, info.FullMethod) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, systemUserPermissions.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, info.FullMethod) if err != nil { return nil, err } diff --git a/internal/api/grpc/server/middleware/auth_interceptor_test.go b/internal/api/grpc/server/middleware/auth_interceptor_test.go index 3551d3e419..e098189445 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor_test.go +++ b/internal/api/grpc/server/middleware/auth_interceptor_test.go @@ -20,6 +20,7 @@ type authzRepoMock struct{} func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "", "", "", "", "", nil } + func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) { return authz.Memberships{{ MemberType: authz.MemberTypeOrganization, @@ -31,9 +32,11 @@ func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) { return "", nil, nil } + func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { return orgID, nil } + func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { return "", "", nil } @@ -252,7 +255,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, 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/server.go b/internal/api/grpc/server/server.go index 27b921b7d5..b686d3add9 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -36,6 +36,7 @@ type WithGatewayPrefix interface { func CreateServer( verifier authz.APITokenVerifier, + systemAuthz authz.Config, authConfig authz.Config, queries *query.Queries, externalDomain string, @@ -53,7 +54,7 @@ func CreateServer( middleware.AccessStorageInterceptor(accessSvc), middleware.ErrorHandler(), middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), - middleware.AuthorizationInterceptor(verifier, authConfig), + middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig), middleware.TranslationHandler(), middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName), middleware.ExecutionHandler(queries), diff --git a/internal/api/grpc/user/v2/integration_test/query_test.go b/internal/api/grpc/user/v2/integration_test/query_test.go index 554de2b69a..15dc959151 100644 --- a/internal/api/grpc/user/v2/integration_test/query_test.go +++ b/internal/api/grpc/user/v2/integration_test/query_test.go @@ -415,6 +415,10 @@ func createUsers(ctx context.Context, orgID string, count int, passwordChangeReq func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr { username := gofakeit.Email() + return createUserWithUserName(ctx, username, orgID, passwordChangeRequired) +} + +func createUserWithUserName(ctx context.Context, username string, orgID string, passwordChangeRequired bool) userAttr { // used as default country prefix phone := "+41" + gofakeit.Phone() resp := Instance.CreateHumanUserVerified(ctx, orgID, username, phone) @@ -1179,6 +1183,97 @@ func TestServer_ListUsers(t *testing.T) { } } +func TestServer_SystemUsers_ListUsers(t *testing.T) { + defer func() { + _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(IamCTX, &feature.ResetInstanceFeaturesRequest{}) + require.NoError(t, err) + }() + + org1 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) + org2 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), "org2@zitadel.com") + org3 := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email()) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser1@zitadel.com", org1.OrganizationId, false) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser2@zitadel.com", org2.OrganizationId, false) + _ = createUserWithUserName(IamCTX, "Test_SystemUsers_ListUser3@zitadel.com", org3.OrganizationId, false) + + tests := []struct { + name string + ctx context.Context + req *user.ListUsersRequest + expectedFoundUsernames []string + checkNumberOfUsersReturned bool + }{ + { + name: "list users with neccessary permissions", + ctx: SystemCTX, + req: &user.ListUsersRequest{}, + // the number of users returned will vary from test run to test run, + // so just check the system user gets back users from different orgs whcih it is not a memeber of + checkNumberOfUsersReturned: false, + expectedFoundUsernames: []string{"Test_SystemUsers_ListUser1@zitadel.com", "Test_SystemUsers_ListUser2@zitadel.com", "Test_SystemUsers_ListUser3@zitadel.com"}, + }, + { + name: "list users without neccessary permissions", + ctx: SystemUserWithNoPermissionsCTX, + req: &user.ListUsersRequest{}, + // check no users returned + checkNumberOfUsersReturned: true, + }, + { + name: "list users with neccessary permissions specifying org", + req: &user.ListUsersRequest{ + Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, + }, + ctx: SystemCTX, + expectedFoundUsernames: []string{"Test_SystemUsers_ListUser2@zitadel.com", "org2@zitadel.com"}, + checkNumberOfUsersReturned: true, + }, + { + name: "list users without neccessary permissions specifying org", + req: &user.ListUsersRequest{ + Queries: []*user.SearchQuery{OrganizationIdQuery(org2.OrganizationId)}, + }, + ctx: SystemUserWithNoPermissionsCTX, + // check no users returned + checkNumberOfUsersReturned: true, + }, + } + + for _, f := range permissionCheckV2Settings { + f := f + for _, tt := range tests { + t.Run(f.TestNamePrependString+tt.name, func(t *testing.T) { + setPermissionCheckV2Flag(t, f.SetFlag) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, 1*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListUsers(tt.ctx, tt.req) + require.NoError(ttt, err) + + if tt.checkNumberOfUsersReturned { + require.Equal(t, len(tt.expectedFoundUsernames), len(got.Result)) + } + + if tt.expectedFoundUsernames != nil { + for _, user := range got.Result { + for i, username := range tt.expectedFoundUsernames { + if username == user.Username { + tt.expectedFoundUsernames = tt.expectedFoundUsernames[i+1:] + break + } + } + if len(tt.expectedFoundUsernames) == 0 { + return + } + } + require.FailNow(t, "unable to find all users with specified usernames") + } + }, retryDuration, tick, "timeout waiting for expected user result") + }) + } + } +} + func InUserIDsQuery(ids []string) *user.SearchQuery { return &user.SearchQuery{ Query: &user.SearchQuery_InUserIdsQuery{ diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index f39212f7e3..bf396fd25d 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -31,12 +31,13 @@ import ( ) var ( - CTX context.Context - IamCTX context.Context - UserCTX context.Context - SystemCTX context.Context - Instance *integration.Instance - Client user.UserServiceClient + CTX context.Context + IamCTX context.Context + UserCTX context.Context + SystemCTX context.Context + SystemUserWithNoPermissionsCTX context.Context + Instance *integration.Instance + Client user.UserServiceClient ) func TestMain(m *testing.M) { @@ -46,6 +47,7 @@ func TestMain(m *testing.M) { Instance = integration.NewInstance(ctx) + SystemUserWithNoPermissionsCTX = integration.WithSystemUserWithNoPermissionsAuthorization(ctx) UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) SystemCTX = integration.WithSystemAuthorization(ctx) @@ -1306,7 +1308,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -3048,7 +3049,6 @@ func TestServer_ListAuthenticationFactors(t *testing.T) { assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult()) }, retryDuration, tick, "timeout waiting for expected auth methods result") - }) } } diff --git a/internal/api/http/middleware/auth_interceptor.go b/internal/api/http/middleware/auth_interceptor.go index 1581d401b4..ae9377b13d 100644 --- a/internal/api/http/middleware/auth_interceptor.go +++ b/internal/api/http/middleware/auth_interceptor.go @@ -14,14 +14,16 @@ import ( ) type AuthInterceptor struct { - verifier authz.APITokenVerifier - authConfig authz.Config + verifier authz.APITokenVerifier + authConfig authz.Config + systemAuthConfig authz.Config } -func AuthorizationInterceptor(verifier authz.APITokenVerifier, authConfig authz.Config) *AuthInterceptor { +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) *AuthInterceptor { return &AuthInterceptor{ - verifier: verifier, - authConfig: authConfig, + verifier: verifier, + authConfig: authConfig, + systemAuthConfig: systemAuthConfig, } } @@ -31,7 +33,7 @@ func (a *AuthInterceptor) Handler(next http.Handler) http.Handler { func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, err := authorize(r, a.verifier, a.authConfig) + ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return @@ -44,7 +46,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc { func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) HandlerFuncWithError { return func(w http.ResponseWriter, r *http.Request) error { - ctx, err := authorize(r, a.verifier, a.authConfig) + ctx, err := authorize(r, a.verifier, a.systemAuthConfig, a.authConfig) if err != nil { return err } @@ -56,7 +58,7 @@ func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) Handle type httpReq struct{} -func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig authz.Config) (_ context.Context, err error) { +func authorize(r *http.Request, verifier authz.APITokenVerifier, systemAuthConfig authz.Config, authConfig authz.Config) (_ context.Context, err error) { ctx := r.Context() authOpt, needsToken := checkAuthMethod(r, verifier) @@ -71,7 +73,7 @@ func authorize(r *http.Request, verifier authz.APITokenVerifier, authConfig auth return nil, zerrors.ThrowUnauthenticated(nil, "AUT-1179", "auth header missing") } - ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, authConfig, authOpt, r.RequestURI) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, &httpReq{}, authToken, http_util.GetOrgID(r), "", verifier, systemAuthConfig.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, r.RequestURI) if err != nil { return nil, err } diff --git a/internal/id/sonyflake.go b/internal/id/sonyflake.go index 22a3247874..cc7086aa66 100644 --- a/internal/id/sonyflake.go +++ b/internal/id/sonyflake.go @@ -73,7 +73,7 @@ func privateIPv4() (net.IP, error) { } } - //change: use "POD_IP" + // change: use "POD_IP" ip := net.ParseIP(os.Getenv("POD_IP")) if ip == nil { return nil, errors.New("no private ip address") @@ -140,7 +140,7 @@ func machineID() (uint16, error) { } logging.WithFields("errors", strings.Join(errors, ", ")).Panic("none of the enabled methods for identifying the machine succeeded") - //this return will never happen because of panic one line before + // this return will never happen because of panic one line before return 0, nil } diff --git a/internal/integration/config.go b/internal/integration/config.go index 5aea740752..0033e00104 100644 --- a/internal/integration/config.go +++ b/internal/integration/config.go @@ -20,10 +20,8 @@ type Config struct { WebAuthNName string } -var ( - //go:embed config/client.yaml - clientYAML []byte -) +//go:embed config/client.yaml +var clientYAML []byte var ( tmpDir string @@ -49,5 +47,6 @@ func init() { if err := loadedConfig.Log.SetLogger(); err != nil { panic(err) } - SystemToken = systemUserToken() + SystemToken = createSystemUserToken() + SystemUserWithNoPermissionsToken = createSystemUserWithNoPermissionsToken() } diff --git a/internal/integration/config/system-user-with-no-permissions.pem b/internal/integration/config/system-user-with-no-permissions.pem new file mode 100644 index 0000000000..801944ca75 --- /dev/null +++ b/internal/integration/config/system-user-with-no-permissions.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMxYRfqb4fdnBl +ZmYweqUaZnWQv8RhWDYGifYGen00ozCFT2L6gGov4YCxRVe+l3aFQ79j5SJb1C+v +H68DJkyCTrhDpATqdjVuCu7CEEI//16Ivfmj3gbNdsp0IcDKVIAF0bN9kve5ofRX +CgU6DIx8GjLsXSooSniZnJ4d/Rnt69mpSsPkykUs3RpG2NSOn3WLAoVKh1q/kqeV +qf8eQ+KzuyD/R9QNAPiyB+ivAuOtVuvmIqojQYK5o8veTg/waBxdmzkim7eg8J7B +VDSjBeHagS5K9IJr/Q2VeO0rZOOeJfLlH9xlSrDvc3AIS/3HtkqI268kNkvpGz0I +sg61pUQtAgMBAAECggEAFzZrv1WPaQNAAex6fdR/fKS4Dqwcjxu7XuUpeUSB+GfP +dLAUR2/c8rPJ45FmaGJz9AIpoWiTe5Z33XYJRyjt1U/zQQ4fFGV1JoXtfHkvX3u1 +5DEFZQDT2NYViMRXFNYNvUfow9Rz/nuG/cJEfd+7W6x7SLANJ1MuY1Ao35OQjsOG +ftTtmEUppEIXyWL0PCeHQc83z8aJrP+p4hpjJOW2mui0NR2Hk456DGYXg8I8fcQD +ar7Ar7/A6thR0OmwG7tkkLjRiCjGwnkr19hCNLz+QAWB2o284T12zZueOqRuYQzu +KwNBZKJlClsPkhdZSPLL4RMFP6hJjKoP5mY0Zdzh8QKBgQDEPrM70aZQiweXHqoE +/vZry7tphGycoEAf6nwBBrZaRPpJdnEA61LBlJFv7C3s59uy6L7nHssTyVUJha9i +zFCWRQ0mHNrwxF5Ybd5p//hgblt3X53IV6vZBFF1+OrwRS/AKki3GynDc/oI++hu +PGHWmUF6lIi3uzWwOTqk6EGovQKBgQC3oqpUlpJ78e0zPjIr9ov61TtnPzAa883D +LL7fuNYP9zxIMoFZw++2bZfT5tbINflQdZnVVDNs5KiwtEu3oZJrsqXpQmzCl3j2 +KA9FTdVJQXc2lU90uYb76c5JZPownojbXFFOPQokBqfsYLSdfvNVHSQGjZ3C90wL +YZC0vA9YMQKBgQDCKSraD2YWoEeFO+CJityx8GNfVZbELETljvDbbxGyJDbhwh6y +AyHgxyZR7wHNN+UFkQN31d6kl/jbr/nDrVQ6KN2GjNwNhKu3oBSDGa9bcTRr2h1Y +32z2DTCvoPSJflptLSi+iVB7wd5rTxk7H+DJGt5O8nCGH+JRlX2xNN3pnQKBgDdA +u21eLM8cWNmNQj1WHoInfIsxSQEjEGtEYF4iWE5PfpTelWrz+IF0cjVxBHkTPGPI +LrQwdJS0LEmWxh2HgO3kv+TydpUKTHwMS6P3qlAzYXJL9K9TT1km3UnaFylf2h/e +pBwdY5q5YfdOlam50+9tKDTMkYZjMD9QaODooNlRAoGAOWow99WCATFtRrG+mGyl +UpwApgkZKT0nhkXUnLdNoQVeP0WHeQBSoOA24YnGBntvG/98Uj2rOwdCAYzTGepz +91bNqscrSOPdD3VN85GEl2DQKtxsRCKCdPKmYkvC/WMGhuzXSIp2U+ePgqEjEQO2 +Sn4xXZ1zwl+4cYHmDvzEQnA= +-----END PRIVATE KEY----- diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index e2642d9b8f..bb8d86376d 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -82,6 +82,13 @@ SystemAPIUsers: - "ORG_OWNER" - cypress: KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + - system-user-with-no-permissions: + KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFqTVdFWDZtK0gzWndaV1ptTUhxbApHbVoxa0wvRVlWZzJCb24yQm5wOU5LTXdoVTlpK29CcUwrR0FzVVZYdnBkMmhVTy9ZK1VpVzlRdnJ4K3ZBeVpNCmdrNjRRNlFFNm5ZMWJncnV3aEJDUC85ZWlMMzVvOTRHelhiS2RDSEF5bFNBQmRHemZaTDN1YUgwVndvRk9neU0KZkJveTdGMHFLRXA0bVp5ZUhmMFo3ZXZacVVyRDVNcEZMTjBhUnRqVWpwOTFpd0tGU29kYXY1S25sYW4vSGtQaQpzN3NnLzBmVURRRDRzZ2ZvcndManJWYnI1aUtxSTBHQ3VhUEwzazRQOEdnY1haczVJcHUzb1BDZXdWUTBvd1hoCjJvRXVTdlNDYS8wTmxYanRLMlRqbmlYeTVSL2NaVXF3NzNOd0NFdjl4N1pLaU51dkpEWkw2UnM5Q0xJT3RhVkUKTFFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" + Memberships: + # MemberType System allows the user to access all APIs for all instances or organizations + - MemberType: IAM + Roles: + - "NO_ROLES" InitProjections: Enabled: true diff --git a/internal/integration/system.go b/internal/integration/system.go index a9673a40ae..badc3db355 100644 --- a/internal/integration/system.go +++ b/internal/integration/system.go @@ -17,13 +17,16 @@ import ( var ( //go:embed config/system-user-key.pem systemUserKey []byte + //go:embed config/system-user-with-no-permissions.pem + systemUserWithNoPermissions []byte ) var ( // SystemClient creates a system connection once and reuses it on every use. // Each client call automatically gets the authorization context for the system user. - SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) - SystemToken string + SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) + SystemToken string + SystemUserWithNoPermissionsToken string ) func systemClient() system.SystemServiceClient { @@ -40,7 +43,7 @@ func systemClient() system.SystemServiceClient { return system.NewSystemServiceClient(cc) } -func systemUserToken() string { +func createSystemUserToken() string { const ISSUER = "tester" audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "") @@ -54,6 +57,24 @@ func systemUserToken() string { return token } +func createSystemUserWithNoPermissionsToken() string { + const ISSUER = "system-user-with-no-permissions" + audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) + signer, err := client.NewSignerFromPrivateKeyByte(systemUserWithNoPermissions, "") + if err != nil { + panic(err) + } + token, err := client.SignedJWTProfileAssertion(ISSUER, []string{audience}, time.Hour, signer) + if err != nil { + panic(err) + } + return token +} + func WithSystemAuthorization(ctx context.Context) context.Context { return WithAuthorizationToken(ctx, SystemToken) } + +func WithSystemUserWithNoPermissionsAuthorization(ctx context.Context) context.Context { + return WithAuthorizationToken(ctx, SystemUserWithNoPermissionsToken) +} diff --git a/internal/query/permission.go b/internal/query/permission.go index aeda33e541..c52b491144 100644 --- a/internal/query/permission.go +++ b/internal/query/permission.go @@ -2,51 +2,74 @@ package query import ( "context" + "encoding/json" "fmt" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/zerrors" ) const ( - // eventstore.permitted_orgs(instanceid text, userid text, perm text, filter_orgs text) - wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?))" + // eventstore.permitted_orgs(instanceid text, userid text, system_user_perms JSONB, perm text filter_orgs text) + wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?, ?))" wherePermittedOrgsOrCurrentUserClause = "(" + wherePermittedOrgsClause + " OR %s = ?" + ")" ) // wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs // for which the authenticated user has the requested permission for. // The user ID is taken from the context. -// // The `orgIDColumn` specifies the table column to which this filter must be applied, // and is typically the `resource_owner` column in ZITADEL. // We use full identifiers in the query builder so this function should be // called with something like `UserResourceOwnerCol.identifier()` for example. -func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) sq.SelectBuilder { - userID := authz.GetCtxData(ctx).UserID - logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "permission", permission, "user_id", userID).Debug("permitted orgs check used") +// func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) (sq.SelectBuilder, error) { +// userID := authz.GetCtxData(ctx).UserID +// logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "permission", permission, "user_id", userID).Debug("permitted orgs check used") - return query.Where( - fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), - authz.GetInstance(ctx).InstanceID(), - userID, - permission, - filterOrgIds, - ) -} +// systemUserPermissions := authz.GetSystemUserPermissions(ctx) +// var systemUserPermissionsJson []byte +// if systemUserPermissions != nil { +// var err error +// systemUserPermissionsJson, err = json.Marshal(systemUserPermissions) +// if err != nil { +// return query, err +// } +// } -func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, userIdColum, permission string) sq.SelectBuilder { +// return query.Where( +// fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), +// authz.GetInstance(ctx).InstanceID(), +// userID, +// systemUserPermissionsJson, +// permission, +// filterOrgIds, +// ), nil +// } + +func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, userIdColum, permission string) (sq.SelectBuilder, error) { userID := authz.GetCtxData(ctx).UserID logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "user_id_colum", userIdColum, "permission", permission, "user_id", userID).Debug("permitted orgs check used") + systemUserPermissions := authz.GetSystemUserPermissions(ctx) + var systemUserPermissionsJson []byte + if systemUserPermissions != nil { + var err error + systemUserPermissionsJson, err = json.Marshal(systemUserPermissions) + if err != nil { + return query, zerrors.ThrowInternal(err, "AUTHZ-HS4us", "Errors.Internal") + } + } + return query.Where( fmt.Sprintf(wherePermittedOrgsOrCurrentUserClause, orgIDColumn, userIdColum), authz.GetInstance(ctx).InstanceID(), userID, + systemUserPermissionsJson, permission, filterOrgIds, userID, - ) + ), nil } diff --git a/internal/query/user.go b/internal/query/user.go index 233faf4aa9..62de234b39 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -654,7 +654,10 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, f UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), }) if permissionCheckV2 { - query = wherePermittedOrgsOrCurrentUser(ctx, query, filterOrgIds, UserResourceOwnerCol.identifier(), UserIDCol.identifier(), domain.PermissionUserRead) + query, err = wherePermittedOrgsOrCurrentUser(ctx, query, filterOrgIds, UserResourceOwnerCol.identifier(), UserIDCol.identifier(), domain.PermissionUserRead) + if err != nil { + return nil, zerrors.ThrowInternal(err, "AUTHZ-HS4us", "Errors.Internal") + } } stmt, args, err := query.ToSql() @@ -736,15 +739,19 @@ func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { func NewUserOrSearchQuery(values []SearchQuery) (SearchQuery, error) { return NewOrQuery(values...) } + func NewUserAndSearchQuery(values []SearchQuery) (SearchQuery, error) { return NewAndQuery(values...) } + func NewUserNotSearchQuery(value SearchQuery) (SearchQuery, error) { return NewNotQuery(value) } + func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(UserIDCol, values) } + func NewUserInUserEmailsSearchQuery(values []string) (SearchQuery, error) { return NewInTextQuery(HumanEmailCol, values) } @@ -806,7 +813,7 @@ func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) { } func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (SearchQuery, error) { - //linking queries for the subselect + // linking queries for the subselect instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals) if err != nil { return nil, err @@ -815,12 +822,12 @@ func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (Searc if err != nil { return nil, err } - //text query to select data from the linked sub select + // text query to select data from the linked sub select loginNameQuery, err := NewTextQuery(LoginNameNameCol, value, comparison) if err != nil { return nil, err } - //full definition of the sub select + // full definition of the sub select subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, loginNameQuery}) if err != nil { return nil, err