Files
zitadel/backend/v3/domain/administrator.go
T
f138082b64 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]>
2026-03-18 10:24:09 +01:00

94 lines
4.1 KiB
Go

package domain
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
//go:generate enumer -type AdministratorScope -transform snake -trimprefix AdministratorScope -sql
type AdministratorScope uint8
const (
AdministratorScopeInstance AdministratorScope = iota
AdministratorScopeOrganization
AdministratorScopeProject
AdministratorScopeProjectGrant
)
type Administrator struct {
// ID is generated by the storage layer during creation and any value
// provided by the caller is ignored. The domain package must not
// generate this value. The format is storage-specific, for example
// PostgreSQL uses `<user_id>:<scope>:<scope_id>`.
ID string `json:"id,omitempty" db:"id"`
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
UserID string `json:"userId,omitempty" db:"user_id"`
Scope AdministratorScope `json:"scope,omitempty" db:"scope"`
OrganizationID *string `json:"organizationId,omitempty" db:"organization_id"`
ProjectID *string `json:"projectId,omitempty" db:"project_id"`
ProjectGrantID *string `json:"projectGrantId,omitempty" db:"project_grant_id"`
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt,omitempty" db:"updated_at"`
Roles []string `json:"roles,omitempty" db:"roles"`
}
type administratorColumns interface {
PrimaryKeyColumns() []database.Column
IDColumn() database.Column
InstanceIDColumn() database.Column
UserIDColumn() database.Column
ScopeColumn() database.Column
OrganizationIDColumn() database.Column
ProjectIDColumn() database.Column
ProjectGrantIDColumn() database.Column
CreatedAtColumn() database.Column
UpdatedAtColumn() database.Column
}
type administratorConditions interface {
PrimaryKeyCondition(instanceID, administratorID string) database.Condition
IDCondition(administratorID string) database.Condition
InstanceIDCondition(instanceID string) database.Condition
UserIDCondition(userID string) database.Condition
ScopeCondition(scope AdministratorScope) database.Condition
InstanceAdministratorCondition(instanceID, userID string) database.Condition
OrganizationAdministratorCondition(instanceID, organizationID, userID string) database.Condition
ProjectAdministratorCondition(instanceID, projectID, userID string) database.Condition
ProjectGrantAdministratorCondition(instanceID, projectGrantID, userID string) database.Condition
OrganizationIDCondition(organizationID string) database.Condition
ProjectIDCondition(projectID string) database.Condition
ProjectGrantIDCondition(projectGrantID string) database.Condition
RoleCondition(op database.TextOperation, role string) database.Condition
}
type administratorChanges interface {
SetUpdatedAt(updatedAt time.Time) database.Change
// AddRole adds the specified role to the administrator.
// If the administrator already has the role, no change will be made.
AddRole(role string) database.Change
// RemoveRole removes the specified role from the administrator.
// If the administrator does not have the role, no change will be made.
RemoveRole(role string) database.Change
// SetRoles replaces all existing roles with the provided ones.
// If no roles are provided, all existing roles will be removed.
SetRoles(roles []string) database.Change
}
//go:generate mockgen -typed -package domainmock -destination ./mock/administrator.mock.go . AdministratorRepository
type AdministratorRepository interface {
Repository
administratorColumns
administratorConditions
administratorChanges
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*Administrator, error)
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*Administrator, error)
Create(ctx context.Context, client database.QueryExecutor, administrator *Administrator) error
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
}