mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 19:07:30 +00:00
perf: role permissions in database (#9152)
# Which Problems Are Solved Currently ZITADEL defines organization and instance member roles and permissions in defaults.yaml. The permission check is done on API call level. For example: "is this user allowed to make this call on this org". This makes sense on the V1 API where the API is permission-level shaped. For example, a search for users always happens in the context of the organization. (Either the organization the calling user belongs to, or through member ship and the x-zitadel-orgid header. However, for resource based APIs we must be able to resolve permissions by object. For example, an IAM_OWNER listing users should be able to get all users in an instance based on the query filters. Alternatively a user may have user.read permissions on one or more orgs. They should be able to read just those users. # How the Problems Are Solved ## Role permission mapping The role permission mappings defined from `defaults.yaml` or local config override are synchronized to the database on every run of `zitadel setup`: - A single query per **aggregate** builds a list of `add` and `remove` actions needed to reach the desired state or role permission mappings from the config. - The required events based on the actions are pushed to the event store. - Events define search fields so that permission checking can use the indices and is strongly consistent for both query and command sides. The migration is split in the following aggregates: - System aggregate for for roles prefixed with `SYSTEM` - Each instance for roles not prefixed with `SYSTEM`. This is in anticipation of instance level management over the API. ## Membership Current instance / org / project membership events now have field table definitions. Like the role permissions this ensures strong consistency while still being able to use the indices of the fields table. A migration is provided to fill the membership fields. ## Permission check I aimed keeping the mental overhead to the developer to a minimal. The provided implementation only provides a permission check for list queries for org level resources, for example users. In the `query` package there is a simple helper function `wherePermittedOrgs` which makes sure the underlying database function is called as part of the `SELECT` query and the permitted organizations are part of the `WHERE` clause. This makes sure results from non-permitted organizations are omitted. Under the hood: - A Pg/PlSQL function searches for a list of organization IDs the passed user has the passed permission. - When the user has the permission on instance level, it returns early with all organizations. - The functions uses a number of views. The views help mapping the fields entries into relational data and simplify the code use for the function. The views provide some pre-filters which allow proper index usage once the final `WHERE` clauses are set by the function. # Additional Changes # Additional Context Closes #9032 Closes https://github.com/zitadel/zitadel/issues/9014 https://github.com/zitadel/zitadel/issues/9188 defines follow-ups for the new permission framework based on this concept.
This commit is contained in:
@@ -1132,6 +1132,7 @@ DefaultInstance:
|
|||||||
LoginDefaultOrg: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINDEFAULTORG
|
LoginDefaultOrg: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINDEFAULTORG
|
||||||
# TriggerIntrospectionProjections: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TRIGGERINTROSPECTIONPROJECTIONS
|
# TriggerIntrospectionProjections: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TRIGGERINTROSPECTIONPROJECTIONS
|
||||||
# LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION
|
# LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION
|
||||||
|
# PermissionCheckV2: false # ZITADEL_DEFAULTINSTANCE_FEATURES_PERMISSIONCHECKV2
|
||||||
Limits:
|
Limits:
|
||||||
# AuditLogRetention limits the number of events that can be queried via the events API by their age.
|
# AuditLogRetention limits the number of events that can be queried via the events API by their age.
|
||||||
# A value of "0s" means that all events are available.
|
# A value of "0s" means that all events are available.
|
||||||
@@ -1195,6 +1196,9 @@ InternalAuthZ:
|
|||||||
# Configure the RolePermissionMappings by environment variable using JSON notation:
|
# Configure the RolePermissionMappings by environment variable using JSON notation:
|
||||||
# ZITADEL_INTERNALAUTHZ_ROLEPERMISSIONMAPPINGS='[{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write"]}]'
|
# ZITADEL_INTERNALAUTHZ_ROLEPERMISSIONMAPPINGS='[{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write"]}]'
|
||||||
# Beware that if you configure the RolePermissionMappings by environment variable, all the default RolePermissionMappings are lost.
|
# Beware that if you configure the RolePermissionMappings by environment variable, all the default RolePermissionMappings are lost.
|
||||||
|
#
|
||||||
|
# Warning: RolePermissionMappings are synhronized to the database.
|
||||||
|
# Changes here will only be applied after running `zitadel setup` or `zitadel start-from-setup`.
|
||||||
RolePermissionMappings:
|
RolePermissionMappings:
|
||||||
- Role: "SYSTEM_OWNER"
|
- Role: "SYSTEM_OWNER"
|
||||||
Permissions:
|
Permissions:
|
||||||
|
39
cmd/setup/46.go
Normal file
39
cmd/setup/46.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InitPermissionFunctions struct {
|
||||||
|
eventstoreClient *database.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed 46/*.sql
|
||||||
|
permissionFunctions embed.FS
|
||||||
|
)
|
||||||
|
|
||||||
|
func (mig *InitPermissionFunctions) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||||
|
statements, err := readStatements(permissionFunctions, "46", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, stmt := range statements {
|
||||||
|
logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement")
|
||||||
|
if _, err := mig.eventstoreClient.ExecContext(ctx, stmt.query); err != nil {
|
||||||
|
return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InitPermissionFunctions) String() string {
|
||||||
|
return "46_init_permission_functions"
|
||||||
|
}
|
6
cmd/setup/46/01-role_permissions_view.sql
Normal file
6
cmd/setup/46/01-role_permissions_view.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE OR REPLACE VIEW eventstore.role_permissions AS
|
||||||
|
SELECT instance_id, aggregate_id, object_id as role, text_value as permission
|
||||||
|
FROM eventstore.fields
|
||||||
|
WHERE aggregate_type = 'permission'
|
||||||
|
AND object_type = 'role_permission'
|
||||||
|
AND field_name = 'permission';
|
6
cmd/setup/46/02-instance_orgs_view.sql
Normal file
6
cmd/setup/46/02-instance_orgs_view.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE OR REPLACE VIEW eventstore.instance_orgs AS
|
||||||
|
SELECT instance_id, aggregate_id as org_id
|
||||||
|
FROM eventstore.fields
|
||||||
|
WHERE aggregate_type = 'org'
|
||||||
|
AND object_type = 'org'
|
||||||
|
AND field_name = 'state';
|
6
cmd/setup/46/03-instance_members_view.sql
Normal file
6
cmd/setup/46/03-instance_members_view.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE OR REPLACE VIEW eventstore.instance_members AS
|
||||||
|
SELECT instance_id, object_id as user_id, text_value as role
|
||||||
|
FROM eventstore.fields
|
||||||
|
WHERE aggregate_type = 'instance'
|
||||||
|
AND object_type = 'instance_member_role'
|
||||||
|
AND field_name = 'instance_role';
|
6
cmd/setup/46/04-org_members_view.sql
Normal file
6
cmd/setup/46/04-org_members_view.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE OR REPLACE VIEW eventstore.org_members AS
|
||||||
|
SELECT instance_id, aggregate_id as org_id, object_id as user_id, text_value as role
|
||||||
|
FROM eventstore.fields
|
||||||
|
WHERE aggregate_type = 'org'
|
||||||
|
AND object_type = 'org_member_role'
|
||||||
|
AND field_name = 'org_role';
|
6
cmd/setup/46/05-project_members_view.sql
Normal file
6
cmd/setup/46/05-project_members_view.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE OR REPLACE VIEW eventstore.project_members AS
|
||||||
|
SELECT instance_id, aggregate_id as project_id, object_id as user_id, text_value as role
|
||||||
|
FROM eventstore.fields
|
||||||
|
WHERE aggregate_type = 'project'
|
||||||
|
AND object_type = 'project_member_role'
|
||||||
|
AND field_name = 'project_role';
|
50
cmd/setup/46/06-permitted_orgs_function.sql
Normal file
50
cmd/setup/46/06-permitted_orgs_function.sql
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION eventstore.permitted_orgs(
|
||||||
|
instanceId TEXT
|
||||||
|
, userId TEXT
|
||||||
|
, perm TEXT
|
||||||
|
|
||||||
|
, org_ids OUT TEXT[]
|
||||||
|
)
|
||||||
|
LANGUAGE 'plpgsql'
|
||||||
|
STABLE
|
||||||
|
AS $$
|
||||||
|
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
|
||||||
|
SELECT array_agg(o.org_id) INTO org_ids
|
||||||
|
FROM eventstore.instance_orgs o
|
||||||
|
WHERE o.instance_id = instanceId;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Return the organizations where permission were granted thru org-level roles
|
||||||
|
SELECT array_agg(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
|
||||||
|
);
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
$$;
|
@@ -87,6 +87,9 @@ func MustNewConfig(v *viper.Viper) *Config {
|
|||||||
|
|
||||||
id.Configure(config.Machine)
|
id.Configure(config.Machine)
|
||||||
|
|
||||||
|
// Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API.
|
||||||
|
config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +134,7 @@ type Steps struct {
|
|||||||
s43CreateFieldsDomainIndex *CreateFieldsDomainIndex
|
s43CreateFieldsDomainIndex *CreateFieldsDomainIndex
|
||||||
s44ReplaceCurrentSequencesIndex *ReplaceCurrentSequencesIndex
|
s44ReplaceCurrentSequencesIndex *ReplaceCurrentSequencesIndex
|
||||||
s45CorrectProjectOwners *CorrectProjectOwners
|
s45CorrectProjectOwners *CorrectProjectOwners
|
||||||
|
s46InitPermissionFunctions *InitPermissionFunctions
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustNewSteps(v *viper.Viper) *Steps {
|
func MustNewSteps(v *viper.Viper) *Steps {
|
||||||
|
@@ -174,6 +174,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
|||||||
steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{dbClient: queryDBClient}
|
steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{dbClient: queryDBClient}
|
||||||
steps.s44ReplaceCurrentSequencesIndex = &ReplaceCurrentSequencesIndex{dbClient: esPusherDBClient}
|
steps.s44ReplaceCurrentSequencesIndex = &ReplaceCurrentSequencesIndex{dbClient: esPusherDBClient}
|
||||||
steps.s45CorrectProjectOwners = &CorrectProjectOwners{eventstore: eventstoreClient}
|
steps.s45CorrectProjectOwners = &CorrectProjectOwners{eventstore: eventstoreClient}
|
||||||
|
steps.s46InitPermissionFunctions = &InitPermissionFunctions{eventstoreClient: esPusherDBClient}
|
||||||
|
|
||||||
err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil)
|
err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||||
logging.OnError(err).Fatal("unable to start projections")
|
logging.OnError(err).Fatal("unable to start projections")
|
||||||
@@ -196,6 +197,10 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
|||||||
&FillFieldsForInstanceDomains{
|
&FillFieldsForInstanceDomains{
|
||||||
eventstore: eventstoreClient,
|
eventstore: eventstoreClient,
|
||||||
},
|
},
|
||||||
|
&SyncRolePermissions{
|
||||||
|
eventstore: eventstoreClient,
|
||||||
|
rolePermissionMappings: config.InternalAuthZ.RolePermissionMappings,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, step := range []migration.Migration{
|
for _, step := range []migration.Migration{
|
||||||
@@ -229,6 +234,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
|||||||
steps.s38BackChannelLogoutNotificationStart,
|
steps.s38BackChannelLogoutNotificationStart,
|
||||||
steps.s44ReplaceCurrentSequencesIndex,
|
steps.s44ReplaceCurrentSequencesIndex,
|
||||||
steps.s45CorrectProjectOwners,
|
steps.s45CorrectProjectOwners,
|
||||||
|
steps.s46InitPermissionFunctions,
|
||||||
} {
|
} {
|
||||||
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
|
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
|
||||||
}
|
}
|
||||||
|
134
cmd/setup/sync_role_permissions.go
Normal file
134
cmd/setup/sync_role_permissions.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/database"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed sync_role_permissions.sql
|
||||||
|
getRolePermissionOperationsQuery string
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncRolePermissions is a repeatable step which synchronizes the InternalAuthZ
|
||||||
|
// RolePermissionMappings from the configuration to the database.
|
||||||
|
// This is needed until role permissions are manageable over the API.
|
||||||
|
type SyncRolePermissions struct {
|
||||||
|
eventstore *eventstore.Eventstore
|
||||||
|
rolePermissionMappings []authz.RoleMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mig *SyncRolePermissions) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||||
|
if err := mig.executeSystem(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return mig.executeInstances(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mig *SyncRolePermissions) executeSystem(ctx context.Context) error {
|
||||||
|
logging.WithFields("migration", mig.String()).Info("prepare system role permission sync events")
|
||||||
|
|
||||||
|
target := rolePermissionMappingsToDatabaseMap(mig.rolePermissionMappings, true)
|
||||||
|
cmds, err := mig.synchronizeCommands(ctx, "SYSTEM", target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
events, err := mig.eventstore.Push(ctx, cmds...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.WithFields("migration", mig.String(), "pushed_events", len(events)).Info("pushed system role permission sync events")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mig *SyncRolePermissions) executeInstances(ctx context.Context) error {
|
||||||
|
instances, err := mig.eventstore.InstanceIDs(
|
||||||
|
ctx,
|
||||||
|
eventstore.NewSearchQueryBuilder(eventstore.ColumnsInstanceIDs).
|
||||||
|
OrderDesc().
|
||||||
|
AddQuery().
|
||||||
|
AggregateTypes(instance.AggregateType).
|
||||||
|
EventTypes(instance.InstanceAddedEventType).
|
||||||
|
Builder().
|
||||||
|
ExcludeAggregateIDs().
|
||||||
|
AggregateTypes(instance.AggregateType).
|
||||||
|
EventTypes(instance.InstanceRemovedEventType).
|
||||||
|
Builder(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := rolePermissionMappingsToDatabaseMap(mig.rolePermissionMappings, false)
|
||||||
|
for i, instanceID := range instances {
|
||||||
|
logging.WithFields("instance_id", instanceID, "migration", mig.String(), "progress", fmt.Sprintf("%d/%d", i+1, len(instances))).Info("prepare instance role permission sync events")
|
||||||
|
cmds, err := mig.synchronizeCommands(ctx, instanceID, target)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
events, err := mig.eventstore.Push(ctx, cmds...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logging.WithFields("instance_id", instanceID, "migration", mig.String(), "pushed_events", len(events)).Info("pushed instance role permission sync events")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// synchronizeCommands checks the current state of role permissions in the eventstore for the aggregate.
|
||||||
|
// It returns the commands required to reach the desired state passed in target.
|
||||||
|
// For system level permissions aggregateID must be set to `SYSTEM`,
|
||||||
|
// else it is the instance ID.
|
||||||
|
func (mig *SyncRolePermissions) synchronizeCommands(ctx context.Context, aggregateID string, target database.Map[[]string]) (cmds []eventstore.Command, err error) {
|
||||||
|
aggregate := permission.NewAggregate(aggregateID)
|
||||||
|
err = mig.eventstore.Client().QueryContext(ctx, func(rows *sql.Rows) error {
|
||||||
|
for rows.Next() {
|
||||||
|
var operation, role, perm string
|
||||||
|
if err := rows.Scan(&operation, &role, &perm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logging.WithFields("aggregate_id", aggregateID, "migration", mig.String(), "operation", operation, "role", role, "permission", perm).Debug("sync role permission")
|
||||||
|
switch operation {
|
||||||
|
case "add":
|
||||||
|
cmds = append(cmds, permission.NewAddedEvent(ctx, aggregate, role, perm))
|
||||||
|
case "remove":
|
||||||
|
cmds = append(cmds, permission.NewRemovedEvent(ctx, aggregate, role, perm))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.Close()
|
||||||
|
|
||||||
|
}, getRolePermissionOperationsQuery, aggregateID, target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmds, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SyncRolePermissions) String() string {
|
||||||
|
return "repeatable_sync_role_permissions"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SyncRolePermissions) Check(lastRun map[string]interface{}) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func rolePermissionMappingsToDatabaseMap(mappings []authz.RoleMapping, system bool) database.Map[[]string] {
|
||||||
|
out := make(database.Map[[]string], len(mappings))
|
||||||
|
for _, m := range mappings {
|
||||||
|
if system == strings.HasPrefix(m.Role, "SYSTEM") {
|
||||||
|
out[m.Role] = m.Permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
52
cmd/setup/sync_role_permissions.sql
Normal file
52
cmd/setup/sync_role_permissions.sql
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
This query creates a change set of permissions that need to be added or removed.
|
||||||
|
It compares the current state in the fields table (thru the role_permissions view)
|
||||||
|
against a passed role permission mapping as JSON, created from Zitadel's config:
|
||||||
|
|
||||||
|
{
|
||||||
|
"IAM_ADMIN_IMPERSONATOR": ["admin.impersonation", "impersonation"],
|
||||||
|
"IAM_END_USER_IMPERSONATOR": ["impersonation"],
|
||||||
|
"FOO_BAR": ["foo.bar", "bar.foo"]
|
||||||
|
}
|
||||||
|
|
||||||
|
It uses an aggregate_id as first argument which may be an instance_id or 'SYSTEM'
|
||||||
|
for system level permissions.
|
||||||
|
*/
|
||||||
|
WITH target AS (
|
||||||
|
-- unmarshal JSON representation into flattened tabular data
|
||||||
|
SELECT
|
||||||
|
key AS role,
|
||||||
|
jsonb_array_elements_text(value) AS permission
|
||||||
|
FROM jsonb_each($2::jsonb)
|
||||||
|
), add AS (
|
||||||
|
-- find all role permissions that exist in `target` and not in `role_permissions`
|
||||||
|
SELECT t.role, t.permission
|
||||||
|
FROM eventstore.role_permissions p
|
||||||
|
RIGHT JOIN target t
|
||||||
|
ON p.aggregate_id = $1::text
|
||||||
|
AND p.role = t.role
|
||||||
|
AND p.permission = t.permission
|
||||||
|
WHERE p.role IS NULL
|
||||||
|
), remove AS (
|
||||||
|
-- find all role permissions that exist `role_permissions` and not in `target`
|
||||||
|
SELECT p.role, p.permission
|
||||||
|
FROM eventstore.role_permissions p
|
||||||
|
LEFT JOIN target t
|
||||||
|
ON p.role = t.role
|
||||||
|
AND p.permission = t.permission
|
||||||
|
WHERE p.aggregate_id = $1::text
|
||||||
|
AND t.role IS NULL
|
||||||
|
)
|
||||||
|
-- return the required operations
|
||||||
|
SELECT
|
||||||
|
'add' AS operation,
|
||||||
|
role,
|
||||||
|
permission
|
||||||
|
FROM add
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
'remove' AS operation,
|
||||||
|
role,
|
||||||
|
permission
|
||||||
|
FROM remove
|
||||||
|
;
|
@@ -127,5 +127,8 @@ func MustNewConfig(v *viper.Viper) *Config {
|
|||||||
id.Configure(config.Machine)
|
id.Configure(config.Machine)
|
||||||
actions.SetHTTPConfig(&config.Actions.HTTP)
|
actions.SetHTTPConfig(&config.Actions.HTTP)
|
||||||
|
|
||||||
|
// Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API.
|
||||||
|
config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command
|
|||||||
DisableUserTokenEvent: req.DisableUserTokenEvent,
|
DisableUserTokenEvent: req.DisableUserTokenEvent,
|
||||||
EnableBackChannelLogout: req.EnableBackChannelLogout,
|
EnableBackChannelLogout: req.EnableBackChannelLogout,
|
||||||
LoginV2: loginV2,
|
LoginV2: loginV2,
|
||||||
|
PermissionCheckV2: req.PermissionCheckV2,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
|
|||||||
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
|
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
|
||||||
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
|
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
|
||||||
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
|
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
|
||||||
|
PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com
|
|||||||
DisableUserTokenEvent: req.DisableUserTokenEvent,
|
DisableUserTokenEvent: req.DisableUserTokenEvent,
|
||||||
EnableBackChannelLogout: req.EnableBackChannelLogout,
|
EnableBackChannelLogout: req.EnableBackChannelLogout,
|
||||||
LoginV2: loginV2,
|
LoginV2: loginV2,
|
||||||
|
PermissionCheckV2: req.PermissionCheckV2,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +90,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
|
|||||||
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
|
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
|
||||||
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
|
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
|
||||||
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
|
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
|
||||||
|
PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -101,6 +101,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
|
|||||||
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
|
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
PermissionCheckV2: query.FeatureSource[bool]{
|
||||||
|
Level: feature.LevelSystem,
|
||||||
|
Value: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
want := &feature_pb.GetSystemFeaturesResponse{
|
want := &feature_pb.GetSystemFeaturesResponse{
|
||||||
Details: &object.Details{
|
Details: &object.Details{
|
||||||
@@ -153,6 +157,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
|
|||||||
BaseUri: gu.Ptr("https://login.com"),
|
BaseUri: gu.Ptr("https://login.com"),
|
||||||
Source: feature_pb.Source_SOURCE_SYSTEM,
|
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||||
},
|
},
|
||||||
|
PermissionCheckV2: &feature_pb.FeatureFlag{
|
||||||
|
Enabled: true,
|
||||||
|
Source: feature_pb.Source_SOURCE_SYSTEM,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
got := systemFeaturesToPb(arg)
|
got := systemFeaturesToPb(arg)
|
||||||
assert.Equal(t, want, got)
|
assert.Equal(t, want, got)
|
||||||
@@ -252,6 +260,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
|||||||
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
|
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
PermissionCheckV2: query.FeatureSource[bool]{
|
||||||
|
Level: feature.LevelInstance,
|
||||||
|
Value: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
want := &feature_pb.GetInstanceFeaturesResponse{
|
want := &feature_pb.GetInstanceFeaturesResponse{
|
||||||
Details: &object.Details{
|
Details: &object.Details{
|
||||||
@@ -312,6 +324,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
|
|||||||
BaseUri: gu.Ptr("https://login.com"),
|
BaseUri: gu.Ptr("https://login.com"),
|
||||||
Source: feature_pb.Source_SOURCE_INSTANCE,
|
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||||
},
|
},
|
||||||
|
PermissionCheckV2: &feature_pb.FeatureFlag{
|
||||||
|
Enabled: true,
|
||||||
|
Source: feature_pb.Source_SOURCE_INSTANCE,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
got := instanceFeaturesToPb(arg)
|
got := instanceFeaturesToPb(arg)
|
||||||
assert.Equal(t, want, got)
|
assert.Equal(t, want, got)
|
||||||
|
@@ -116,14 +116,15 @@ type InstanceSetup struct {
|
|||||||
MaxOTPAttempts uint64
|
MaxOTPAttempts uint64
|
||||||
ShouldShowLockoutFailure bool
|
ShouldShowLockoutFailure bool
|
||||||
}
|
}
|
||||||
EmailTemplate []byte
|
EmailTemplate []byte
|
||||||
MessageTexts []*domain.CustomMessageText
|
MessageTexts []*domain.CustomMessageText
|
||||||
SMTPConfiguration *SMTPConfiguration
|
SMTPConfiguration *SMTPConfiguration
|
||||||
OIDCSettings *OIDCSettings
|
OIDCSettings *OIDCSettings
|
||||||
Quotas *SetQuotas
|
Quotas *SetQuotas
|
||||||
Features *InstanceFeatures
|
Features *InstanceFeatures
|
||||||
Limits *SetLimits
|
Limits *SetLimits
|
||||||
Restrictions *SetRestrictions
|
Restrictions *SetRestrictions
|
||||||
|
RolePermissionMappings []authz.RoleMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
type SMTPConfiguration struct {
|
type SMTPConfiguration struct {
|
||||||
@@ -379,6 +380,7 @@ func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup
|
|||||||
setup.LabelPolicy.ThemeMode,
|
setup.LabelPolicy.ThemeMode,
|
||||||
),
|
),
|
||||||
prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate),
|
prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate),
|
||||||
|
prepareAddRolePermissions(instanceAgg, setup.RolePermissionMappings),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,6 +29,7 @@ type InstanceFeatures struct {
|
|||||||
DisableUserTokenEvent *bool
|
DisableUserTokenEvent *bool
|
||||||
EnableBackChannelLogout *bool
|
EnableBackChannelLogout *bool
|
||||||
LoginV2 *feature.LoginV2
|
LoginV2 *feature.LoginV2
|
||||||
|
PermissionCheckV2 *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *InstanceFeatures) isEmpty() bool {
|
func (m *InstanceFeatures) isEmpty() bool {
|
||||||
@@ -45,7 +46,8 @@ func (m *InstanceFeatures) isEmpty() bool {
|
|||||||
m.OIDCSingleV1SessionTermination == nil &&
|
m.OIDCSingleV1SessionTermination == nil &&
|
||||||
m.DisableUserTokenEvent == nil &&
|
m.DisableUserTokenEvent == nil &&
|
||||||
m.EnableBackChannelLogout == nil &&
|
m.EnableBackChannelLogout == nil &&
|
||||||
m.LoginV2 == nil
|
m.LoginV2 == nil &&
|
||||||
|
m.PermissionCheckV2 == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
|
||||||
|
@@ -79,6 +79,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
feature_v2.InstanceDisableUserTokenEvent,
|
feature_v2.InstanceDisableUserTokenEvent,
|
||||||
feature_v2.InstanceEnableBackChannelLogout,
|
feature_v2.InstanceEnableBackChannelLogout,
|
||||||
feature_v2.InstanceLoginVersion,
|
feature_v2.InstanceLoginVersion,
|
||||||
|
feature_v2.InstancePermissionCheckV2,
|
||||||
).
|
).
|
||||||
Builder().ResourceOwner(m.ResourceOwner)
|
Builder().ResourceOwner(m.ResourceOwner)
|
||||||
}
|
}
|
||||||
@@ -129,6 +130,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
|
|||||||
features.EnableBackChannelLogout = &v
|
features.EnableBackChannelLogout = &v
|
||||||
case feature.KeyLoginV2:
|
case feature.KeyLoginV2:
|
||||||
features.LoginV2 = value.(*feature.LoginV2)
|
features.LoginV2 = value.(*feature.LoginV2)
|
||||||
|
case feature.KeyPermissionCheckV2:
|
||||||
|
v := value.(bool)
|
||||||
|
features.PermissionCheckV2 = &v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,5 +152,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
|
|||||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.InstanceDisableUserTokenEvent)
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.InstanceDisableUserTokenEvent)
|
||||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.InstanceEnableBackChannelLogout)
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.InstanceEnableBackChannelLogout)
|
||||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.InstanceLoginVersion)
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.InstanceLoginVersion)
|
||||||
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.PermissionCheckV2, f.PermissionCheckV2, feature_v2.InstancePermissionCheckV2)
|
||||||
return cmds
|
return cmds
|
||||||
}
|
}
|
||||||
|
29
internal/command/instance_permissions.go
Normal file
29
internal/command/instance_permissions.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
func prepareAddRolePermissions(a *instance.Aggregate, roles []authz.RoleMapping) preparation.Validation {
|
||||||
|
return func() (preparation.CreateCommands, error) {
|
||||||
|
return func(ctx context.Context, _ preparation.FilterToQueryReducer) (cmds []eventstore.Command, _ error) {
|
||||||
|
aggregate := permission.NewAggregate(a.InstanceID)
|
||||||
|
for _, r := range roles {
|
||||||
|
if strings.HasPrefix(r.Role, "SYSTEM") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, p := range r.Permissions {
|
||||||
|
cmds = append(cmds, permission.NewAddedEvent(ctx, aggregate, r.Role, p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cmds, nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
@@ -21,6 +21,7 @@ type SystemFeatures struct {
|
|||||||
DisableUserTokenEvent *bool
|
DisableUserTokenEvent *bool
|
||||||
EnableBackChannelLogout *bool
|
EnableBackChannelLogout *bool
|
||||||
LoginV2 *feature.LoginV2
|
LoginV2 *feature.LoginV2
|
||||||
|
PermissionCheckV2 *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SystemFeatures) isEmpty() bool {
|
func (m *SystemFeatures) isEmpty() bool {
|
||||||
@@ -35,7 +36,8 @@ func (m *SystemFeatures) isEmpty() bool {
|
|||||||
m.OIDCSingleV1SessionTermination == nil &&
|
m.OIDCSingleV1SessionTermination == nil &&
|
||||||
m.DisableUserTokenEvent == nil &&
|
m.DisableUserTokenEvent == nil &&
|
||||||
m.EnableBackChannelLogout == nil &&
|
m.EnableBackChannelLogout == nil &&
|
||||||
m.LoginV2 == nil
|
m.LoginV2 == nil &&
|
||||||
|
m.PermissionCheckV2 == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {
|
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {
|
||||||
|
@@ -70,6 +70,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
feature_v2.SystemDisableUserTokenEvent,
|
feature_v2.SystemDisableUserTokenEvent,
|
||||||
feature_v2.SystemEnableBackChannelLogout,
|
feature_v2.SystemEnableBackChannelLogout,
|
||||||
feature_v2.SystemLoginVersion,
|
feature_v2.SystemLoginVersion,
|
||||||
|
feature_v2.SystemPermissionCheckV2,
|
||||||
).
|
).
|
||||||
Builder().ResourceOwner(m.ResourceOwner)
|
Builder().ResourceOwner(m.ResourceOwner)
|
||||||
}
|
}
|
||||||
@@ -113,6 +114,9 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) {
|
|||||||
features.EnableBackChannelLogout = &v
|
features.EnableBackChannelLogout = &v
|
||||||
case feature.KeyLoginV2:
|
case feature.KeyLoginV2:
|
||||||
features.LoginV2 = value.(*feature.LoginV2)
|
features.LoginV2 = value.(*feature.LoginV2)
|
||||||
|
case feature.KeyPermissionCheckV2:
|
||||||
|
v := value.(bool)
|
||||||
|
features.PermissionCheckV2 = &v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +134,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
|
|||||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.SystemDisableUserTokenEvent)
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.SystemDisableUserTokenEvent)
|
||||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.SystemEnableBackChannelLogout)
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.SystemEnableBackChannelLogout)
|
||||||
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.SystemLoginVersion)
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginV2, f.LoginV2, feature_v2.SystemLoginVersion)
|
||||||
|
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.PermissionCheckV2, f.PermissionCheckV2, feature_v2.SystemPermissionCheckV2)
|
||||||
return cmds
|
return cmds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,6 +23,7 @@ const (
|
|||||||
KeyDisableUserTokenEvent
|
KeyDisableUserTokenEvent
|
||||||
KeyEnableBackChannelLogout
|
KeyEnableBackChannelLogout
|
||||||
KeyLoginV2
|
KeyLoginV2
|
||||||
|
KeyPermissionCheckV2
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate enumer -type Level -transform snake -trimprefix Level
|
//go:generate enumer -type Level -transform snake -trimprefix Level
|
||||||
@@ -52,6 +53,7 @@ type Features struct {
|
|||||||
DisableUserTokenEvent bool `json:"disable_user_token_event,omitempty"`
|
DisableUserTokenEvent bool `json:"disable_user_token_event,omitempty"`
|
||||||
EnableBackChannelLogout bool `json:"enable_back_channel_logout,omitempty"`
|
EnableBackChannelLogout bool `json:"enable_back_channel_logout,omitempty"`
|
||||||
LoginV2 LoginV2 `json:"login_v2,omitempty"`
|
LoginV2 LoginV2 `json:"login_v2,omitempty"`
|
||||||
|
PermissionCheckV2 bool `json:"permission_check_v2,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImprovedPerformanceType int32
|
type ImprovedPerformanceType int32
|
||||||
|
@@ -7,11 +7,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2"
|
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2"
|
||||||
|
|
||||||
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255}
|
var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255, 274}
|
||||||
|
|
||||||
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2"
|
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2"
|
||||||
|
|
||||||
func (i Key) String() string {
|
func (i Key) String() string {
|
||||||
if i < 0 || i >= Key(len(_KeyIndex)-1) {
|
if i < 0 || i >= Key(len(_KeyIndex)-1) {
|
||||||
@@ -38,9 +38,10 @@ func _KeyNoOp() {
|
|||||||
_ = x[KeyDisableUserTokenEvent-(11)]
|
_ = x[KeyDisableUserTokenEvent-(11)]
|
||||||
_ = x[KeyEnableBackChannelLogout-(12)]
|
_ = x[KeyEnableBackChannelLogout-(12)]
|
||||||
_ = x[KeyLoginV2-(13)]
|
_ = x[KeyLoginV2-(13)]
|
||||||
|
_ = x[KeyPermissionCheckV2-(14)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2}
|
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2}
|
||||||
|
|
||||||
var _KeyNameToValueMap = map[string]Key{
|
var _KeyNameToValueMap = map[string]Key{
|
||||||
_KeyName[0:11]: KeyUnspecified,
|
_KeyName[0:11]: KeyUnspecified,
|
||||||
@@ -71,6 +72,8 @@ var _KeyNameToValueMap = map[string]Key{
|
|||||||
_KeyLowerName[221:247]: KeyEnableBackChannelLogout,
|
_KeyLowerName[221:247]: KeyEnableBackChannelLogout,
|
||||||
_KeyName[247:255]: KeyLoginV2,
|
_KeyName[247:255]: KeyLoginV2,
|
||||||
_KeyLowerName[247:255]: KeyLoginV2,
|
_KeyLowerName[247:255]: KeyLoginV2,
|
||||||
|
_KeyName[255:274]: KeyPermissionCheckV2,
|
||||||
|
_KeyLowerName[255:274]: KeyPermissionCheckV2,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _KeyNames = []string{
|
var _KeyNames = []string{
|
||||||
@@ -88,6 +91,7 @@ var _KeyNames = []string{
|
|||||||
_KeyName[197:221],
|
_KeyName[197:221],
|
||||||
_KeyName[221:247],
|
_KeyName[221:247],
|
||||||
_KeyName[247:255],
|
_KeyName[247:255],
|
||||||
|
_KeyName[255:274],
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyString retrieves an enum value from the enum constants string name.
|
// KeyString retrieves an enum value from the enum constants string name.
|
||||||
|
@@ -22,6 +22,7 @@ type InstanceFeatures struct {
|
|||||||
DisableUserTokenEvent FeatureSource[bool]
|
DisableUserTokenEvent FeatureSource[bool]
|
||||||
EnableBackChannelLogout FeatureSource[bool]
|
EnableBackChannelLogout FeatureSource[bool]
|
||||||
LoginV2 FeatureSource[*feature.LoginV2]
|
LoginV2 FeatureSource[*feature.LoginV2]
|
||||||
|
PermissionCheckV2 FeatureSource[bool]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {
|
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {
|
||||||
|
@@ -75,6 +75,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
feature_v2.InstanceDisableUserTokenEvent,
|
feature_v2.InstanceDisableUserTokenEvent,
|
||||||
feature_v2.InstanceEnableBackChannelLogout,
|
feature_v2.InstanceEnableBackChannelLogout,
|
||||||
feature_v2.InstanceLoginVersion,
|
feature_v2.InstanceLoginVersion,
|
||||||
|
feature_v2.InstancePermissionCheckV2,
|
||||||
).
|
).
|
||||||
Builder().ResourceOwner(m.ResourceOwner)
|
Builder().ResourceOwner(m.ResourceOwner)
|
||||||
}
|
}
|
||||||
@@ -139,6 +140,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
|
|||||||
features.EnableBackChannelLogout.set(level, event.Value)
|
features.EnableBackChannelLogout.set(level, event.Value)
|
||||||
case feature.KeyLoginV2:
|
case feature.KeyLoginV2:
|
||||||
features.LoginV2.set(level, event.Value)
|
features.LoginV2.set(level, event.Value)
|
||||||
|
case feature.KeyPermissionCheckV2:
|
||||||
|
features.PermissionCheckV2.set(level, event.Value)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
35
internal/query/permission.go
Normal file
35
internal/query/permission.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// eventstore.permitted_orgs(instanceid text, userid text, perm text)
|
||||||
|
wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?))"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs
|
||||||
|
// for which the authenticated user has the requested permission for.
|
||||||
|
// The user ID is taken from the context.
|
||||||
|
//
|
||||||
|
// The `orgIDColumn` specifies the table column to which this filter must be applied,
|
||||||
|
// and is typically the `resource_owner` column in ZITADEL.
|
||||||
|
// We use full identifiers in the query builder so this function should be
|
||||||
|
// called with something like `UserResourceOwnerCol.identifier()` for example.
|
||||||
|
func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, 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")
|
||||||
|
return query.Where(
|
||||||
|
fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn),
|
||||||
|
authz.GetInstance(ctx).InstanceID(),
|
||||||
|
userID,
|
||||||
|
permission,
|
||||||
|
)
|
||||||
|
}
|
@@ -112,6 +112,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
|
|||||||
Event: feature_v2.InstanceLoginVersion,
|
Event: feature_v2.InstanceLoginVersion,
|
||||||
Reduce: reduceInstanceSetFeature[*feature.LoginV2],
|
Reduce: reduceInstanceSetFeature[*feature.LoginV2],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Event: feature_v2.InstancePermissionCheckV2,
|
||||||
|
Reduce: reduceInstanceSetFeature[bool],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Event: instance.InstanceRemovedEventType,
|
Event: instance.InstanceRemovedEventType,
|
||||||
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),
|
||||||
|
@@ -92,6 +92,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
|
|||||||
Event: feature_v2.SystemLoginVersion,
|
Event: feature_v2.SystemLoginVersion,
|
||||||
Reduce: reduceSystemSetFeature[*feature.LoginV2],
|
Reduce: reduceSystemSetFeature[*feature.LoginV2],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Event: feature_v2.SystemPermissionCheckV2,
|
||||||
|
Reduce: reduceSystemSetFeature[bool],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
@@ -31,6 +31,7 @@ type SystemFeatures struct {
|
|||||||
DisableUserTokenEvent FeatureSource[bool]
|
DisableUserTokenEvent FeatureSource[bool]
|
||||||
EnableBackChannelLogout FeatureSource[bool]
|
EnableBackChannelLogout FeatureSource[bool]
|
||||||
LoginV2 FeatureSource[*feature.LoginV2]
|
LoginV2 FeatureSource[*feature.LoginV2]
|
||||||
|
PermissionCheckV2 FeatureSource[bool]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {
|
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {
|
||||||
|
@@ -66,6 +66,7 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
feature_v2.SystemDisableUserTokenEvent,
|
feature_v2.SystemDisableUserTokenEvent,
|
||||||
feature_v2.SystemEnableBackChannelLogout,
|
feature_v2.SystemEnableBackChannelLogout,
|
||||||
feature_v2.SystemLoginVersion,
|
feature_v2.SystemLoginVersion,
|
||||||
|
feature_v2.SystemPermissionCheckV2,
|
||||||
).
|
).
|
||||||
Builder().ResourceOwner(m.ResourceOwner)
|
Builder().ResourceOwner(m.ResourceOwner)
|
||||||
}
|
}
|
||||||
@@ -105,6 +106,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S
|
|||||||
features.EnableBackChannelLogout.set(level, event.Value)
|
features.EnableBackChannelLogout.set(level, event.Value)
|
||||||
case feature.KeyLoginV2:
|
case feature.KeyLoginV2:
|
||||||
features.LoginV2.set(level, event.Value)
|
features.LoginV2.set(level, event.Value)
|
||||||
|
case feature.KeyPermissionCheckV2:
|
||||||
|
features.PermissionCheckV2.set(level, event.Value)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -605,24 +605,29 @@ func (q *Queries) GetNotifyUser(ctx context.Context, shouldTriggered bool, queri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheck domain.PermissionCheck) (*Users, error) {
|
func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheck domain.PermissionCheck) (*Users, error) {
|
||||||
users, err := q.searchUsers(ctx, queries)
|
users, err := q.searchUsers(ctx, queries, permissionCheck != nil && authz.GetFeatures(ctx).PermissionCheckV2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if permissionCheck != nil {
|
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
|
||||||
usersCheckPermission(ctx, users, permissionCheck)
|
usersCheckPermission(ctx, users, permissionCheck)
|
||||||
}
|
}
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries) (users *Users, err error) {
|
func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheckV2 bool) (users *Users, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
query, scan := prepareUsersQuery(ctx, q.client)
|
query, scan := prepareUsersQuery(ctx, q.client)
|
||||||
eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
|
query = queries.toQuery(query).Where(sq.Eq{
|
||||||
stmt, args, err := queries.toQuery(query).Where(eq).
|
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||||
ToSql()
|
})
|
||||||
|
if permissionCheckV2 {
|
||||||
|
query = wherePermittedOrgs(ctx, query, UserResourceOwnerCol.identifier(), domain.PermissionUserRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, args, err := query.ToSql()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment")
|
return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment")
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ func init() {
|
|||||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]])
|
eventstore.RegisterFilterEventMapper(AggregateType, SystemDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]])
|
eventstore.RegisterFilterEventMapper(AggregateType, SystemEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, SystemLoginVersion, eventstore.GenericEventMapper[SetEvent[*feature.LoginV2]])
|
eventstore.RegisterFilterEventMapper(AggregateType, SystemLoginVersion, eventstore.GenericEventMapper[SetEvent[*feature.LoginV2]])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, SystemPermissionCheckV2, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||||
|
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent])
|
eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent])
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||||
@@ -33,4 +34,5 @@ func init() {
|
|||||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]])
|
eventstore.RegisterFilterEventMapper(AggregateType, InstanceDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]])
|
eventstore.RegisterFilterEventMapper(AggregateType, InstanceEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||||
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginVersion, eventstore.GenericEventMapper[SetEvent[*feature.LoginV2]])
|
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginVersion, eventstore.GenericEventMapper[SetEvent[*feature.LoginV2]])
|
||||||
|
eventstore.RegisterFilterEventMapper(AggregateType, InstancePermissionCheckV2, eventstore.GenericEventMapper[SetEvent[bool]])
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,7 @@ var (
|
|||||||
SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent)
|
SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent)
|
||||||
SystemEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelSystem, feature.KeyEnableBackChannelLogout)
|
SystemEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelSystem, feature.KeyEnableBackChannelLogout)
|
||||||
SystemLoginVersion = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginV2)
|
SystemLoginVersion = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginV2)
|
||||||
|
SystemPermissionCheckV2 = setEventTypeFromFeature(feature.LevelSystem, feature.KeyPermissionCheckV2)
|
||||||
|
|
||||||
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
|
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
|
||||||
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
|
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
|
||||||
@@ -38,6 +39,7 @@ var (
|
|||||||
InstanceDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDisableUserTokenEvent)
|
InstanceDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDisableUserTokenEvent)
|
||||||
InstanceEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelInstance, feature.KeyEnableBackChannelLogout)
|
InstanceEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelInstance, feature.KeyEnableBackChannelLogout)
|
||||||
InstanceLoginVersion = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginV2)
|
InstanceLoginVersion = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginV2)
|
||||||
|
InstancePermissionCheckV2 = setEventTypeFromFeature(feature.LevelInstance, feature.KeyPermissionCheckV2)
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@@ -7,17 +7,25 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/repository/member"
|
"github.com/zitadel/zitadel/internal/repository/member"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
MemberAddedEventType = instanceEventTypePrefix + member.AddedEventType
|
MemberAddedEventType = instanceEventTypePrefix + member.AddedEventType
|
||||||
MemberChangedEventType = instanceEventTypePrefix + member.ChangedEventType
|
MemberChangedEventType = instanceEventTypePrefix + member.ChangedEventType
|
||||||
MemberRemovedEventType = instanceEventTypePrefix + member.RemovedEventType
|
MemberRemovedEventType = instanceEventTypePrefix + member.RemovedEventType
|
||||||
MemberCascadeRemovedEventType = instanceEventTypePrefix + member.CascadeRemovedEventType
|
MemberCascadeRemovedEventType = instanceEventTypePrefix + member.CascadeRemovedEventType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fieldPrefix = "instance"
|
||||||
|
)
|
||||||
|
|
||||||
type MemberAddedEvent struct {
|
type MemberAddedEvent struct {
|
||||||
member.MemberAddedEvent
|
member.MemberAddedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberAddedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberAddedEvent(
|
func NewMemberAddedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -51,6 +59,10 @@ type MemberChangedEvent struct {
|
|||||||
member.MemberChangedEvent
|
member.MemberChangedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberChangedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberChangedEvent(
|
func NewMemberChangedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -83,6 +95,10 @@ type MemberRemovedEvent struct {
|
|||||||
member.MemberRemovedEvent
|
member.MemberRemovedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberRemovedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberRemovedEvent(
|
func NewMemberRemovedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -113,6 +129,10 @@ type MemberCascadeRemovedEvent struct {
|
|||||||
member.MemberCascadeRemovedEvent
|
member.MemberCascadeRemovedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberCascadeRemovedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberCascadeRemovedEvent(
|
func NewMemberCascadeRemovedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/zerrors"
|
"github.com/zitadel/zitadel/internal/zerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Event types
|
||||||
const (
|
const (
|
||||||
UniqueMember = "member"
|
UniqueMember = "member"
|
||||||
AddedEventType = "member.added"
|
AddedEventType = "member.added"
|
||||||
@@ -15,6 +16,13 @@ const (
|
|||||||
CascadeRemovedEventType = "member.cascade.removed"
|
CascadeRemovedEventType = "member.cascade.removed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Field table and unique types
|
||||||
|
const (
|
||||||
|
memberRoleTypeSuffix string = "_member_role"
|
||||||
|
MemberRoleRevision uint8 = 1
|
||||||
|
roleSearchFieldSuffix string = "_role"
|
||||||
|
)
|
||||||
|
|
||||||
func NewAddMemberUniqueConstraint(aggregateID, userID string) *eventstore.UniqueConstraint {
|
func NewAddMemberUniqueConstraint(aggregateID, userID string) *eventstore.UniqueConstraint {
|
||||||
return eventstore.NewAddEventUniqueConstraint(
|
return eventstore.NewAddEventUniqueConstraint(
|
||||||
UniqueMember,
|
UniqueMember,
|
||||||
@@ -44,6 +52,32 @@ func (e *MemberAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
|||||||
return []*eventstore.UniqueConstraint{NewAddMemberUniqueConstraint(e.Aggregate().ID, e.UserID)}
|
return []*eventstore.UniqueConstraint{NewAddMemberUniqueConstraint(e.Aggregate().ID, e.UserID)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberAddedEvent) FieldOperations(prefix string) []*eventstore.FieldOperation {
|
||||||
|
ops := make([]*eventstore.FieldOperation, len(e.Roles))
|
||||||
|
for i, role := range e.Roles {
|
||||||
|
ops[i] = eventstore.SetField(
|
||||||
|
e.Aggregate(),
|
||||||
|
memberSearchObject(prefix, e.UserID),
|
||||||
|
prefix+roleSearchFieldSuffix,
|
||||||
|
&eventstore.Value{
|
||||||
|
Value: role,
|
||||||
|
MustBeUnique: false,
|
||||||
|
ShouldIndex: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
eventstore.FieldTypeInstanceID,
|
||||||
|
eventstore.FieldTypeResourceOwner,
|
||||||
|
eventstore.FieldTypeAggregateType,
|
||||||
|
eventstore.FieldTypeAggregateID,
|
||||||
|
eventstore.FieldTypeObjectType,
|
||||||
|
eventstore.FieldTypeObjectID,
|
||||||
|
eventstore.FieldTypeFieldName,
|
||||||
|
eventstore.FieldTypeValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberAddedEvent(
|
func NewMemberAddedEvent(
|
||||||
base *eventstore.BaseEvent,
|
base *eventstore.BaseEvent,
|
||||||
userID string,
|
userID string,
|
||||||
@@ -85,6 +119,38 @@ func (e *MemberChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FieldOperations removes the existing membership role fields first and sets the new roles after.
|
||||||
|
func (e *MemberChangedEvent) FieldOperations(prefix string) []*eventstore.FieldOperation {
|
||||||
|
ops := make([]*eventstore.FieldOperation, len(e.Roles)+1)
|
||||||
|
ops[0] = eventstore.RemoveSearchFieldsByAggregateAndObject(
|
||||||
|
e.Aggregate(),
|
||||||
|
memberSearchObject(prefix, e.UserID),
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, role := range e.Roles {
|
||||||
|
ops[i+1] = eventstore.SetField(
|
||||||
|
e.Aggregate(),
|
||||||
|
memberSearchObject(prefix, e.UserID),
|
||||||
|
prefix+roleSearchFieldSuffix,
|
||||||
|
&eventstore.Value{
|
||||||
|
Value: role,
|
||||||
|
MustBeUnique: false,
|
||||||
|
ShouldIndex: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
eventstore.FieldTypeInstanceID,
|
||||||
|
eventstore.FieldTypeResourceOwner,
|
||||||
|
eventstore.FieldTypeAggregateType,
|
||||||
|
eventstore.FieldTypeAggregateID,
|
||||||
|
eventstore.FieldTypeObjectType,
|
||||||
|
eventstore.FieldTypeObjectID,
|
||||||
|
eventstore.FieldTypeFieldName,
|
||||||
|
eventstore.FieldTypeValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberChangedEvent(
|
func NewMemberChangedEvent(
|
||||||
base *eventstore.BaseEvent,
|
base *eventstore.BaseEvent,
|
||||||
userID string,
|
userID string,
|
||||||
@@ -124,6 +190,15 @@ func (e *MemberRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint
|
|||||||
return []*eventstore.UniqueConstraint{NewRemoveMemberUniqueConstraint(e.Aggregate().ID, e.UserID)}
|
return []*eventstore.UniqueConstraint{NewRemoveMemberUniqueConstraint(e.Aggregate().ID, e.UserID)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberRemovedEvent) FieldOperations(prefix string) []*eventstore.FieldOperation {
|
||||||
|
return []*eventstore.FieldOperation{
|
||||||
|
eventstore.RemoveSearchFieldsByAggregateAndObject(
|
||||||
|
e.Aggregate(),
|
||||||
|
memberSearchObject(prefix, e.UserID),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewRemovedEvent(
|
func NewRemovedEvent(
|
||||||
base *eventstore.BaseEvent,
|
base *eventstore.BaseEvent,
|
||||||
userID string,
|
userID string,
|
||||||
@@ -162,6 +237,15 @@ func (e *MemberCascadeRemovedEvent) UniqueConstraints() []*eventstore.UniqueCons
|
|||||||
return []*eventstore.UniqueConstraint{NewRemoveMemberUniqueConstraint(e.Aggregate().ID, e.UserID)}
|
return []*eventstore.UniqueConstraint{NewRemoveMemberUniqueConstraint(e.Aggregate().ID, e.UserID)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberCascadeRemovedEvent) FieldOperations(prefix string) []*eventstore.FieldOperation {
|
||||||
|
return []*eventstore.FieldOperation{
|
||||||
|
eventstore.RemoveSearchFieldsByAggregateAndObject(
|
||||||
|
e.Aggregate(),
|
||||||
|
memberSearchObject(prefix, e.UserID),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewCascadeRemovedEvent(
|
func NewCascadeRemovedEvent(
|
||||||
base *eventstore.BaseEvent,
|
base *eventstore.BaseEvent,
|
||||||
userID string,
|
userID string,
|
||||||
@@ -185,3 +269,11 @@ func CascadeRemovedEventMapper(event eventstore.Event) (eventstore.Event, error)
|
|||||||
|
|
||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func memberSearchObject(prefix, userID string) eventstore.Object {
|
||||||
|
return eventstore.Object{
|
||||||
|
Type: prefix + memberRoleTypeSuffix,
|
||||||
|
ID: userID,
|
||||||
|
Revision: MemberRoleRevision,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -7,17 +7,25 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/repository/member"
|
"github.com/zitadel/zitadel/internal/repository/member"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
const (
|
||||||
MemberAddedEventType = orgEventTypePrefix + member.AddedEventType
|
MemberAddedEventType = orgEventTypePrefix + member.AddedEventType
|
||||||
MemberChangedEventType = orgEventTypePrefix + member.ChangedEventType
|
MemberChangedEventType = orgEventTypePrefix + member.ChangedEventType
|
||||||
MemberRemovedEventType = orgEventTypePrefix + member.RemovedEventType
|
MemberRemovedEventType = orgEventTypePrefix + member.RemovedEventType
|
||||||
MemberCascadeRemovedEventType = orgEventTypePrefix + member.CascadeRemovedEventType
|
MemberCascadeRemovedEventType = orgEventTypePrefix + member.CascadeRemovedEventType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fieldPrefix = "org"
|
||||||
|
)
|
||||||
|
|
||||||
type MemberAddedEvent struct {
|
type MemberAddedEvent struct {
|
||||||
member.MemberAddedEvent
|
member.MemberAddedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberAddedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberAddedEvent(
|
func NewMemberAddedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -50,6 +58,10 @@ type MemberChangedEvent struct {
|
|||||||
member.MemberChangedEvent
|
member.MemberChangedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberChangedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberChangedEvent(
|
func NewMemberChangedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -83,6 +95,10 @@ type MemberRemovedEvent struct {
|
|||||||
member.MemberRemovedEvent
|
member.MemberRemovedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberRemovedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberRemovedEvent(
|
func NewMemberRemovedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -113,6 +129,10 @@ type MemberCascadeRemovedEvent struct {
|
|||||||
member.MemberCascadeRemovedEvent
|
member.MemberCascadeRemovedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberCascadeRemovedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewMemberCascadeRemovedEvent(
|
func NewMemberCascadeRemovedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
|
22
internal/repository/permission/aggregate.go
Normal file
22
internal/repository/permission/aggregate.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package permission
|
||||||
|
|
||||||
|
import "github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
|
||||||
|
const (
|
||||||
|
AggregateType eventstore.AggregateType = "permission"
|
||||||
|
AggregateVersion eventstore.Version = "v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAggregate(aggregateID string) *eventstore.Aggregate {
|
||||||
|
var instanceID string
|
||||||
|
if aggregateID != "SYSTEM" {
|
||||||
|
instanceID = aggregateID
|
||||||
|
}
|
||||||
|
return &eventstore.Aggregate{
|
||||||
|
ID: aggregateID,
|
||||||
|
Type: AggregateType,
|
||||||
|
ResourceOwner: aggregateID,
|
||||||
|
InstanceID: instanceID,
|
||||||
|
Version: AggregateVersion,
|
||||||
|
}
|
||||||
|
}
|
114
internal/repository/permission/permission.go
Normal file
114
internal/repository/permission/permission.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package permission
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event types
|
||||||
|
const (
|
||||||
|
permissionEventPrefix eventstore.EventType = "permission."
|
||||||
|
AddedType = permissionEventPrefix + "added"
|
||||||
|
RemovedType = permissionEventPrefix + "removed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Field table and unique types
|
||||||
|
const (
|
||||||
|
RolePermissionType string = "role_permission"
|
||||||
|
RolePermissionRevision uint8 = 1
|
||||||
|
PermissionSearchField string = "permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddedEvent struct {
|
||||||
|
*eventstore.BaseEvent `json:"-"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Permission string `json:"permission"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AddedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return []*eventstore.FieldOperation{
|
||||||
|
eventstore.SetField(
|
||||||
|
e.Aggregate(),
|
||||||
|
roleSearchObject(e.Role),
|
||||||
|
PermissionSearchField,
|
||||||
|
&eventstore.Value{
|
||||||
|
Value: e.Permission,
|
||||||
|
MustBeUnique: false,
|
||||||
|
ShouldIndex: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
eventstore.FieldTypeInstanceID,
|
||||||
|
eventstore.FieldTypeResourceOwner,
|
||||||
|
eventstore.FieldTypeAggregateType,
|
||||||
|
eventstore.FieldTypeAggregateID,
|
||||||
|
eventstore.FieldTypeObjectType,
|
||||||
|
eventstore.FieldTypeObjectID,
|
||||||
|
eventstore.FieldTypeFieldName,
|
||||||
|
eventstore.FieldTypeValue,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddedEvent(ctx context.Context, aggregate *eventstore.Aggregate, role, permission string) *AddedEvent {
|
||||||
|
return &AddedEvent{
|
||||||
|
BaseEvent: eventstore.NewBaseEventForPush(ctx, aggregate, AddedType),
|
||||||
|
Role: role,
|
||||||
|
Permission: permission,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemovedEvent struct {
|
||||||
|
*eventstore.BaseEvent `json:"-"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Permission string `json:"permission"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RemovedEvent) Payload() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RemovedEvent) SetBaseEvent(event *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *RemovedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return []*eventstore.FieldOperation{
|
||||||
|
eventstore.RemoveSearchFieldsByAggregateAndObject(
|
||||||
|
e.Aggregate(),
|
||||||
|
roleSearchObject(e.Role),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemovedEvent(ctx context.Context, aggregate *eventstore.Aggregate, role, permission string) *RemovedEvent {
|
||||||
|
return &RemovedEvent{
|
||||||
|
BaseEvent: eventstore.NewBaseEventForPush(ctx, aggregate, AddedType),
|
||||||
|
Role: role,
|
||||||
|
Permission: permission,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func roleSearchObject(role string) eventstore.Object {
|
||||||
|
return eventstore.Object{
|
||||||
|
Type: RolePermissionType,
|
||||||
|
ID: role,
|
||||||
|
Revision: RolePermissionRevision,
|
||||||
|
}
|
||||||
|
}
|
@@ -14,10 +14,18 @@ var (
|
|||||||
MemberCascadeRemovedType = projectEventTypePrefix + member.CascadeRemovedEventType
|
MemberCascadeRemovedType = projectEventTypePrefix + member.CascadeRemovedEventType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fieldPrefix = "project"
|
||||||
|
)
|
||||||
|
|
||||||
type MemberAddedEvent struct {
|
type MemberAddedEvent struct {
|
||||||
member.MemberAddedEvent
|
member.MemberAddedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberAddedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewProjectMemberAddedEvent(
|
func NewProjectMemberAddedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -50,6 +58,10 @@ type MemberChangedEvent struct {
|
|||||||
member.MemberChangedEvent
|
member.MemberChangedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberChangedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewProjectMemberChangedEvent(
|
func NewProjectMemberChangedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -83,6 +95,10 @@ type MemberRemovedEvent struct {
|
|||||||
member.MemberRemovedEvent
|
member.MemberRemovedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberRemovedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewProjectMemberRemovedEvent(
|
func NewProjectMemberRemovedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
@@ -114,6 +130,10 @@ type MemberCascadeRemovedEvent struct {
|
|||||||
member.MemberCascadeRemovedEvent
|
member.MemberCascadeRemovedEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *MemberCascadeRemovedEvent) Fields() []*eventstore.FieldOperation {
|
||||||
|
return e.FieldOperations(fieldPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
func NewProjectMemberCascadeRemovedEvent(
|
func NewProjectMemberCascadeRemovedEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
aggregate *eventstore.Aggregate,
|
aggregate *eventstore.Aggregate,
|
||||||
|
@@ -99,6 +99,13 @@ message SetInstanceFeaturesRequest{
|
|||||||
description: "Specify the login UI for all users and applications regardless of their preference.";
|
description: "Specify the login UI for all users and applications regardless of their preference.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
optional bool permission_check_v2 = 14 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "true";
|
||||||
|
description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs.";
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetInstanceFeaturesResponse {
|
message SetInstanceFeaturesResponse {
|
||||||
@@ -212,4 +219,10 @@ message GetInstanceFeaturesResponse {
|
|||||||
description: "If the flag is set, all users will be redirected to the login V2 regardless of the application's preference.";
|
description: "If the flag is set, all users will be redirected to the login V2 regardless of the application's preference.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
FeatureFlag permission_check_v2 = 15 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs.";
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@@ -88,6 +88,13 @@ message SetSystemFeaturesRequest{
|
|||||||
description: "Specify the login UI for all users and applications regardless of their preference.";
|
description: "Specify the login UI for all users and applications regardless of their preference.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
optional bool permission_check_v2 = 12 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "true";
|
||||||
|
description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs.";
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetSystemFeaturesResponse {
|
message SetSystemFeaturesResponse {
|
||||||
@@ -180,4 +187,10 @@ message GetSystemFeaturesResponse {
|
|||||||
description: "If the flag is set, all users will be redirected to the login V2 regardless of the application's preference.";
|
description: "If the flag is set, all users will be redirected to the login V2 regardless of the application's preference.";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
FeatureFlag permission_check_v2 = 13 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "Enable a newer, more performant, permission check used for v2 and v3 resource based APIs.";
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user