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:
Tim Möhlmann
2025-01-16 11:09:15 +01:00
committed by GitHub
parent 690147b30e
commit 3f6ea78c87
41 changed files with 789 additions and 22 deletions

View File

@@ -29,6 +29,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command
DisableUserTokenEvent: req.DisableUserTokenEvent,
EnableBackChannelLogout: req.EnableBackChannelLogout,
LoginV2: loginV2,
PermissionCheckV2: req.PermissionCheckV2,
}, nil
}
@@ -46,6 +47,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2),
}
}
@@ -68,6 +70,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com
DisableUserTokenEvent: req.DisableUserTokenEvent,
EnableBackChannelLogout: req.EnableBackChannelLogout,
LoginV2: loginV2,
PermissionCheckV2: req.PermissionCheckV2,
}, nil
}
@@ -87,6 +90,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent),
EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout),
LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2),
PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2),
}
}

View File

@@ -101,6 +101,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
},
PermissionCheckV2: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: true,
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@@ -153,6 +157,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
BaseUri: gu.Ptr("https://login.com"),
Source: feature_pb.Source_SOURCE_SYSTEM,
},
PermissionCheckV2: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@@ -252,6 +260,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
BaseURI: &url.URL{Scheme: "https", Host: "login.com"},
},
},
PermissionCheckV2: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@@ -312,6 +324,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
BaseUri: gu.Ptr("https://login.com"),
Source: feature_pb.Source_SOURCE_INSTANCE,
},
PermissionCheckV2: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@@ -116,14 +116,15 @@ type InstanceSetup struct {
MaxOTPAttempts uint64
ShouldShowLockoutFailure bool
}
EmailTemplate []byte
MessageTexts []*domain.CustomMessageText
SMTPConfiguration *SMTPConfiguration
OIDCSettings *OIDCSettings
Quotas *SetQuotas
Features *InstanceFeatures
Limits *SetLimits
Restrictions *SetRestrictions
EmailTemplate []byte
MessageTexts []*domain.CustomMessageText
SMTPConfiguration *SMTPConfiguration
OIDCSettings *OIDCSettings
Quotas *SetQuotas
Features *InstanceFeatures
Limits *SetLimits
Restrictions *SetRestrictions
RolePermissionMappings []authz.RoleMapping
}
type SMTPConfiguration struct {
@@ -379,6 +380,7 @@ func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup
setup.LabelPolicy.ThemeMode,
),
prepareAddDefaultEmailTemplate(instanceAgg, setup.EmailTemplate),
prepareAddRolePermissions(instanceAgg, setup.RolePermissionMappings),
}
}

View File

@@ -29,6 +29,7 @@ type InstanceFeatures struct {
DisableUserTokenEvent *bool
EnableBackChannelLogout *bool
LoginV2 *feature.LoginV2
PermissionCheckV2 *bool
}
func (m *InstanceFeatures) isEmpty() bool {
@@ -45,7 +46,8 @@ func (m *InstanceFeatures) isEmpty() bool {
m.OIDCSingleV1SessionTermination == nil &&
m.DisableUserTokenEvent == 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) {

View File

@@ -79,6 +79,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceDisableUserTokenEvent,
feature_v2.InstanceEnableBackChannelLogout,
feature_v2.InstanceLoginVersion,
feature_v2.InstancePermissionCheckV2,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -129,6 +130,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
features.EnableBackChannelLogout = &v
case feature.KeyLoginV2:
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.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.InstanceEnableBackChannelLogout)
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
}

View 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
}
}

View File

@@ -21,6 +21,7 @@ type SystemFeatures struct {
DisableUserTokenEvent *bool
EnableBackChannelLogout *bool
LoginV2 *feature.LoginV2
PermissionCheckV2 *bool
}
func (m *SystemFeatures) isEmpty() bool {
@@ -35,7 +36,8 @@ func (m *SystemFeatures) isEmpty() bool {
m.OIDCSingleV1SessionTermination == nil &&
m.DisableUserTokenEvent == 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) {

View File

@@ -70,6 +70,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemDisableUserTokenEvent,
feature_v2.SystemEnableBackChannelLogout,
feature_v2.SystemLoginVersion,
feature_v2.SystemPermissionCheckV2,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -113,6 +114,9 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) {
features.EnableBackChannelLogout = &v
case feature.KeyLoginV2:
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.EnableBackChannelLogout, f.EnableBackChannelLogout, feature_v2.SystemEnableBackChannelLogout)
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
}

View File

@@ -23,6 +23,7 @@ const (
KeyDisableUserTokenEvent
KeyEnableBackChannelLogout
KeyLoginV2
KeyPermissionCheckV2
)
//go:generate enumer -type Level -transform snake -trimprefix Level
@@ -52,6 +53,7 @@ type Features struct {
DisableUserTokenEvent bool `json:"disable_user_token_event,omitempty"`
EnableBackChannelLogout bool `json:"enable_back_channel_logout,omitempty"`
LoginV2 LoginV2 `json:"login_v2,omitempty"`
PermissionCheckV2 bool `json:"permission_check_v2,omitempty"`
}
type ImprovedPerformanceType int32

View File

@@ -7,11 +7,11 @@ import (
"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 {
if i < 0 || i >= Key(len(_KeyIndex)-1) {
@@ -38,9 +38,10 @@ func _KeyNoOp() {
_ = x[KeyDisableUserTokenEvent-(11)]
_ = x[KeyEnableBackChannelLogout-(12)]
_ = 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{
_KeyName[0:11]: KeyUnspecified,
@@ -71,6 +72,8 @@ var _KeyNameToValueMap = map[string]Key{
_KeyLowerName[221:247]: KeyEnableBackChannelLogout,
_KeyName[247:255]: KeyLoginV2,
_KeyLowerName[247:255]: KeyLoginV2,
_KeyName[255:274]: KeyPermissionCheckV2,
_KeyLowerName[255:274]: KeyPermissionCheckV2,
}
var _KeyNames = []string{
@@ -88,6 +91,7 @@ var _KeyNames = []string{
_KeyName[197:221],
_KeyName[221:247],
_KeyName[247:255],
_KeyName[255:274],
}
// KeyString retrieves an enum value from the enum constants string name.

View File

@@ -22,6 +22,7 @@ type InstanceFeatures struct {
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
PermissionCheckV2 FeatureSource[bool]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {

View File

@@ -75,6 +75,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceDisableUserTokenEvent,
feature_v2.InstanceEnableBackChannelLogout,
feature_v2.InstanceLoginVersion,
feature_v2.InstancePermissionCheckV2,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -139,6 +140,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
features.EnableBackChannelLogout.set(level, event.Value)
case feature.KeyLoginV2:
features.LoginV2.set(level, event.Value)
case feature.KeyPermissionCheckV2:
features.PermissionCheckV2.set(level, event.Value)
}
return nil
}

View 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,
)
}

View File

@@ -112,6 +112,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceLoginVersion,
Reduce: reduceInstanceSetFeature[*feature.LoginV2],
},
{
Event: feature_v2.InstancePermissionCheckV2,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),

View File

@@ -92,6 +92,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.SystemLoginVersion,
Reduce: reduceSystemSetFeature[*feature.LoginV2],
},
{
Event: feature_v2.SystemPermissionCheckV2,
Reduce: reduceSystemSetFeature[bool],
},
},
}}
}

View File

@@ -31,6 +31,7 @@ type SystemFeatures struct {
DisableUserTokenEvent FeatureSource[bool]
EnableBackChannelLogout FeatureSource[bool]
LoginV2 FeatureSource[*feature.LoginV2]
PermissionCheckV2 FeatureSource[bool]
}
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {

View File

@@ -66,6 +66,7 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemDisableUserTokenEvent,
feature_v2.SystemEnableBackChannelLogout,
feature_v2.SystemLoginVersion,
feature_v2.SystemPermissionCheckV2,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@@ -105,6 +106,8 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S
features.EnableBackChannelLogout.set(level, event.Value)
case feature.KeyLoginV2:
features.LoginV2.set(level, event.Value)
case feature.KeyPermissionCheckV2:
features.PermissionCheckV2.set(level, event.Value)
}
return nil
}

View File

@@ -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) {
users, err := q.searchUsers(ctx, queries)
users, err := q.searchUsers(ctx, queries, permissionCheck != nil && authz.GetFeatures(ctx).PermissionCheckV2)
if err != nil {
return nil, err
}
if permissionCheck != nil {
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
usersCheckPermission(ctx, users, permissionCheck)
}
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)
defer func() { span.EndWithError(err) }()
query, scan := prepareUsersQuery(ctx, q.client)
eq := sq.Eq{UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()}
stmt, args, err := queries.toQuery(query).Where(eq).
ToSql()
query = queries.toQuery(query).Where(sq.Eq{
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
})
if permissionCheckV2 {
query = wherePermittedOrgs(ctx, query, UserResourceOwnerCol.identifier(), domain.PermissionUserRead)
}
stmt, args, err := query.ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatment")
}

View File

@@ -18,6 +18,7 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SystemDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]])
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, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
@@ -33,4 +34,5 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, InstanceDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceEnableBackChannelLogout, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginVersion, eventstore.GenericEventMapper[SetEvent[*feature.LoginV2]])
eventstore.RegisterFilterEventMapper(AggregateType, InstancePermissionCheckV2, eventstore.GenericEventMapper[SetEvent[bool]])
}

View File

@@ -23,6 +23,7 @@ var (
SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent)
SystemEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelSystem, feature.KeyEnableBackChannelLogout)
SystemLoginVersion = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginV2)
SystemPermissionCheckV2 = setEventTypeFromFeature(feature.LevelSystem, feature.KeyPermissionCheckV2)
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
@@ -38,6 +39,7 @@ var (
InstanceDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDisableUserTokenEvent)
InstanceEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelInstance, feature.KeyEnableBackChannelLogout)
InstanceLoginVersion = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginV2)
InstancePermissionCheckV2 = setEventTypeFromFeature(feature.LevelInstance, feature.KeyPermissionCheckV2)
)
const (

View File

@@ -7,17 +7,25 @@ import (
"github.com/zitadel/zitadel/internal/repository/member"
)
var (
const (
MemberAddedEventType = instanceEventTypePrefix + member.AddedEventType
MemberChangedEventType = instanceEventTypePrefix + member.ChangedEventType
MemberRemovedEventType = instanceEventTypePrefix + member.RemovedEventType
MemberCascadeRemovedEventType = instanceEventTypePrefix + member.CascadeRemovedEventType
)
const (
fieldPrefix = "instance"
)
type MemberAddedEvent struct {
member.MemberAddedEvent
}
func (e *MemberAddedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -51,6 +59,10 @@ type MemberChangedEvent struct {
member.MemberChangedEvent
}
func (e *MemberChangedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -83,6 +95,10 @@ type MemberRemovedEvent struct {
member.MemberRemovedEvent
}
func (e *MemberRemovedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -113,6 +129,10 @@ type MemberCascadeRemovedEvent struct {
member.MemberCascadeRemovedEvent
}
func (e *MemberCascadeRemovedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberCascadeRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,

View File

@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
// Event types
const (
UniqueMember = "member"
AddedEventType = "member.added"
@@ -15,6 +16,13 @@ const (
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 {
return eventstore.NewAddEventUniqueConstraint(
UniqueMember,
@@ -44,6 +52,32 @@ func (e *MemberAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
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(
base *eventstore.BaseEvent,
userID string,
@@ -85,6 +119,38 @@ func (e *MemberChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint
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(
base *eventstore.BaseEvent,
userID string,
@@ -124,6 +190,15 @@ func (e *MemberRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint
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(
base *eventstore.BaseEvent,
userID string,
@@ -162,6 +237,15 @@ func (e *MemberCascadeRemovedEvent) UniqueConstraints() []*eventstore.UniqueCons
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(
base *eventstore.BaseEvent,
userID string,
@@ -185,3 +269,11 @@ func CascadeRemovedEventMapper(event eventstore.Event) (eventstore.Event, error)
return e, nil
}
func memberSearchObject(prefix, userID string) eventstore.Object {
return eventstore.Object{
Type: prefix + memberRoleTypeSuffix,
ID: userID,
Revision: MemberRoleRevision,
}
}

View File

@@ -7,17 +7,25 @@ import (
"github.com/zitadel/zitadel/internal/repository/member"
)
var (
const (
MemberAddedEventType = orgEventTypePrefix + member.AddedEventType
MemberChangedEventType = orgEventTypePrefix + member.ChangedEventType
MemberRemovedEventType = orgEventTypePrefix + member.RemovedEventType
MemberCascadeRemovedEventType = orgEventTypePrefix + member.CascadeRemovedEventType
)
const (
fieldPrefix = "org"
)
type MemberAddedEvent struct {
member.MemberAddedEvent
}
func (e *MemberAddedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -50,6 +58,10 @@ type MemberChangedEvent struct {
member.MemberChangedEvent
}
func (e *MemberChangedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -83,6 +95,10 @@ type MemberRemovedEvent struct {
member.MemberRemovedEvent
}
func (e *MemberRemovedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -113,6 +129,10 @@ type MemberCascadeRemovedEvent struct {
member.MemberCascadeRemovedEvent
}
func (e *MemberCascadeRemovedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewMemberCascadeRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,

View 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,
}
}

View 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,
}
}

View File

@@ -14,10 +14,18 @@ var (
MemberCascadeRemovedType = projectEventTypePrefix + member.CascadeRemovedEventType
)
const (
fieldPrefix = "project"
)
type MemberAddedEvent struct {
member.MemberAddedEvent
}
func (e *MemberAddedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewProjectMemberAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -50,6 +58,10 @@ type MemberChangedEvent struct {
member.MemberChangedEvent
}
func (e *MemberChangedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewProjectMemberChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -83,6 +95,10 @@ type MemberRemovedEvent struct {
member.MemberRemovedEvent
}
func (e *MemberRemovedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewProjectMemberRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
@@ -114,6 +130,10 @@ type MemberCascadeRemovedEvent struct {
member.MemberCascadeRemovedEvent
}
func (e *MemberCascadeRemovedEvent) Fields() []*eventstore.FieldOperation {
return e.FieldOperations(fieldPrefix)
}
func NewProjectMemberCascadeRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,