mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-21 23:47:40 +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.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
|
||||||
|
@@ -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
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/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 {
|
||||||
|
@@ -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,
|
||||||
|
@@ -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(),
|
||||||
|
@@ -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)
|
||||||
|
@@ -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:
|
||||||
|
@@ -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
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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),
|
||||||
|
@@ -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{
|
||||||
|
@@ -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")
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
@@ -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"
|
- "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
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user