feat: add administrator repository (#11803)

Introduces the `administrators` relational table and repository layer
for the v3 backend, unifying instance, organization, project, and
project-grant membership into a single scoped model.

Closes #10220

## Changes

- **Domain**: `Administrator` struct, `AdministratorScope` enum,
`AdministratorRepository` interface with typed CRUD, column, condition,
and change helpers. Generated enumer and GoMock.
- **Migration** (`016`): `zitadel.administrators` table with a generated
`id`, scope-alignment check constraint, cascading foreign keys, and a
child `administrator_roles` table.
- **Repository**: CTE-based role mutations (`AddRole`, `RemoveRole`,
`SetRoles`) for atomicity. Roles aggregated via `ARRAY_AGG`.
- **Projection**: Event reducers for
`member.added/changed/removed/cascade_removed` across all four admin
scopes.
- **Fixes**: `Rows.Close()` error wrapping; `TextArray` nil-safe scan +
full `pgtype` array interface; `authorization` and `projectGrant` scan
helpers fixed for `ARRAY_AGG` under `database/sql`; removed unused `ctx`
parameter from `callReduce`.

---------

Co-authored-by: Marco Ardizzone <[email protected]>
This commit is contained in:
Silvan
2026-03-18 10:24:09 +01:00
committed by GitHub
co-authored by Marco Ardizzone
parent 6d90a120a6
commit f138082b64
21 changed files with 3513 additions and 46 deletions
@@ -0,0 +1,226 @@
package projection
import (
"context"
"database/sql"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
v3_sql "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/sql"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (p *relationalTablesProjection) reduceInstanceAdminAdded(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*instance.MemberAddedEvent](event)
if err != nil {
return nil, err
}
return p.createAdministratorStatement(e, &domain.Administrator{
InstanceID: e.Aggregate().InstanceID,
UserID: e.UserID,
Scope: domain.AdministratorScopeInstance,
Roles: e.Roles,
CreatedAt: e.CreationDate(),
UpdatedAt: e.CreationDate(),
})
}
func (p *relationalTablesProjection) reduceInstanceAdminChanged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*instance.MemberChangedEvent](event)
if err != nil {
return nil, err
}
return p.updateAdministratorStatement(e,
repository.AdministratorRepository().InstanceAdministratorCondition(e.Aggregate().InstanceID, e.UserID),
repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()),
repository.AdministratorRepository().SetRoles(e.Roles),
)
}
func (p *relationalTablesProjection) reduceInstanceAdminRemoved(event eventstore.Event) (*handler.Statement, error) {
switch e := event.(type) {
case *instance.MemberRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().InstanceAdministratorCondition(e.Aggregate().InstanceID, e.UserID),
)
case *instance.MemberCascadeRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().InstanceAdministratorCondition(e.Aggregate().InstanceID, e.UserID),
)
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM03", "reduce.wrong.event.type %v", []eventstore.EventType{instance.MemberRemovedEventType, instance.MemberCascadeRemovedEventType})
}
}
func (p *relationalTablesProjection) reduceOrganizationAdminAdded(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*org.MemberAddedEvent](event)
if err != nil {
return nil, err
}
return p.createAdministratorStatement(e, &domain.Administrator{
InstanceID: e.Aggregate().InstanceID,
UserID: e.UserID,
Scope: domain.AdministratorScopeOrganization,
OrganizationID: stringPtr(e.Aggregate().ID),
Roles: e.Roles,
CreatedAt: e.CreationDate(),
UpdatedAt: e.CreationDate(),
})
}
func (p *relationalTablesProjection) reduceOrganizationAdminChanged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*org.MemberChangedEvent](event)
if err != nil {
return nil, err
}
return p.updateAdministratorStatement(e,
repository.AdministratorRepository().OrganizationAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID),
repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()),
repository.AdministratorRepository().SetRoles(e.Roles),
)
}
func (p *relationalTablesProjection) reduceOrganizationAdminRemoved(event eventstore.Event) (*handler.Statement, error) {
switch e := event.(type) {
case *org.MemberRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().OrganizationAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID),
)
case *org.MemberCascadeRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().OrganizationAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID),
)
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM06", "reduce.wrong.event.type %v", []eventstore.EventType{org.MemberRemovedEventType, org.MemberCascadeRemovedEventType})
}
}
func (p *relationalTablesProjection) reduceProjectAdminAdded(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*project.MemberAddedEvent](event)
if err != nil {
return nil, err
}
return p.createAdministratorStatement(e, &domain.Administrator{
InstanceID: e.Aggregate().InstanceID,
UserID: e.UserID,
Scope: domain.AdministratorScopeProject,
ProjectID: stringPtr(e.Aggregate().ID),
Roles: e.Roles,
CreatedAt: e.CreationDate(),
UpdatedAt: e.CreationDate(),
})
}
func (p *relationalTablesProjection) reduceProjectAdminChanged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*project.MemberChangedEvent](event)
if err != nil {
return nil, err
}
return p.updateAdministratorStatement(e,
repository.AdministratorRepository().ProjectAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID),
repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()),
repository.AdministratorRepository().SetRoles(e.Roles),
)
}
func (p *relationalTablesProjection) reduceProjectAdminRemoved(event eventstore.Event) (*handler.Statement, error) {
switch e := event.(type) {
case *project.MemberRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().ProjectAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID),
)
case *project.MemberCascadeRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().ProjectAdministratorCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.UserID),
)
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM09", "reduce.wrong.event.type %v", []eventstore.EventType{project.MemberRemovedEventType, project.MemberCascadeRemovedEventType})
}
}
func (p *relationalTablesProjection) reduceProjectGrantAdminAdded(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*project.GrantMemberAddedEvent](event)
if err != nil {
return nil, err
}
return p.createAdministratorStatement(e, &domain.Administrator{
InstanceID: e.Aggregate().InstanceID,
UserID: e.UserID,
Scope: domain.AdministratorScopeProjectGrant,
ProjectGrantID: stringPtr(e.GrantID),
Roles: e.Roles,
CreatedAt: e.CreationDate(),
UpdatedAt: e.CreationDate(),
})
}
func (p *relationalTablesProjection) reduceProjectGrantAdminChanged(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*project.GrantMemberChangedEvent](event)
if err != nil {
return nil, err
}
return p.updateAdministratorStatement(e,
repository.AdministratorRepository().ProjectGrantAdministratorCondition(e.Aggregate().InstanceID, e.GrantID, e.UserID),
repository.AdministratorRepository().SetUpdatedAt(e.CreationDate()),
repository.AdministratorRepository().SetRoles(e.Roles),
)
}
func (p *relationalTablesProjection) reduceProjectGrantAdminRemoved(event eventstore.Event) (*handler.Statement, error) {
switch e := event.(type) {
case *project.GrantMemberRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().ProjectGrantAdministratorCondition(e.Aggregate().InstanceID, e.GrantID, e.UserID),
)
case *project.GrantMemberCascadeRemovedEvent:
return p.removeAdministratorStatement(e,
repository.AdministratorRepository().ProjectGrantAdministratorCondition(e.Aggregate().InstanceID, e.GrantID, e.UserID),
)
default:
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM12", "reduce.wrong.event.type %v", []eventstore.EventType{project.GrantMemberRemovedType, project.GrantMemberCascadeRemovedType})
}
}
func (p *relationalTablesProjection) createAdministratorStatement(event eventstore.Event, administrator *domain.Administrator) (*handler.Statement, error) {
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, _ string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM13", "reduce.wrong.db.pool %T", ex)
}
return repository.AdministratorRepository().Create(ctx, v3_sql.SQLTx(tx), administrator)
}), nil
}
func (p *relationalTablesProjection) updateAdministratorStatement(event eventstore.Event, condition database.Condition, changes ...database.Change) (*handler.Statement, error) {
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, _ string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM14", "reduce.wrong.db.pool %T", ex)
}
repo := repository.AdministratorRepository()
_, err := repo.Update(ctx, v3_sql.SQLTx(tx), condition, changes...)
return err
}), nil
}
func (p *relationalTablesProjection) removeAdministratorStatement(event eventstore.Event, condition database.Condition) (*handler.Statement, error) {
return handler.NewStatement(event, func(ctx context.Context, ex handler.Executer, _ string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-v3ADM15", "reduce.wrong.db.pool %T", ex)
}
repo := repository.AdministratorRepository()
_, err := repo.Delete(ctx, v3_sql.SQLTx(tx), condition)
return err
}), nil
}
func stringPtr(value string) *string {
return &value
}
@@ -0,0 +1,369 @@
package projection
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
repoDomain "github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/project"
)
func TestAdministratorRelationalReducers(t *testing.T) {
handler := new(relationalTablesProjection)
rawTx, tx := getTransactions(t)
t.Cleanup(func() {
require.NoError(t, rawTx.Rollback())
})
instanceID, _, orgID, projectID, grantID := seedAdministratorRelationalState(t, tx)
adminRepo := repository.AdministratorRepository()
userRepo := repository.UserRepository()
var userSeq int
createUser := func(t *testing.T) string {
t.Helper()
userSeq++
userID := fmt.Sprintf("admin-test-user-%d", userSeq)
err := userRepo.Create(t.Context(), tx, &repoDomain.User{
InstanceID: instanceID,
OrganizationID: orgID,
ID: userID,
Username: userID,
State: repoDomain.UserStateActive,
Machine: &repoDomain.MachineUser{
Name: "machine",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
return userID
}
t.Run("'instance member added' reducer should create instance administrator", func(t *testing.T) {
userID := createUser(t)
added := instance.NewMemberAddedEvent(t.Context(), &instance.NewAggregate(instanceID).Aggregate, userID, "IAM_OWNER")
require.True(t, callReduce(t, rawTx, handler, added))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.InstanceAdministratorCondition(instanceID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"IAM_OWNER"}, admin.Roles)
})
t.Run("'instance member changed' reducer should update instance administrator roles", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeInstance,
Roles: []string{"IAM_OWNER"},
})
require.NoError(t, err)
changed := instance.NewMemberChangedEvent(t.Context(), &instance.NewAggregate(instanceID).Aggregate, userID, "IAM_OWNER_VIEWER")
require.True(t, callReduce(t, rawTx, handler, changed))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.InstanceAdministratorCondition(instanceID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"IAM_OWNER_VIEWER"}, admin.Roles)
})
t.Run("'instance member removed' reducer should remove instance administrator", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeInstance,
Roles: []string{"IAM_OWNER"},
})
require.NoError(t, err)
removed := instance.NewMemberRemovedEvent(t.Context(), &instance.NewAggregate(instanceID).Aggregate, userID)
require.True(t, callReduce(t, rawTx, handler, removed))
_, err = adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.InstanceAdministratorCondition(instanceID, userID),
))
require.ErrorIs(t, err, database.NewNoRowFoundError(nil))
})
t.Run("'org member added' reducer should create organization administrator", func(t *testing.T) {
userID := createUser(t)
orgAggregate := org.NewAggregate(orgID)
orgAggregate.InstanceID = instanceID
added := org.NewMemberAddedEvent(t.Context(), &orgAggregate.Aggregate, userID, "ORG_OWNER")
require.True(t, callReduce(t, rawTx, handler, added))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"ORG_OWNER"}, admin.Roles)
})
t.Run("'org member changed' reducer should update organization administrator roles", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeOrganization,
OrganizationID: &orgID,
Roles: []string{"ORG_OWNER"},
})
require.NoError(t, err)
orgAggregate := org.NewAggregate(orgID)
orgAggregate.InstanceID = instanceID
changed := org.NewMemberChangedEvent(t.Context(), &orgAggregate.Aggregate, userID, "ORG_OWNER_VIEWER")
require.True(t, callReduce(t, rawTx, handler, changed))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"ORG_OWNER_VIEWER"}, admin.Roles)
})
t.Run("'org member removed' reducer should remove organization administrator", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeOrganization,
OrganizationID: &orgID,
Roles: []string{"ORG_OWNER"},
})
require.NoError(t, err)
orgAggregate := org.NewAggregate(orgID)
orgAggregate.InstanceID = instanceID
removed := org.NewMemberRemovedEvent(t.Context(), &orgAggregate.Aggregate, userID)
require.True(t, callReduce(t, rawTx, handler, removed))
_, err = adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.OrganizationAdministratorCondition(instanceID, orgID, userID),
))
require.ErrorIs(t, err, database.NewNoRowFoundError(nil))
})
t.Run("'project member added' reducer should create project administrator", func(t *testing.T) {
userID := createUser(t)
projectAggregate := project.NewAggregate(projectID, orgID)
projectAggregate.InstanceID = instanceID
added := project.NewProjectMemberAddedEvent(t.Context(), &projectAggregate.Aggregate, userID, "PROJECT_OWNER")
require.True(t, callReduce(t, rawTx, handler, added))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.ProjectAdministratorCondition(instanceID, projectID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"PROJECT_OWNER"}, admin.Roles)
})
t.Run("'project member changed' reducer should update project administrator roles", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeProject,
ProjectID: &projectID,
Roles: []string{"PROJECT_OWNER"},
})
require.NoError(t, err)
projectAggregate := project.NewAggregate(projectID, orgID)
projectAggregate.InstanceID = instanceID
changed := project.NewProjectMemberChangedEvent(t.Context(), &projectAggregate.Aggregate, userID, "PROJECT_OWNER_VIEWER")
require.True(t, callReduce(t, rawTx, handler, changed))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.ProjectAdministratorCondition(instanceID, projectID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"PROJECT_OWNER_VIEWER"}, admin.Roles)
})
t.Run("'project member removed' reducer should remove project administrator", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeProject,
ProjectID: &projectID,
Roles: []string{"PROJECT_OWNER"},
})
require.NoError(t, err)
projectAggregate := project.NewAggregate(projectID, orgID)
projectAggregate.InstanceID = instanceID
removed := project.NewProjectMemberRemovedEvent(t.Context(), &projectAggregate.Aggregate, userID)
require.True(t, callReduce(t, rawTx, handler, removed))
_, err = adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.ProjectAdministratorCondition(instanceID, projectID, userID),
))
require.ErrorIs(t, err, database.NewNoRowFoundError(nil))
})
t.Run("'project grant member added' reducer should create project grant administrator", func(t *testing.T) {
userID := createUser(t)
projectAggregate := project.NewAggregate(projectID, orgID)
projectAggregate.InstanceID = instanceID
added := project.NewProjectGrantMemberAddedEvent(t.Context(), &projectAggregate.Aggregate, userID, grantID, "PROJECT_OWNER")
require.True(t, callReduce(t, rawTx, handler, added))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.ProjectGrantAdministratorCondition(instanceID, grantID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"PROJECT_OWNER"}, admin.Roles)
})
t.Run("'project grant member changed' reducer should update project grant administrator roles", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeProjectGrant,
ProjectGrantID: &grantID,
Roles: []string{"PROJECT_OWNER"},
})
require.NoError(t, err)
projectAggregate := project.NewAggregate(projectID, orgID)
projectAggregate.InstanceID = instanceID
changed := project.NewProjectGrantMemberChangedEvent(t.Context(), &projectAggregate.Aggregate, userID, grantID, "PROJECT_OWNER_VIEWER")
require.True(t, callReduce(t, rawTx, handler, changed))
admin, err := adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.ProjectGrantAdministratorCondition(instanceID, grantID, userID),
))
require.NoError(t, err)
assert.Equal(t, []string{"PROJECT_OWNER_VIEWER"}, admin.Roles)
})
t.Run("'project grant member cascade removed' reducer should remove project grant administrator", func(t *testing.T) {
userID := createUser(t)
err := adminRepo.Create(t.Context(), tx, &repoDomain.Administrator{
InstanceID: instanceID,
UserID: userID,
Scope: repoDomain.AdministratorScopeProjectGrant,
ProjectGrantID: &grantID,
Roles: []string{"PROJECT_OWNER"},
})
require.NoError(t, err)
projectAggregate := project.NewAggregate(projectID, orgID)
projectAggregate.InstanceID = instanceID
removed := project.NewProjectGrantMemberCascadeRemovedEvent(t.Context(), &projectAggregate.Aggregate, userID, grantID)
require.True(t, callReduce(t, rawTx, handler, removed))
_, err = adminRepo.Get(t.Context(), tx, database.WithCondition(
adminRepo.ProjectGrantAdministratorCondition(instanceID, grantID, userID),
))
require.ErrorIs(t, err, database.NewNoRowFoundError(nil))
})
}
func seedAdministratorRelationalState(t *testing.T, tx database.QueryExecutor) (instanceID, userID, orgID, projectID, projectGrantID string) {
t.Helper()
instanceRepo := repository.InstanceRepository()
orgRepo := repository.OrganizationRepository()
projectRepo := repository.ProjectRepository()
projectGrantRepo := repository.ProjectGrantRepository()
userRepo := repository.UserRepository()
now := time.Now().UnixNano()
instanceID = fmt.Sprintf("instance-%d", now)
err := instanceRepo.Create(t.Context(), tx, &repoDomain.Instance{
ID: instanceID,
Name: "instance",
DefaultOrgID: "default-org",
IAMProjectID: "iam-project",
ConsoleClientID: "console-client",
ConsoleAppID: "console-app",
DefaultLanguage: "en",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
orgID = fmt.Sprintf("org-%d", now+1)
err = orgRepo.Create(t.Context(), tx, &repoDomain.Organization{
InstanceID: instanceID,
ID: orgID,
Name: "org",
State: repoDomain.OrgStateActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
projectID = fmt.Sprintf("project-%d", now+2)
err = projectRepo.Create(t.Context(), tx, &repoDomain.Project{
InstanceID: instanceID,
OrganizationID: orgID,
ID: projectID,
Name: "project",
State: repoDomain.ProjectStateActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
projectGrantID = fmt.Sprintf("grant-%d", now+3)
err = projectGrantRepo.Create(t.Context(), tx, &repoDomain.ProjectGrant{
InstanceID: instanceID,
ID: projectGrantID,
ProjectID: projectID,
GrantingOrganizationID: orgID,
GrantedOrganizationID: orgID,
State: repoDomain.ProjectGrantStateActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
userID = fmt.Sprintf("user-%d", now+4)
err = userRepo.Create(t.Context(), tx, &repoDomain.User{
InstanceID: instanceID,
OrganizationID: orgID,
ID: userID,
Username: userID,
State: repoDomain.UserStateActive,
Machine: &repoDomain.MachineUser{
Name: "machine",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
require.NoError(t, err)
return instanceID, userID, orgID, projectID, projectGrantID
}
@@ -85,7 +85,7 @@ func getTransactions(t *testing.T) (rawTx *sql.Tx, v3SQLTx *v3_sql.Transaction)
// 4. the reducer function for the input event executes without errors
//
// Returns false if any of the above checks fails
func callReduce(t *testing.T, ctx context.Context, tx *sql.Tx, projection handler.Projection, event eventstore.Event) bool {
func callReduce(t *testing.T, tx *sql.Tx, projection handler.Projection, event eventstore.Event) bool {
reducers := projection.Reducers()
aggregateReducerIdx := slices.IndexFunc(reducers, func(r handler.AggregateReducer) bool {
return r.Aggregate == event.Aggregate().Type
@@ -111,6 +111,6 @@ func callReduce(t *testing.T, ctx context.Context, tx *sql.Tx, projection handle
return false
}
err = stmt.Execute(ctx, tx, "")
err = stmt.Execute(t.Context(), tx, "")
return assert.NoError(t, err)
}
@@ -44,7 +44,7 @@ func TestInstanceDomainReducers(t *testing.T) {
domainAddedEvt := instance.NewDomainAddedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName, false)
// Test
res := callReduce(t, ctx, rawTx, handler, domainAddedEvt)
res := callReduce(t, rawTx, handler, domainAddedEvt)
require.True(t, res)
// Verify
@@ -85,7 +85,7 @@ func TestInstanceDomainReducers(t *testing.T) {
domainPrimarySetEvt := instance.NewDomainPrimarySetEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName)
// Test
res := callReduce(t, ctx, rawTx, handler, domainPrimarySetEvt)
res := callReduce(t, rawTx, handler, domainPrimarySetEvt)
require.True(t, res)
// Verify
@@ -127,7 +127,7 @@ func TestInstanceDomainReducers(t *testing.T) {
domainRemovedEvt := instance.NewDomainRemovedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName)
// Test
res := callReduce(t, ctx, rawTx, handler, domainRemovedEvt)
res := callReduce(t, rawTx, handler, domainRemovedEvt)
require.True(t, res)
// Verify
@@ -150,7 +150,7 @@ func TestInstanceDomainReducers(t *testing.T) {
trustedDomainAddedEvt := instance.NewTrustedDomainAddedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName)
// Test
res := callReduce(t, ctx, rawTx, handler, trustedDomainAddedEvt)
res := callReduce(t, rawTx, handler, trustedDomainAddedEvt)
require.True(t, res)
// Verify
@@ -185,7 +185,7 @@ func TestInstanceDomainReducers(t *testing.T) {
trustedDomainRemovedEvt := instance.NewTrustedDomainRemovedEvent(ctx, &instance.NewAggregate("123").Aggregate, domainName)
// Test
res := callReduce(t, ctx, rawTx, handler, trustedDomainRemovedEvt)
res := callReduce(t, rawTx, handler, trustedDomainRemovedEvt)
require.True(t, res)
// Verify
@@ -377,6 +377,24 @@ func (p *relationalTablesProjection) Reducers() []handler.AggregateReducer {
Event: instance.IDPRemovedEventType,
Reduce: p.reduceIDPRemoved,
},
// Administrators
{
Event: instance.MemberAddedEventType,
Reduce: p.reduceInstanceAdminAdded,
},
{
Event: instance.MemberChangedEventType,
Reduce: p.reduceInstanceAdminChanged,
},
{
Event: instance.MemberRemovedEventType,
Reduce: p.reduceInstanceAdminRemoved,
},
{
Event: instance.MemberCascadeRemovedEventType,
Reduce: p.reduceInstanceAdminRemoved,
},
},
},
{
@@ -750,6 +768,24 @@ func (p *relationalTablesProjection) Reducers() []handler.AggregateReducer {
Event: org.IDPRemovedEventType,
Reduce: p.reduceIDPRemoved,
},
// Administrators
{
Event: org.MemberAddedEventType,
Reduce: p.reduceOrganizationAdminAdded,
},
{
Event: org.MemberChangedEventType,
Reduce: p.reduceOrganizationAdminChanged,
},
{
Event: org.MemberRemovedEventType,
Reduce: p.reduceOrganizationAdminRemoved,
},
{
Event: org.MemberCascadeRemovedEventType,
Reduce: p.reduceOrganizationAdminRemoved,
},
},
},
{
@@ -828,6 +864,40 @@ func (p *relationalTablesProjection) Reducers() []handler.AggregateReducer {
Event: project.RoleRemovedType,
Reduce: p.reduceProjectRoleRemoved,
},
// Administrators
{
Event: project.MemberAddedEventType,
Reduce: p.reduceProjectAdminAdded,
},
{
Event: project.MemberChangedEventType,
Reduce: p.reduceProjectAdminChanged,
},
{
Event: project.MemberRemovedEventType,
Reduce: p.reduceProjectAdminRemoved,
},
{
Event: project.MemberCascadeRemovedEventType,
Reduce: p.reduceProjectAdminRemoved,
},
{
Event: project.GrantMemberAddedType,
Reduce: p.reduceProjectGrantAdminAdded,
},
{
Event: project.GrantMemberChangedType,
Reduce: p.reduceProjectGrantAdminChanged,
},
{
Event: project.GrantMemberRemovedType,
Reduce: p.reduceProjectGrantAdminRemoved,
},
{
Event: project.GrantMemberCascadeRemovedType,
Reduce: p.reduceProjectGrantAdminRemoved,
},
},
},
{
@@ -1,7 +1,6 @@
package projection
import (
"context"
"testing"
"time"
@@ -23,13 +22,11 @@ func TestUserRelationalProjection_Reducers(t *testing.T) {
require.NoError(t, rawTx.Rollback())
})
ctx := t.Context()
// create instance
instanceRepo := repository.InstanceRepository()
instanceID := gofakeit.UUID()
orgID := gofakeit.UUID()
err := instanceRepo.Create(ctx, tx, &domain.Instance{
err := instanceRepo.Create(t.Context(), tx, &domain.Instance{
ID: instanceID,
Name: "test-instance",
DefaultOrgID: orgID,
@@ -38,7 +35,7 @@ func TestUserRelationalProjection_Reducers(t *testing.T) {
// create org
orgRepo := repository.OrganizationRepository()
err = orgRepo.Create(ctx, tx, &domain.Organization{
err = orgRepo.Create(t.Context(), tx, &domain.Organization{
InstanceID: instanceID,
ID: orgID,
Name: "test-org",
@@ -54,22 +51,22 @@ func TestUserRelationalProjection_Reducers(t *testing.T) {
t.Run("reduce user.human.mfa.recoverycode.added event", func(t *testing.T) {
// create user
existingUserID := gofakeit.UUID()
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID)
// set `user.human.mfa.recoverycode.added` event
recoveryCodes := []string{"code1", "code2", "code3"}
recoveryCodesAddedEvent := user.NewHumanRecoveryCodesAddedEvent(
ctx,
t.Context(),
&existingUserAgg.Aggregate,
recoveryCodes,
nil)
// reduce the event
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodesAddedEvent)
eventReduced := callReduce(t, rawTx, handler, recoveryCodesAddedEvent)
require.True(t, eventReduced)
// assert that the recovery codes are stored
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition(
database.And(
userRepo.IDCondition(existingUserID),
userRepo.InstanceIDCondition(instanceID),
@@ -84,26 +81,26 @@ func TestUserRelationalProjection_Reducers(t *testing.T) {
t.Run("reduce user.human.mfa.recoverycode.removed event", func(t *testing.T) {
// create user
existingUserID := gofakeit.UUID()
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID)
// add recovery codes to the user
_, err = userRepo.Update(ctx, tx,
_, err = userRepo.Update(t.Context(), tx,
userRepo.PrimaryKeyCondition(instanceID, existingUserID),
userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"}))
require.NoError(t, err)
// set `user.human.mfa.recoverycode.removed` event
recoveryCodesRemovedEvent := user.NewHumanRecoveryCodeRemovedEvent(
ctx,
t.Context(),
&existingUserAgg.Aggregate,
nil)
// reduce the event
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodesRemovedEvent)
eventReduced := callReduce(t, rawTx, handler, recoveryCodesRemovedEvent)
require.True(t, eventReduced)
// assert that the recovery codes are removed
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition(
database.And(
userRepo.IDCondition(existingUserID),
userRepo.InstanceIDCondition(instanceID),
@@ -118,27 +115,27 @@ func TestUserRelationalProjection_Reducers(t *testing.T) {
t.Run("reduce user.human.mfa.recoverycode.check.succeeded event", func(t *testing.T) {
// create user
existingUserID := gofakeit.UUID()
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID)
// add recovery codes to the user
_, err = userRepo.Update(ctx, tx,
_, err = userRepo.Update(t.Context(), tx,
userRepo.PrimaryKeyCondition(instanceID, existingUserID),
userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"}))
require.NoError(t, err)
// set `user.human.mfa.recoverycode.check.succeeded` event
recoveryCodeCheckSucceededEvent := user.NewHumanRecoveryCodeCheckSucceededEvent(
ctx,
t.Context(),
&existingUserAgg.Aggregate,
"code1",
nil)
// reduce the event
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodeCheckSucceededEvent)
eventReduced := callReduce(t, rawTx, handler, recoveryCodeCheckSucceededEvent)
require.True(t, eventReduced)
// assert that the recovery code is removed and the last_successfully_checked_at timestamp is set
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition(
database.And(
userRepo.IDCondition(existingUserID),
userRepo.InstanceIDCondition(instanceID),
@@ -153,27 +150,27 @@ func TestUserRelationalProjection_Reducers(t *testing.T) {
t.Run("reduce user.human.mfa.recoverycode.check.failed event", func(t *testing.T) {
// create user
existingUserID := gofakeit.UUID()
existingUserAgg := createUser(t, ctx, tx, userRepo, instanceID, orgID, existingUserID)
existingUserAgg := createUser(t, tx, userRepo, instanceID, orgID, existingUserID)
// add recovery codes to the user
_, err = userRepo.Update(ctx, tx,
_, err = userRepo.Update(t.Context(), tx,
userRepo.PrimaryKeyCondition(instanceID, existingUserID),
userRepo.Human().AddRecoveryCodes([]string{"code1", "code2", "code3"}))
require.NoError(t, err)
// set `user.human.mfa.recoverycode.check.failed` event
recoveryCodeCheckFailedEvent := user.NewHumanRecoveryCodeCheckFailedEvent(
ctx,
t.Context(),
&existingUserAgg.Aggregate,
nil,
)
// reduce the event
eventReduced := callReduce(t, ctx, rawTx, handler, recoveryCodeCheckFailedEvent)
eventReduced := callReduce(t, rawTx, handler, recoveryCodeCheckFailedEvent)
require.True(t, eventReduced)
// assert that the recovery code failed_attempts is incremented
gotUser, err := userRepo.Get(ctx, tx, database.WithCondition(
gotUser, err := userRepo.Get(t.Context(), tx, database.WithCondition(
database.And(
userRepo.IDCondition(existingUserID),
userRepo.InstanceIDCondition(instanceID),
@@ -187,7 +184,6 @@ func TestUserRelationalProjection_Reducers(t *testing.T) {
}
func createUser(t *testing.T,
ctx context.Context,
tx *sql.Transaction,
userRepo domain.UserRepository,
instanceID,
@@ -197,7 +193,7 @@ func createUser(t *testing.T,
userAgg := user.NewAggregate(userID, orgID)
userAgg.InstanceID = instanceID
err := userRepo.Create(ctx, tx, &domain.User{
err := userRepo.Create(t.Context(), tx, &domain.User{
InstanceID: instanceID,
OrganizationID: orgID,
ID: userID,