diff --git a/cmd/setup/49/01-permitted_orgs_function.sql b/cmd/setup/49/01-permitted_orgs_function.sql index 9f291c016b..5e7f674c14 100644 --- a/cmd/setup/49/01-permitted_orgs_function.sql +++ b/cmd/setup/49/01-permitted_orgs_function.sql @@ -4,6 +4,7 @@ CREATE OR REPLACE FUNCTION eventstore.permitted_orgs( instanceId TEXT , userId TEXT , perm TEXT + , system_roles TEXT[] , filter_orgs TEXT , org_ids OUT TEXT[] @@ -18,17 +19,36 @@ BEGIN FROM eventstore.role_permissions rp WHERE rp.instance_id = instanceId AND rp.permission = perm; - + + IF system_roles IS NOT NULL THEN + DECLARE + permission_found_in_system_roles bool; + BEGIN + SELECT result.role_found INTO permission_found_in_system_roles + FROM (SELECT matched_roles && system_roles AS role_found) AS result; + + IF permission_found_in_system_roles THEN + 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; + END IF; + -- 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; + 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 @@ -54,3 +74,4 @@ BEGIN RETURN; END; $$; + diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index 2099b3e426..ea09e5209c 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -30,6 +30,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, if requiredAuthOption.Permission == authenticated { return func(parent context.Context) context.Context { + parent = propagateSystemMemberRoles(ctxData, parent) return context.WithValue(parent, dataKey, ctxData) }, nil } @@ -50,6 +51,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 = propagateSystemMemberRoles(ctxData, parent) return parent }, nil } @@ -125,3 +127,15 @@ func GetAllPermissionCtxIDs(perms []string) []string { } return ctxIDs } + +func propagateSystemMemberRoles(ctxData CtxData, parent context.Context) context.Context { + if ctxData.SystemMemberships != nil { + roles := make([]string, 0) + for _, member := range ctxData.SystemMemberships { + roles = append(roles, member.Roles...) + } + parent = context.WithValue(parent, SystemPermissionsKey, roles) + } + + return parent +} diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index ff401f8862..1dc8591032 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -22,6 +22,7 @@ const ( dataKey key = 2 allPermissionsKey key = 3 instanceKey key = 4 + SystemPermissionsKey key = 5 ) type CtxData struct { @@ -50,7 +51,7 @@ type Memberships []*Membership type Membership struct { MemberType MemberType AggregateID string - //ObjectID differs from aggregate id if object is sub of an aggregate + // ObjectID differs from aggregate id if object is sub of an aggregate ObjectID string Roles []string diff --git a/internal/query/permission.go b/internal/query/permission.go index aeda33e541..369e602414 100644 --- a/internal/query/permission.go +++ b/internal/query/permission.go @@ -11,8 +11,8 @@ import ( ) 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, perm text, system_roles text[], filter_orgs text) + wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?, ?))" wherePermittedOrgsOrCurrentUserClause = "(" + wherePermittedOrgsClause + " OR %s = ?" + ")" ) @@ -24,7 +24,7 @@ const ( // 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 { +func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, systemRoles []string, 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") @@ -33,11 +33,12 @@ func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgId authz.GetInstance(ctx).InstanceID(), userID, permission, + systemRoles, filterOrgIds, ) } -func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, userIdColum, permission string) sq.SelectBuilder { +func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder, systemRoles []string, filterOrgIds, orgIDColumn, userIdColum, permission string) sq.SelectBuilder { 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") @@ -46,6 +47,7 @@ func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder authz.GetInstance(ctx).InstanceID(), userID, permission, + systemRoles, filterOrgIds, userID, ) diff --git a/internal/query/user.go b/internal/query/user.go index 3ee9a48463..27633bd9e8 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -655,7 +655,17 @@ 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) + // extract system roles + var systemRoles []string + systemRolesValue := ctx.Value(authz.SystemPermissionsKey) + if systemRolesValue != nil { + var ok bool + systemRoles, ok = systemRolesValue.([]string) + if !ok { + return nil, zerrors.ThrowInternal(errors.New("unable to extract system roles"), "QUERY-GS9gs", "Errors.Internal") + } + } + query = wherePermittedOrgsOrCurrentUser(ctx, query, systemRoles, filterOrgIds, UserResourceOwnerCol.identifier(), UserIDCol.identifier(), domain.PermissionUserRead) } stmt, args, err := query.ToSql() @@ -737,15 +747,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) } @@ -807,7 +821,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 @@ -816,12 +830,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