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
This commit is contained in:
Iraq
2025-03-26 15:15:17 +00:00
committed by GitHub
parent e6edf5f4cf
commit e4102ebd20
29 changed files with 854 additions and 94 deletions

View File

@@ -1713,6 +1713,298 @@ InternalAuthZ:
- "user.grant.read" - "user.grant.read"
- "user.membership.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) # 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: # 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 # https://zitadel.com/docs/self-hosting/manage/updating_scaling

View File

@@ -84,6 +84,7 @@ type ProjectionsConfig struct {
ExternalDomain string ExternalDomain string
ExternalSecure bool ExternalSecure bool
InternalAuthZ internal_authz.Config InternalAuthZ internal_authz.Config
SystemAuthZ internal_authz.Config
SystemDefaults systemdefaults.SystemDefaults SystemDefaults systemdefaults.SystemDefaults
Telemetry *handlers.TelemetryPusherConfig Telemetry *handlers.TelemetryPusherConfig
Login login.Config Login login.Config
@@ -150,7 +151,7 @@ func projections(
sessionTokenVerifier, sessionTokenVerifier,
func(q *query.Queries) domain.PermissionCheck { func(q *query.Queries) domain.PermissionCheck {
return func(ctx context.Context, permission, orgID, resourceID string) (err error) { 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, 0,
@@ -187,7 +188,7 @@ func projections(
keys.Target, keys.Target,
&http.Client{}, &http.Client{},
func(ctx context.Context, permission, orgID, resourceID string) (err error) { 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, sessionTokenVerifier,
config.OIDC.DefaultAccessTokenLifetime, config.OIDC.DefaultAccessTokenLifetime,

37
cmd/setup/52.go Normal file
View File

@@ -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"
}

View File

@@ -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;
$$;

View File

@@ -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;
$$;

View File

@@ -12,7 +12,7 @@ import (
"github.com/zitadel/zitadel/cmd/encryption" "github.com/zitadel/zitadel/cmd/encryption"
"github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/cmd/hooks"
"github.com/zitadel/zitadel/internal/actions" "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/oidc"
"github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/cache/connector" "github.com/zitadel/zitadel/internal/cache/connector"
@@ -34,7 +34,8 @@ type Config struct {
Database database.Config Database database.Config
Caches *connector.CachesConfig Caches *connector.CachesConfig
SystemDefaults systemdefaults.SystemDefaults SystemDefaults systemdefaults.SystemDefaults
InternalAuthZ internal_authz.Config InternalAuthZ authz.Config
SystemAuthZ authz.Config
ExternalDomain string ExternalDomain string
ExternalPort uint16 ExternalPort uint16
ExternalSecure bool ExternalSecure bool
@@ -53,7 +54,7 @@ type Config struct {
Login login.Config Login login.Config
WebAuthNName string WebAuthNName string
Telemetry *handlers.TelemetryPusherConfig Telemetry *handlers.TelemetryPusherConfig
SystemAPIUsers map[string]*internal_authz.SystemAPIUser SystemAPIUsers map[string]*authz.SystemAPIUser
} }
type InitProjections struct { type InitProjections struct {
@@ -68,12 +69,12 @@ func MustNewConfig(v *viper.Viper) *Config {
err := v.Unmarshal(config, err := v.Unmarshal(config,
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hooks.SliceTypeStringDecode[*domain.CustomMessageText], hooks.SliceTypeStringDecode[*domain.CustomMessageText],
hooks.SliceTypeStringDecode[internal_authz.RoleMapping], hooks.SliceTypeStringDecode[authz.RoleMapping],
hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], hooks.MapTypeStringDecode[string, *authz.SystemAPIUser],
hooks.MapHTTPHeaderStringDecode, hooks.MapHTTPHeaderStringDecode,
database.DecodeHook(false), database.DecodeHook(false),
actions.HTTPConfigDecodeHook, actions.HTTPConfigDecodeHook,
hook.EnumHookFunc(internal_authz.MemberTypeString), hook.EnumHookFunc(authz.MemberTypeString),
hook.Base64ToBytesHookFunc(), hook.Base64ToBytesHookFunc(),
hook.TagToLanguageHookFunc(), hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(), mapstructure.StringToTimeDurationHookFunc(),
@@ -142,6 +143,7 @@ type Steps struct {
s49InitPermittedOrgsFunction *InitPermittedOrgsFunction s49InitPermittedOrgsFunction *InitPermittedOrgsFunction
s50IDPTemplate6UsePKCE *IDPTemplate6UsePKCE s50IDPTemplate6UsePKCE *IDPTemplate6UsePKCE
s51IDPTemplate6RootCA *IDPTemplate6RootCA s51IDPTemplate6RootCA *IDPTemplate6RootCA
s52InitPermittedOrgsFunction *InitPermittedOrgsFunction52
} }
func MustNewSteps(v *viper.Viper) *Steps { func MustNewSteps(v *viper.Viper) *Steps {

View File

@@ -178,6 +178,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s49InitPermittedOrgsFunction = &InitPermittedOrgsFunction{eventstoreClient: dbClient} steps.s49InitPermittedOrgsFunction = &InitPermittedOrgsFunction{eventstoreClient: dbClient}
steps.s50IDPTemplate6UsePKCE = &IDPTemplate6UsePKCE{dbClient: dbClient} steps.s50IDPTemplate6UsePKCE = &IDPTemplate6UsePKCE{dbClient: dbClient}
steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient} steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient}
steps.s52InitPermittedOrgsFunction = &InitPermittedOrgsFunction52{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections") 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.s49InitPermittedOrgsFunction,
steps.s50IDPTemplate6UsePKCE, steps.s50IDPTemplate6UsePKCE,
steps.s51IDPTemplate6RootCA, steps.s51IDPTemplate6RootCA,
steps.s52InitPermittedOrgsFunction,
} { } {
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed") mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
} }
@@ -409,7 +411,7 @@ func startCommandsQueries(
sessionTokenVerifier, sessionTokenVerifier,
func(q *query.Queries) domain.PermissionCheck { func(q *query.Queries) domain.PermissionCheck {
return func(ctx context.Context, permission, orgID, resourceID string) (err error) { 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 0, // not needed for projections
@@ -434,7 +436,7 @@ func startCommandsQueries(
authZRepo, err := authz.Start(queries, eventstoreClient, dbClient, keys.OIDC, config.ExternalSecure) authZRepo, err := authz.Start(queries, eventstoreClient, dbClient, keys.OIDC, config.ExternalSecure)
logging.OnError(err).Fatal("unable to start authz repo") logging.OnError(err).Fatal("unable to start authz repo")
permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { 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, commands, err := command.StartCommands(ctx,

View File

@@ -11,7 +11,7 @@ import (
"github.com/zitadel/zitadel/cmd/hooks" "github.com/zitadel/zitadel/cmd/hooks"
"github.com/zitadel/zitadel/internal/actions" "github.com/zitadel/zitadel/internal/actions"
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" 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/http/middleware"
"github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/api/saml"
@@ -67,12 +67,13 @@ type Config struct {
Login login.Config Login login.Config
Console console.Config Console console.Config
AssetStorage static_config.AssetStorageConfig AssetStorage static_config.AssetStorageConfig
InternalAuthZ internal_authz.Config InternalAuthZ authz.Config
SystemAuthZ authz.Config
SystemDefaults systemdefaults.SystemDefaults SystemDefaults systemdefaults.SystemDefaults
EncryptionKeys *encryption.EncryptionKeyConfig EncryptionKeys *encryption.EncryptionKeyConfig
DefaultInstance command.InstanceSetup DefaultInstance command.InstanceSetup
AuditLogRetention time.Duration AuditLogRetention time.Duration
SystemAPIUsers map[string]*internal_authz.SystemAPIUser SystemAPIUsers map[string]*authz.SystemAPIUser
CustomerPortal string CustomerPortal string
Machine *id.Config Machine *id.Config
Actions *actions.Config Actions *actions.Config
@@ -96,12 +97,12 @@ func MustNewConfig(v *viper.Viper) *Config {
err := v.Unmarshal(config, err := v.Unmarshal(config,
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hooks.SliceTypeStringDecode[*domain.CustomMessageText], hooks.SliceTypeStringDecode[*domain.CustomMessageText],
hooks.SliceTypeStringDecode[internal_authz.RoleMapping], hooks.SliceTypeStringDecode[authz.RoleMapping],
hooks.MapTypeStringDecode[string, *internal_authz.SystemAPIUser], hooks.MapTypeStringDecode[string, *authz.SystemAPIUser],
hooks.MapHTTPHeaderStringDecode, hooks.MapHTTPHeaderStringDecode,
database.DecodeHook(false), database.DecodeHook(false),
actions.HTTPConfigDecodeHook, actions.HTTPConfigDecodeHook,
hook.EnumHookFunc(internal_authz.MemberTypeString), hook.EnumHookFunc(authz.MemberTypeString),
hooks.MapTypeStringDecode[domain.Feature, any], hooks.MapTypeStringDecode[domain.Feature, any],
hooks.SliceTypeStringDecode[*command.SetQuota], hooks.SliceTypeStringDecode[*command.SetQuota],
hook.Base64ToBytesHookFunc(), hook.Base64ToBytesHookFunc(),

View File

@@ -56,7 +56,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/system" "github.com/zitadel/zitadel/internal/api/grpc/system"
user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2"
user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" 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" http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/idp" "github.com/zitadel/zitadel/internal/api/idp"
@@ -193,7 +193,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
sessionTokenVerifier, sessionTokenVerifier,
func(q *query.Queries) domain.PermissionCheck { func(q *query.Queries) domain.PermissionCheck {
return func(ctx context.Context, permission, orgID, resourceID string) (err error) { 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, 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) return fmt.Errorf("error starting authz repo: %w", err)
} }
permissionCheck := func(ctx context.Context, permission, orgID, resourceID string) (err error) { 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) storage, err := config.AssetStorage.NewStorage(dbClient.DB)
@@ -418,7 +418,7 @@ func startAPIs(
http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))), http_util.WithMaxAge(int(math.Floor(config.Quotas.Access.ExhaustedCookieMaxAge.Seconds()))),
) )
limitingAccessInterceptor := middleware.NewAccessInterceptor(accessSvc, exhaustedCookieHandler, &config.Quotas.Access.AccessConfig) 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 { if err != nil {
return nil, fmt.Errorf("error creating api %w", err) return nil, fmt.Errorf("error creating api %w", err)
} }
@@ -501,7 +501,7 @@ func startAPIs(
} }
instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) 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)) apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler))
@@ -545,7 +545,7 @@ func startAPIs(
keys.User, keys.User,
&config.SCIM, &config.SCIM,
instanceInterceptor.HandlerFuncWithError, 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) c, err := console.Start(config.Console, config.ExternalSecure, oidcServer.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, limitingAccessInterceptor, config.CustomerPortal)
if err != nil { if err != nil {
@@ -611,7 +611,7 @@ func listen(ctx context.Context, router *mux.Router, port uint16, tlsConfig *tls
go func() { go func() {
logging.Infof("server is listening on %s", lis.Addr().String()) logging.Infof("server is listening on %s", lis.Addr().String())
if tlsConfig != nil { 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, "", "") errCh <- http1Server.ServeTLS(lis, "", "")
} else { } else {
errCh <- http1Server.Serve(lis) errCh <- http1Server.Serve(lis)

View File

@@ -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`. 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 keypair
Generate an RSA private key with 2048 bit modulus: Generate an RSA private key with 2048 bit modulus:

View File

@@ -15,7 +15,7 @@ import (
healthpb "google.golang.org/grpc/health/grpc_health_v1" healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection" "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" "github.com/zitadel/zitadel/internal/api/grpc/server"
http_util "github.com/zitadel/zitadel/internal/api/http" http_util "github.com/zitadel/zitadel/internal/api/http"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
@@ -29,7 +29,7 @@ import (
type API struct { type API struct {
port uint16 port uint16
grpcServer *grpc.Server grpcServer *grpc.Server
verifier internal_authz.APITokenVerifier verifier authz.APITokenVerifier
health healthCheck health healthCheck
router *mux.Router router *mux.Router
hostHeaders []string hostHeaders []string
@@ -72,8 +72,9 @@ func New(
port uint16, port uint16,
router *mux.Router, router *mux.Router,
queries *query.Queries, queries *query.Queries,
verifier internal_authz.APITokenVerifier, verifier authz.APITokenVerifier,
authZ internal_authz.Config, systemAuthz authz.Config,
authZ authz.Config,
tlsConfig *tls.Config, tlsConfig *tls.Config,
externalDomain string, externalDomain string,
hostHeaders []string, hostHeaders []string,
@@ -89,7 +90,7 @@ func New(
hostHeaders: hostHeaders, 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) api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -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) translator, err := i18n.NewZitadelTranslator(language.English)
logging.OnError(err).Panic("unable to get translator") logging.OnError(err).Panic("unable to get translator")
h := &Handler{ h := &Handler{
commands: commands, commands: commands,
errorHandler: DefaultErrorHandler(translator), errorHandler: DefaultErrorHandler(translator),
authInterceptor: http_mw.AuthorizationInterceptor(verifier, authConfig), authInterceptor: http_mw.AuthorizationInterceptor(verifier, systemAuthCOnfig, authConfig),
idGenerator: idGenerator, idGenerator: idGenerator,
storage: storage, storage: storage,
query: queries, query: queries,
@@ -129,8 +129,10 @@ func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string
return ownerPath return ownerPath
} }
const maxMemory = 2 << 20 const (
const paramFile = "file" maxMemory = 2 << 20
paramFile = "file"
)
func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) { func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {

View File

@@ -4,8 +4,11 @@ import (
"context" "context"
"fmt" "fmt"
"reflect" "reflect"
"slices"
"strings" "strings"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
) )
@@ -16,10 +19,10 @@ const (
// CheckUserAuthorization verifies that: // CheckUserAuthorization verifies that:
// - the token is active, // - 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) // - 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] // 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) ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@@ -30,11 +33,12 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID,
if requiredAuthOption.Permission == authenticated { if requiredAuthOption.Permission == authenticated {
return func(parent context.Context) context.Context { return func(parent context.Context) context.Context {
parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData)
return context.WithValue(parent, dataKey, ctxData) return context.WithValue(parent, dataKey, ctxData)
}, nil }, 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 { if err != nil {
return nil, err 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, dataKey, ctxData)
parent = context.WithValue(parent, allPermissionsKey, allPermissions) parent = context.WithValue(parent, allPermissionsKey, allPermissions)
parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions) parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions)
parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData)
return parent return parent
}, nil }, nil
} }
@@ -125,3 +130,43 @@ func GetAllPermissionCtxIDs(perms []string) []string {
} }
return ctxIDs 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
}

View File

@@ -22,6 +22,7 @@ const (
dataKey key = 2 dataKey key = 2
allPermissionsKey key = 3 allPermissionsKey key = 3
instanceKey key = 4 instanceKey key = 4
systemUserRolesKey key = 5
) )
type CtxData struct { type CtxData struct {
@@ -50,7 +51,8 @@ type Memberships []*Membership
type Membership struct { type Membership struct {
MemberType MemberType MemberType MemberType
AggregateID string 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 ObjectID string
Roles []string Roles []string

View File

@@ -7,8 +7,8 @@ import (
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
) )
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) { func CheckPermission(ctx context.Context, resolver MembershipsResolver, systemUserRoleMapping []RoleMapping, roleMappings []RoleMapping, permission, orgID, resourceID string) (err error) {
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, GetCtxData(ctx), orgID) requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, systemUserRoleMapping, roleMappings, GetCtxData(ctx), orgID)
if err != nil { if err != nil {
return err 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), // 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. // 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) ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }() defer func() { span.EndWithError(err) }()
@@ -31,7 +31,7 @@ func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requi
} }
if ctxData.SystemMemberships != nil { if ctxData.SystemMemberships != nil {
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, roleMappings) requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, ctxData.SystemMemberships, systemUserRoleMappings)
return requestedPermissions, allPermissions, nil return requestedPermissions, allPermissions, nil
} }

View File

@@ -120,7 +120,7 @@ func Test_GetUserPermissions(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err) t.Errorf("got wrong result, should get err: actual: %v ", err)

View File

@@ -13,13 +13,13 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing" "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 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) authOpt, needsToken := verifier.CheckAuthMethod(info.FullMethod)
if !needsToken { if !needsToken {
return handler(ctx, req) return handler(ctx, req)
@@ -34,7 +34,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
} }
orgID, orgDomain := orgIDAndDomainFromRequest(authCtx, req) 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -20,6 +20,7 @@ type authzRepoMock struct{}
func (v *authzRepoMock) 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 return "", "", "", "", "", nil
} }
func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) { func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) {
return authz.Memberships{{ return authz.Memberships{{
MemberType: authz.MemberTypeOrganization, 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) { func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) {
return "", nil, nil return "", nil, nil
} }
func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) {
return orgID, nil return orgID, nil
} }
func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) {
return "", "", nil return "", "", nil
} }
@@ -252,7 +255,7 @@ func Test_authorize(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { if (err != nil) != tt.res.wantErr {
t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr) t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr)
return return

View File

@@ -36,6 +36,7 @@ type WithGatewayPrefix interface {
func CreateServer( func CreateServer(
verifier authz.APITokenVerifier, verifier authz.APITokenVerifier,
systemAuthz authz.Config,
authConfig authz.Config, authConfig authz.Config,
queries *query.Queries, queries *query.Queries,
externalDomain string, externalDomain string,
@@ -53,7 +54,7 @@ func CreateServer(
middleware.AccessStorageInterceptor(accessSvc), middleware.AccessStorageInterceptor(accessSvc),
middleware.ErrorHandler(), middleware.ErrorHandler(),
middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName),
middleware.AuthorizationInterceptor(verifier, authConfig), middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig),
middleware.TranslationHandler(), middleware.TranslationHandler(),
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName), middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
middleware.ExecutionHandler(queries), middleware.ExecutionHandler(queries),

View File

@@ -415,6 +415,10 @@ func createUsers(ctx context.Context, orgID string, count int, passwordChangeReq
func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr { func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr {
username := gofakeit.Email() 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 // used as default country prefix
phone := "+41" + gofakeit.Phone() phone := "+41" + gofakeit.Phone()
resp := Instance.CreateHumanUserVerified(ctx, orgID, username, 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 { func InUserIDsQuery(ids []string) *user.SearchQuery {
return &user.SearchQuery{ return &user.SearchQuery{
Query: &user.SearchQuery_InUserIdsQuery{ Query: &user.SearchQuery_InUserIdsQuery{

View File

@@ -31,12 +31,13 @@ import (
) )
var ( var (
CTX context.Context CTX context.Context
IamCTX context.Context IamCTX context.Context
UserCTX context.Context UserCTX context.Context
SystemCTX context.Context SystemCTX context.Context
Instance *integration.Instance SystemUserWithNoPermissionsCTX context.Context
Client user.UserServiceClient Instance *integration.Instance
Client user.UserServiceClient
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@@ -46,6 +47,7 @@ func TestMain(m *testing.M) {
Instance = integration.NewInstance(ctx) Instance = integration.NewInstance(ctx)
SystemUserWithNoPermissionsCTX = integration.WithSystemUserWithNoPermissionsAuthorization(ctx)
UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission) UserCTX = Instance.WithAuthorization(ctx, integration.UserTypeNoPermission)
IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) IamCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
SystemCTX = integration.WithSystemAuthorization(ctx) SystemCTX = integration.WithSystemAuthorization(ctx)
@@ -1306,7 +1308,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req) got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req)
if tt.wantErr { if tt.wantErr {
require.Error(t, err) require.Error(t, err)
@@ -3048,7 +3049,6 @@ func TestServer_ListAuthenticationFactors(t *testing.T) {
assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult()) assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult())
}, retryDuration, tick, "timeout waiting for expected auth methods result") }, retryDuration, tick, "timeout waiting for expected auth methods result")
}) })
} }
} }

View File

@@ -14,14 +14,16 @@ import (
) )
type AuthInterceptor struct { type AuthInterceptor struct {
verifier authz.APITokenVerifier verifier authz.APITokenVerifier
authConfig authz.Config 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{ return &AuthInterceptor{
verifier: verifier, verifier: verifier,
authConfig: authConfig, 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 { func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized) http.Error(w, err.Error(), http.StatusUnauthorized)
return return
@@ -44,7 +46,7 @@ func (a *AuthInterceptor) HandlerFunc(next http.Handler) http.HandlerFunc {
func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) HandlerFuncWithError { func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) HandlerFuncWithError {
return func(w http.ResponseWriter, r *http.Request) error { 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 { if err != nil {
return err return err
} }
@@ -56,7 +58,7 @@ func (a *AuthInterceptor) HandlerFuncWithError(next HandlerFuncWithError) Handle
type httpReq struct{} 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() ctx := r.Context()
authOpt, needsToken := checkAuthMethod(r, verifier) 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") 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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -73,7 +73,7 @@ func privateIPv4() (net.IP, error) {
} }
} }
//change: use "POD_IP" // change: use "POD_IP"
ip := net.ParseIP(os.Getenv("POD_IP")) ip := net.ParseIP(os.Getenv("POD_IP"))
if ip == nil { if ip == nil {
return nil, errors.New("no private ip address") 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") 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 return 0, nil
} }

View File

@@ -20,10 +20,8 @@ type Config struct {
WebAuthNName string WebAuthNName string
} }
var ( //go:embed config/client.yaml
//go:embed config/client.yaml var clientYAML []byte
clientYAML []byte
)
var ( var (
tmpDir string tmpDir string
@@ -49,5 +47,6 @@ func init() {
if err := loadedConfig.Log.SetLogger(); err != nil { if err := loadedConfig.Log.SetLogger(); err != nil {
panic(err) panic(err)
} }
SystemToken = systemUserToken() SystemToken = createSystemUserToken()
SystemUserWithNoPermissionsToken = createSystemUserWithNoPermissionsToken()
} }

View File

@@ -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-----

View File

@@ -82,6 +82,13 @@ SystemAPIUsers:
- "ORG_OWNER" - "ORG_OWNER"
- cypress: - cypress:
KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==" 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: InitProjections:
Enabled: true Enabled: true

View File

@@ -17,13 +17,16 @@ import (
var ( var (
//go:embed config/system-user-key.pem //go:embed config/system-user-key.pem
systemUserKey []byte systemUserKey []byte
//go:embed config/system-user-with-no-permissions.pem
systemUserWithNoPermissions []byte
) )
var ( var (
// SystemClient creates a system connection once and reuses it on every use. // SystemClient creates a system connection once and reuses it on every use.
// Each client call automatically gets the authorization context for the system user. // Each client call automatically gets the authorization context for the system user.
SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient) SystemClient = sync.OnceValue[system.SystemServiceClient](systemClient)
SystemToken string SystemToken string
SystemUserWithNoPermissionsToken string
) )
func systemClient() system.SystemServiceClient { func systemClient() system.SystemServiceClient {
@@ -40,7 +43,7 @@ func systemClient() system.SystemServiceClient {
return system.NewSystemServiceClient(cc) return system.NewSystemServiceClient(cc)
} }
func systemUserToken() string { func createSystemUserToken() string {
const ISSUER = "tester" const ISSUER = "tester"
audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure) audience := http_util.BuildOrigin(loadedConfig.Host(), loadedConfig.Secure)
signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "") signer, err := client.NewSignerFromPrivateKeyByte(systemUserKey, "")
@@ -54,6 +57,24 @@ func systemUserToken() string {
return token 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 { func WithSystemAuthorization(ctx context.Context) context.Context {
return WithAuthorizationToken(ctx, SystemToken) return WithAuthorizationToken(ctx, SystemToken)
} }
func WithSystemUserWithNoPermissionsAuthorization(ctx context.Context) context.Context {
return WithAuthorizationToken(ctx, SystemUserWithNoPermissionsToken)
}

View File

@@ -2,51 +2,74 @@ package query
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
sq "github.com/Masterminds/squirrel" sq "github.com/Masterminds/squirrel"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/zerrors"
) )
const ( const (
// eventstore.permitted_orgs(instanceid text, userid text, perm text, filter_orgs text) // eventstore.permitted_orgs(instanceid text, userid text, system_user_perms JSONB, perm text filter_orgs text)
wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?))" wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?, ?))"
wherePermittedOrgsOrCurrentUserClause = "(" + wherePermittedOrgsClause + " OR %s = ?" + ")" wherePermittedOrgsOrCurrentUserClause = "(" + wherePermittedOrgsClause + " OR %s = ?" + ")"
) )
// wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs // wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs
// for which the authenticated user has the requested permission for. // for which the authenticated user has the requested permission for.
// The user ID is taken from the context. // The user ID is taken from the context.
//
// The `orgIDColumn` specifies the table column to which this filter must be applied, // The `orgIDColumn` specifies the table column to which this filter must be applied,
// and is typically the `resource_owner` column in ZITADEL. // and is typically the `resource_owner` column in ZITADEL.
// We use full identifiers in the query builder so this function should be // We use full identifiers in the query builder so this function should be
// called with something like `UserResourceOwnerCol.identifier()` for example. // called with something like `UserResourceOwnerCol.identifier()` for example.
func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) sq.SelectBuilder { // func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) (sq.SelectBuilder, error) {
userID := authz.GetCtxData(ctx).UserID // 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") // 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( // systemUserPermissions := authz.GetSystemUserPermissions(ctx)
fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), // var systemUserPermissionsJson []byte
authz.GetInstance(ctx).InstanceID(), // if systemUserPermissions != nil {
userID, // var err error
permission, // systemUserPermissionsJson, err = json.Marshal(systemUserPermissions)
filterOrgIds, // 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 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") 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( return query.Where(
fmt.Sprintf(wherePermittedOrgsOrCurrentUserClause, orgIDColumn, userIdColum), fmt.Sprintf(wherePermittedOrgsOrCurrentUserClause, orgIDColumn, userIdColum),
authz.GetInstance(ctx).InstanceID(), authz.GetInstance(ctx).InstanceID(),
userID, userID,
systemUserPermissionsJson,
permission, permission,
filterOrgIds, filterOrgIds,
userID, userID,
) ), nil
} }

View File

@@ -654,7 +654,10 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, f
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}) })
if permissionCheckV2 { 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() stmt, args, err := query.ToSql()
@@ -736,15 +739,19 @@ func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error {
func NewUserOrSearchQuery(values []SearchQuery) (SearchQuery, error) { func NewUserOrSearchQuery(values []SearchQuery) (SearchQuery, error) {
return NewOrQuery(values...) return NewOrQuery(values...)
} }
func NewUserAndSearchQuery(values []SearchQuery) (SearchQuery, error) { func NewUserAndSearchQuery(values []SearchQuery) (SearchQuery, error) {
return NewAndQuery(values...) return NewAndQuery(values...)
} }
func NewUserNotSearchQuery(value SearchQuery) (SearchQuery, error) { func NewUserNotSearchQuery(value SearchQuery) (SearchQuery, error) {
return NewNotQuery(value) return NewNotQuery(value)
} }
func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) { func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) {
return NewInTextQuery(UserIDCol, values) return NewInTextQuery(UserIDCol, values)
} }
func NewUserInUserEmailsSearchQuery(values []string) (SearchQuery, error) { func NewUserInUserEmailsSearchQuery(values []string) (SearchQuery, error) {
return NewInTextQuery(HumanEmailCol, values) return NewInTextQuery(HumanEmailCol, values)
} }
@@ -806,7 +813,7 @@ func NewUserLoginNamesSearchQuery(value string) (SearchQuery, error) {
} }
func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (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) instanceQuery, err := NewColumnComparisonQuery(LoginNameInstanceIDCol, UserInstanceIDCol, ColumnEquals)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -815,12 +822,12 @@ func NewUserLoginNameExistsQuery(value string, comparison TextComparison) (Searc
if err != nil { if err != nil {
return nil, err 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) loginNameQuery, err := NewTextQuery(LoginNameNameCol, value, comparison)
if err != nil { if err != nil {
return nil, err return nil, err
} }
//full definition of the sub select // full definition of the sub select
subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, loginNameQuery}) subSelect, err := NewSubSelect(LoginNameUserIDCol, []SearchQuery{instanceQuery, userIDQuery, loginNameQuery})
if err != nil { if err != nil {
return nil, err return nil, err