From a63a99526982923588dc86ba11e4fb9d9b9b7443 Mon Sep 17 00:00:00 2001 From: Silvan Date: Thu, 13 Jan 2022 11:02:39 +0100 Subject: [PATCH] fix(projections): add user grant projection (#2837) * refactor(domain): add user type * fix(projections): start with login names * fix(login_policy): correct handling of user domain claimed event * fix(projections): add members * refactor: simplify member projections * add migration for members * add metadata to member projections * refactor: login name projection * fix: set correct suffixes on login name projections * test(projections): login name reduces * fix: correct cols in reduce member * test(projections): org, iam, project members * member additional cols and conds as opt, add project grant members * fix(migration): members * fix(migration): correct database name * migration version * migs * better naming for member cond and col * split project and project grant members * prepare member columns * feat(queries): membership query * test(queries): membership prepare * fix(queries): multiple projections for latest sequence * fix(api): use query for membership queries in auth and management * feat: org member queries * fix(api): use query for iam member calls * fix(queries): org members * fix(queries): project members * fix(queries): project grant members * fix(query): member queries and user avatar column * member cols * fix(queries): membership stmt * fix user test * fix user test * fix(projections): add user grant projection * fix(user_grant): handle state changes * add state to migration * merge eventstore-naming into user-grant-projection * fix(migrations): version * fix(query): event mappers for usergrant aggregate * fix(projection): correct aggregate for user grants * cleanup reducers * add tests for projection Co-authored-by: Livio Amstutz --- internal/query/projection/projection.go | 1 + internal/query/projection/user_grant.go | 255 ++++++++++++++ internal/query/projection/user_grant_test.go | 341 +++++++++++++++++++ internal/query/query.go | 2 + migrations/cockroach/V1.104__user_grants.sql | 17 + 5 files changed, 616 insertions(+) create mode 100644 internal/query/projection/user_grant.go create mode 100644 internal/query/projection/user_grant_test.go create mode 100644 migrations/cockroach/V1.104__user_grants.sql diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index abb9600ce5..83c90021d2 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -64,6 +64,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co NewProjectMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_members"])) NewProjectGrantMemberProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_members"])) NewAuthNKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["authn_keys"])) + NewUserGrantProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_grants"])) _, err := NewKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), defaults.KeyConfig, keyChan) return err diff --git a/internal/query/projection/user_grant.go b/internal/query/projection/user_grant.go new file mode 100644 index 0000000000..2ced2c50c4 --- /dev/null +++ b/internal/query/projection/user_grant.go @@ -0,0 +1,255 @@ +package projection + +import ( + "context" + + "github.com/caos/logging" + "github.com/lib/pq" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/handler/crdb" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" + "github.com/caos/zitadel/internal/repository/usergrant" +) + +type UserGrantProjection struct { + crdb.StatementHandler +} + +const ( + UserGrantProjectionTable = "zitadel.projections.user_grants" +) + +func NewUserGrantProjection(ctx context.Context, config crdb.StatementHandlerConfig) *UserGrantProjection { + p := &UserGrantProjection{} + config.ProjectionName = UserGrantProjectionTable + config.Reducers = p.reducers() + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *UserGrantProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: usergrant.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: usergrant.UserGrantAddedType, + Reduce: p.reduceAdded, + }, + { + Event: usergrant.UserGrantChangedType, + Reduce: p.reduceChanged, + }, + { + Event: usergrant.UserGrantCascadeChangedType, + Reduce: p.reduceChanged, + }, + { + Event: usergrant.UserGrantRemovedType, + Reduce: p.reduceRemoved, + }, + { + Event: usergrant.UserGrantCascadeRemovedType, + Reduce: p.reduceRemoved, + }, + { + Event: usergrant.UserGrantDeactivatedType, + Reduce: p.reduceDeactivated, + }, + { + Event: usergrant.UserGrantReactivatedType, + Reduce: p.reduceReactivated, + }, + }, + }, + { + Aggregate: user.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: user.UserRemovedType, + Reduce: p.reduceUserRemoved, + }, + }, + }, + { + Aggregate: project.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: project.ProjectRemovedType, + Reduce: p.reduceProjectRemoved, + }, + { + Event: project.GrantRemovedType, + Reduce: p.reduceProjectGrantRemoved, + }, + }, + }, + } +} + +type UserGrantColumn string + +const ( + UserGrantID = "id" + UserGrantResourceOwner = "resource_owner" + UserGrantCreationDate = "creation_date" + UserGrantChangeDate = "change_date" + UserGrantSequence = "sequence" + UserGrantUserID = "user_id" + UserGrantProjectID = "project_id" + UserGrantGrantID = "grant_id" + UserGrantRoles = "roles" + UserGrantState = "state" +) + +func (p *UserGrantProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*usergrant.UserGrantAddedEvent) + if !ok { + logging.LogWithFields("PROJE-WYOHD", "seq", event.Sequence(), "expectedType", usergrant.UserGrantAddedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-MQHVB", "reduce.wrong.event.type") + } + return crdb.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(UserGrantID, e.Aggregate().ID), + handler.NewCol(UserGrantResourceOwner, e.Aggregate().ResourceOwner), + handler.NewCol(UserGrantCreationDate, e.CreationDate()), + handler.NewCol(UserGrantChangeDate, e.CreationDate()), + handler.NewCol(UserGrantSequence, e.Sequence()), + handler.NewCol(UserGrantUserID, e.UserID), + handler.NewCol(UserGrantProjectID, e.ProjectID), + handler.NewCol(UserGrantGrantID, e.ProjectGrantID), + handler.NewCol(UserGrantRoles, pq.StringArray(e.RoleKeys)), + handler.NewCol(UserGrantState, domain.UserGrantStateActive), + }, + ), nil +} + +func (p *UserGrantProjection) reduceChanged(event eventstore.Event) (*handler.Statement, error) { + var roles pq.StringArray + + switch e := event.(type) { + case *usergrant.UserGrantChangedEvent: + roles = e.RoleKeys + case *usergrant.UserGrantCascadeChangedEvent: + roles = e.RoleKeys + default: + logging.LogWithFields("PROJE-dIflx", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{usergrant.UserGrantChangedType, usergrant.UserGrantCascadeChangedType}).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-hOr1E", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + event, + []handler.Column{ + handler.NewCol(UserGrantChangeDate, event.CreationDate()), + handler.NewCol(UserGrantRoles, roles), + handler.NewCol(UserGrantSequence, event.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserGrantID, event.Aggregate().ID), + }, + ), nil +} + +func (p *UserGrantProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) { + switch event.(type) { + case *usergrant.UserGrantRemovedEvent, *usergrant.UserGrantCascadeRemovedEvent: + // ok + default: + logging.LogWithFields("PROJE-Nw0cR", "seq", event.Sequence(), "expectedTypes", []eventstore.EventType{usergrant.UserGrantRemovedType, usergrant.UserGrantCascadeRemovedType}).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-7OBEC", "reduce.wrong.event.type") + } + + return crdb.NewDeleteStatement( + event, + []handler.Condition{ + handler.NewCond(UserGrantID, event.Aggregate().ID), + }, + ), nil +} + +func (p *UserGrantProjection) reduceDeactivated(event eventstore.Event) (*handler.Statement, error) { + if _, ok := event.(*usergrant.UserGrantDeactivatedEvent); !ok { + logging.LogWithFields("PROJE-V3txf", "seq", event.Sequence(), "expectedType", usergrant.UserGrantDeactivatedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-oP7Gm", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + event, + []handler.Column{ + handler.NewCol(UserGrantChangeDate, event.CreationDate()), + handler.NewCol(UserGrantState, domain.UserGrantStateInactive), + handler.NewCol(UserGrantSequence, event.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserGrantID, event.Aggregate().ID), + }, + ), nil +} + +func (p *UserGrantProjection) reduceReactivated(event eventstore.Event) (*handler.Statement, error) { + if _, ok := event.(*usergrant.UserGrantDeactivatedEvent); !ok { + logging.LogWithFields("PROJE-ly6oe", "seq", event.Sequence(), "expectedType", usergrant.UserGrantReactivatedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-DGsKh", "reduce.wrong.event.type") + } + + return crdb.NewUpdateStatement( + event, + []handler.Column{ + handler.NewCol(UserGrantChangeDate, event.CreationDate()), + handler.NewCol(UserGrantState, domain.UserGrantStateActive), + handler.NewCol(UserGrantSequence, event.Sequence()), + }, + []handler.Condition{ + handler.NewCond(UserGrantID, event.Aggregate().ID), + }, + ), nil +} + +func (p *UserGrantProjection) reduceUserRemoved(event eventstore.Event) (*handler.Statement, error) { + if _, ok := event.(*user.UserRemovedEvent); !ok { + logging.LogWithFields("PROJE-Vfeg3", "seq", event.Sequence(), "expectedType", user.UserRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-Bner2a", "reduce.wrong.event.type") + } + + return crdb.NewDeleteStatement( + event, + []handler.Condition{ + handler.NewCond(UserGrantUserID, event.Aggregate().ID), + }, + ), nil +} + +func (p *UserGrantProjection) reduceProjectRemoved(event eventstore.Event) (*handler.Statement, error) { + if _, ok := event.(*project.ProjectRemovedEvent); !ok { + logging.LogWithFields("PROJE-Vfeg3", "seq", event.Sequence(), "expectedType", project.ProjectRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-Bne2a", "reduce.wrong.event.type") + } + + return crdb.NewDeleteStatement( + event, + []handler.Condition{ + handler.NewCond(UserGrantProjectID, event.Aggregate().ID), + }, + ), nil +} + +func (p *UserGrantProjection) reduceProjectGrantRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*project.GrantRemovedEvent) + if !ok { + logging.LogWithFields("PROJE-DGfe2", "seq", event.Sequence(), "expectedType", project.GrantRemovedType).Error("wrong event type") + return nil, errors.ThrowInvalidArgument(nil, "PROJE-dGr2a", "reduce.wrong.event.type") + } + + return crdb.NewDeleteStatement( + event, + []handler.Condition{ + handler.NewCond(UserGrantGrantID, e.GrantID), + }, + ), nil +} diff --git a/internal/query/projection/user_grant_test.go b/internal/query/projection/user_grant_test.go new file mode 100644 index 0000000000..d92b57ab20 --- /dev/null +++ b/internal/query/projection/user_grant_test.go @@ -0,0 +1,341 @@ +package projection + +import ( + "testing" + + "github.com/lib/pq" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/eventstore/handler" + "github.com/caos/zitadel/internal/eventstore/repository" + "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/user" + "github.com/caos/zitadel/internal/repository/usergrant" +) + +func TestUserGrantProjection_reduces(t *testing.T) { + type args struct { + event func(t *testing.T) eventstore.Event + } + tests := []struct { + name string + args args + reduce func(event eventstore.Event) (*handler.Statement, error) + want wantReduce + }{ + { + name: "reduceAdded", + args: args{ + event: getEvent(testEvent( + repository.EventType(usergrant.UserGrantAddedType), + usergrant.AggregateType, + []byte(`{ + "userId": "user-id", + "projectId": "project-id", + "roleKeys": ["role"] + }`), + ), usergrant.UserGrantAddedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceAdded, + want: wantReduce{ + aggregateType: usergrant.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "INSERT INTO zitadel.projections.user_grants (id, resource_owner, creation_date, change_date, sequence, user_id, project_id, grant_id, roles, state) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", + expectedArgs: []interface{}{ + "agg-id", + "ro-id", + anyArg{}, + anyArg{}, + uint64(15), + "user-id", + "project-id", + "", + pq.StringArray{"role"}, + domain.UserGrantStateActive, + }, + }, + }, + }, + }, + }, + { + name: "reduceChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(usergrant.UserGrantChangedType), + usergrant.AggregateType, + []byte(`{ + "roleKeys": ["role"] + }`), + ), usergrant.UserGrantChangedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceChanged, + want: wantReduce{ + aggregateType: usergrant.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.user_grants SET (change_date, roles, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + pq.StringArray{"role"}, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceCascadeChanged", + args: args{ + event: getEvent(testEvent( + repository.EventType(usergrant.UserGrantCascadeChangedType), + usergrant.AggregateType, + []byte(`{ + "roleKeys": ["role"] + }`), + ), usergrant.UserGrantCascadeChangedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceChanged, + want: wantReduce{ + aggregateType: usergrant.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.user_grants SET (change_date, roles, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + pq.StringArray{"role"}, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(usergrant.UserGrantRemovedType), + usergrant.AggregateType, + nil, + ), usergrant.UserGrantRemovedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceRemoved, + want: wantReduce{ + aggregateType: usergrant.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.user_grants WHERE (id = $1)", + expectedArgs: []interface{}{ + anyArg{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceCascadeRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(usergrant.UserGrantCascadeRemovedType), + usergrant.AggregateType, + nil, + ), usergrant.UserGrantCascadeRemovedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceRemoved, + want: wantReduce{ + aggregateType: usergrant.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.user_grants WHERE (id = $1)", + expectedArgs: []interface{}{ + anyArg{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceDeactivated", + args: args{ + event: getEvent(testEvent( + repository.EventType(usergrant.UserGrantDeactivatedType), + usergrant.AggregateType, + nil, + ), usergrant.UserGrantDeactivatedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceDeactivated, + want: wantReduce{ + aggregateType: usergrant.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.user_grants SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + domain.UserGrantStateInactive, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceReactivated", + args: args{ + event: getEvent(testEvent( + repository.EventType(usergrant.UserGrantReactivatedType), + usergrant.AggregateType, + nil, + ), usergrant.UserGrantDeactivatedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceReactivated, + want: wantReduce{ + aggregateType: usergrant.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "UPDATE zitadel.projections.user_grants SET (change_date, state, sequence) = ($1, $2, $3) WHERE (id = $4)", + expectedArgs: []interface{}{ + anyArg{}, + domain.UserGrantStateActive, + uint64(15), + "agg-id", + }, + }, + }, + }, + }, + }, + { + name: "reduceUserRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(user.UserRemovedType), + user.AggregateType, + nil, + ), user.UserRemovedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceUserRemoved, + want: wantReduce{ + aggregateType: user.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.user_grants WHERE (user_id = $1)", + expectedArgs: []interface{}{ + anyArg{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceProjectRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.ProjectRemovedType), + project.AggregateType, + nil, + ), project.ProjectRemovedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceProjectRemoved, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.user_grants WHERE (project_id = $1)", + expectedArgs: []interface{}{ + anyArg{}, + }, + }, + }, + }, + }, + }, + { + name: "reduceProjectGrantRemoved", + args: args{ + event: getEvent(testEvent( + repository.EventType(project.GrantRemovedType), + project.AggregateType, + []byte(`{"grantId": "grantID"}`), + ), project.GrantRemovedEventMapper), + }, + reduce: (&UserGrantProjection{}).reduceProjectGrantRemoved, + want: wantReduce{ + aggregateType: project.AggregateType, + sequence: 15, + previousSequence: 10, + projection: UserGrantProjectionTable, + executer: &testExecuter{ + executions: []execution{ + { + expectedStmt: "DELETE FROM zitadel.projections.user_grants WHERE (grant_id = $1)", + expectedArgs: []interface{}{ + "grantID", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + event := baseEvent(t) + got, err := tt.reduce(event) + if _, ok := err.(errors.InvalidArgument); !ok { + t.Errorf("no wrong event mapping: %v, got: %v", err, got) + } + + event = tt.args.event(t) + got, err = tt.reduce(event) + assertReduce(t, got, err, tt.want) + }) + } +} diff --git a/internal/query/query.go b/internal/query/query.go index 2f5685a213..04531ed0af 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -18,6 +18,7 @@ import ( "github.com/caos/zitadel/internal/repository/org" "github.com/caos/zitadel/internal/repository/project" usr_repo "github.com/caos/zitadel/internal/repository/user" + "github.com/caos/zitadel/internal/repository/usergrant" "github.com/caos/zitadel/internal/telemetry/tracing" "github.com/rakyll/statik/fs" "golang.org/x/text/language" @@ -68,6 +69,7 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, projections pr project.RegisterEventMappers(repo.eventstore) action.RegisterEventMappers(repo.eventstore) keypair.RegisterEventMappers(repo.eventstore) + usergrant.RegisterEventMappers(repo.eventstore) err = projection.Start(ctx, sqlClient, es, projections, defaults, keyChan) if err != nil { diff --git a/migrations/cockroach/V1.104__user_grants.sql b/migrations/cockroach/V1.104__user_grants.sql new file mode 100644 index 0000000000..00902f90f1 --- /dev/null +++ b/migrations/cockroach/V1.104__user_grants.sql @@ -0,0 +1,17 @@ +CREATE TABLE zitadel.projections.user_grants ( + id STRING NOT NULL + , resource_owner STRING NOT NULL + , creation_date TIMESTAMPTZ NOT NULL + , change_date TIMESTAMPTZ NOT NULL + , sequence INT8 NOT NULL + + , user_id STRING NOT NULL + , project_id STRING NOT NULL + , grant_id STRING NOT NULL + , roles STRING[] + , state INT2 NOT NULL + + , PRIMARY KEY (id) + , INDEX idx_user (user_id) + , INDEX idx_ro (resource_owner) +); \ No newline at end of file