mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-22 09:07:45 +00:00
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:
@@ -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
|
||||
|
@@ -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,
|
||||
|
37
cmd/setup/52.go
Normal file
37
cmd/setup/52.go
Normal 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"
|
||||
}
|
43
cmd/setup/52/01-get-permissions-from-JSON.sql
Normal file
43
cmd/setup/52/01-get-permissions-from-JSON.sql
Normal 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;
|
||||
$$;
|
||||
|
144
cmd/setup/52/02-permitted_orgs_function.sql
Normal file
144
cmd/setup/52/02-permitted_orgs_function.sql
Normal 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;
|
||||
$$;
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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(),
|
||||
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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),
|
||||
|
@@ -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{
|
||||
|
@@ -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")
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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-----
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user