feat(rt): project grants (#10951)

This pull request introduces a new feature that allows adding, updating,
and querying project grants The changes are primarily in the backend and
include new database tables, repositories, and domain logic to support
project grants.

## Changes

* New `project_grants` table: A new table `zitadel.project_grants` is
introduced to store project grants It includes columns for
`instance_id`, `id`, `state`, `project_id`, `granted_org_id` and
`granting_org_id`.
* New `ProjectGrantRepository`: A new repository
`ProjectGrantRepository` is created to handle all database operations
for project grants. It provides methods to `Get`, `List`, `Create`,
`Update`, and `Delete`.
* New `project_grant_roles` table: A new table
`zitadel.project_grant_roles` is introduced to store project grant roles
It includes columns for `instance_id`, `grant_id`, `key`, `project_id`
and `project_org_id`.
* New `ProjectGrantRoleRepository`: A new repository
`ProjectGrantRoleRepository` is created to handle all database
operations for project grant roles. It provides methods to `Get`,
`List`, `Add`, and `Delete`.
* New `project_grant_relational_projection`: A new projection
`project_grant_relational_projection` is added to update the
`zitadel.project_grants` and `zitadel.project_grant_roles` tables based
on events. It handles all project grant events.
* Updated `ProjectRepository`: The `ProjectRepository` is updated to
list granted projects.

# Additional Context

Closes https://github.com/zitadel/zitadel/issues/10766

---------

Co-authored-by: Stefan Benz <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Silvan <[email protected]>
This commit is contained in:
Tim Möhlmann
2025-11-14 11:04:16 +00:00
committed by GitHub
co-authored by Stefan Benz Marco A. Silvan
parent 6051ce591d
commit 9274e008fe
15 changed files with 1782 additions and 30 deletions
+9
View File
@@ -27,6 +27,11 @@ type Project struct {
IsAuthorizationRequired bool `json:"isAuthorizationRequired,omitempty" db:"is_authorization_required"`
IsProjectAccessRequired bool `json:"isProjectAccessRequired,omitempty" db:"is_project_access_required"`
UsedLabelingSettingOwner int16 `json:"usedLabelingSettingOwner,omitempty" db:"used_labeling_setting_owner"`
// Granted Project only filled in List
GrantID *string `json:"grantId,omitempty" db:"grant_id"`
GrantedOrganizationID *string `json:"grantedOrganizationId,omitempty" db:"granted_organization_id"`
GrantState *ProjectGrantState `json:"grantState,omitempty" db:"grant_state"`
}
type projectColumns interface {
@@ -34,6 +39,8 @@ type projectColumns interface {
InstanceIDColumn() database.Column
// OrganizationIDColumn returns the column for the organization id field
OrganizationIDColumn() database.Column
// GrantedOrganizationIDColumn returns the column for the granted organization id field
GrantedOrganizationIDColumn() database.Column
// IDColumn returns the column for the id field.
IDColumn() database.Column
// CreatedAtColumn returns the column for the created at field.
@@ -61,6 +68,8 @@ type projectConditions interface {
InstanceIDCondition(instanceID string) database.Condition
// OrganizationIDCondition returns a filter on the organization id field.
OrganizationIDCondition(organizationID string) database.Condition
// GrantedOrganizationIDCondition returns a filter on the granted organization id field.
GrantedOrganizationIDCondition(organizationID string) database.Condition
// IDCondition returns an equal filter on the id field.
IDCondition(projectID string) database.Condition
// NameCondition returns a filter on the name field.
+102
View File
@@ -0,0 +1,102 @@
package domain
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
//go:generate enumer -type ProjectGrantState -transform lower -trimprefix ProjectGrantState -sql
type ProjectGrantState uint8
const (
ProjectGrantStateActive ProjectGrantState = iota
ProjectGrantStateInactive
)
type ProjectGrant struct {
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
ID string `json:"id,omitempty" db:"id"`
ProjectID string `json:"projectId,omitempty" db:"project_id"`
GrantedOrganizationID string `json:"grantedOrganizationId,omitempty" db:"granted_organization_id"`
GrantingOrganizationID string `json:"grantingOrganizationId,omitempty" db:"granting_organization_id"`
CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"`
State ProjectGrantState `json:"state,omitempty" db:"state"`
RoleKeys []string `json:"roleKeys,omitempty" db:"role_keys"`
}
type projectGrantColumns interface {
// PrimaryKeyColumns returns the columns for the primary key fields
PrimaryKeyColumns() []database.Column
// InstanceIDColumn returns the column for the instance id field
InstanceIDColumn() database.Column
// IDColumn returns the column for the id field
IDColumn() database.Column
// ProjectIDColumn returns the column for the project id field
ProjectIDColumn() database.Column
// GrantingOrganizationIDColumn returns the column for the granting organization id field
GrantingOrganizationIDColumn() database.Column
// GrantedOrganizationIDColumn returns the column for the granted organization id field
GrantedOrganizationIDColumn() database.Column
// CreatedAtColumn returns the column for the created at field.
CreatedAtColumn() database.Column
// UpdatedAtColumn returns the column for the updated at field.
UpdatedAtColumn() database.Column
// StateColumn returns the column for the state field.
StateColumn() database.Column
}
type projectGrantConditions interface {
// PrimaryKeyCondition returns a filter on the primary key fields.
PrimaryKeyCondition(instanceID, id string) database.Condition
// InstanceIDCondition returns a filter on the instance id field.
InstanceIDCondition(instanceID string) database.Condition
// IDCondition returns a filter on the id field.
IDCondition(id string) database.Condition
// ProjectIDCondition returns a filter on the project id field.
ProjectIDCondition(projectID string) database.Condition
// GrantingOrganizationIDCondition returns a filter on the granting organization id field.
GrantingOrganizationIDCondition(grantingOrgID string) database.Condition
// GrantedOrganizationIDCondition returns a filter on the granted organization id field.
GrantedOrganizationIDCondition(grantedOrgID string) database.Condition
// StateCondition returns a filter on the state field.
StateCondition(state ProjectGrantState) database.Condition
// RoleKeyCondition returns a filter on the role field.
RoleKeyCondition(op database.TextOperation, role string) database.Condition
ExistsRoleKey(cond database.Condition) database.Condition
}
type projectGrantChanges interface {
// SetUpdatedAt sets the updated at column.
// Only use this when reducing events,
// during regular updates the DB sets this column automatically.
SetUpdatedAt(updatedAt time.Time) database.Change
// SetState sets the state column.
SetState(state ProjectGrantState) database.Change
}
// ProjectGrantRepository manages project grants.
//
//go:generate mockgen -typed -package domainmock -destination ./mock/project_grant.mock.go . ProjectGrantRepository
type ProjectGrantRepository interface {
Repository
projectGrantColumns
projectGrantConditions
projectGrantChanges
// Get a single project grant. An error is returned if not exactly one project is found.
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*ProjectGrant, error)
// List projects grant. An empty list is returned if no project grants are found.
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*ProjectGrant, error)
// Create a new project grant.
Create(ctx context.Context, client database.QueryExecutor, project *ProjectGrant) error
// Update an existing project grant.
// The condition must include the instanceID and ID of the project grant to update.
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, roleKeys []string, changes ...database.Change) (int64, error)
// Delete an existing project grant.
// The condition must include the instanceID and ID of the project grant to delete.
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
}
@@ -0,0 +1,109 @@
// Code generated by "enumer -type ProjectGrantState -transform lower -trimprefix ProjectGrantState -sql"; DO NOT EDIT.
package domain
import (
"database/sql/driver"
"fmt"
"strings"
)
const _ProjectGrantStateName = "activeinactive"
var _ProjectGrantStateIndex = [...]uint8{0, 6, 14}
const _ProjectGrantStateLowerName = "activeinactive"
func (i ProjectGrantState) String() string {
if i >= ProjectGrantState(len(_ProjectGrantStateIndex)-1) {
return fmt.Sprintf("ProjectGrantState(%d)", i)
}
return _ProjectGrantStateName[_ProjectGrantStateIndex[i]:_ProjectGrantStateIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _ProjectGrantStateNoOp() {
var x [1]struct{}
_ = x[ProjectGrantStateActive-(0)]
_ = x[ProjectGrantStateInactive-(1)]
}
var _ProjectGrantStateValues = []ProjectGrantState{ProjectGrantStateActive, ProjectGrantStateInactive}
var _ProjectGrantStateNameToValueMap = map[string]ProjectGrantState{
_ProjectGrantStateName[0:6]: ProjectGrantStateActive,
_ProjectGrantStateLowerName[0:6]: ProjectGrantStateActive,
_ProjectGrantStateName[6:14]: ProjectGrantStateInactive,
_ProjectGrantStateLowerName[6:14]: ProjectGrantStateInactive,
}
var _ProjectGrantStateNames = []string{
_ProjectGrantStateName[0:6],
_ProjectGrantStateName[6:14],
}
// ProjectGrantStateString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func ProjectGrantStateString(s string) (ProjectGrantState, error) {
if val, ok := _ProjectGrantStateNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _ProjectGrantStateNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to ProjectGrantState values", s)
}
// ProjectGrantStateValues returns all values of the enum
func ProjectGrantStateValues() []ProjectGrantState {
return _ProjectGrantStateValues
}
// ProjectGrantStateStrings returns a slice of all String values of the enum
func ProjectGrantStateStrings() []string {
strs := make([]string, len(_ProjectGrantStateNames))
copy(strs, _ProjectGrantStateNames)
return strs
}
// IsAProjectGrantState returns "true" if the value is listed in the enum definition. "false" otherwise
func (i ProjectGrantState) IsAProjectGrantState() bool {
for _, v := range _ProjectGrantStateValues {
if i == v {
return true
}
}
return false
}
func (i ProjectGrantState) Value() (driver.Value, error) {
return i.String(), nil
}
func (i *ProjectGrantState) Scan(value interface{}) error {
if value == nil {
return nil
}
var str string
switch v := value.(type) {
case []byte:
str = string(v)
case string:
str = v
case fmt.Stringer:
str = v.String()
default:
return fmt.Errorf("invalid value of ProjectGrantState: %[1]T(%[1]v)", value)
}
val, err := ProjectGrantStateString(str)
if err != nil {
return err
}
*i = val
return nil
}
@@ -0,0 +1,16 @@
package migration
import (
_ "embed"
)
var (
//go:embed 008_project_grants_table/up.sql
up008ProjectGrantsTable string
//go:embed 008_project_grants_table/down.sql
down008ProjectGrantsTable string
)
func init() {
registerSQLMigration(8, up008ProjectGrantsTable, down008ProjectGrantsTable)
}
@@ -0,0 +1,3 @@
DROP TYPE zitadel.project_grant_state;
DROP TABLE IF EXISTS zitadel.project_grant_roles;
DROP TABLE IF EXISTS zitadel.project_grants;
@@ -0,0 +1,45 @@
CREATE TYPE zitadel.project_grant_state AS ENUM (
'active',
'inactive'
);
CREATE TABLE zitadel.project_grants(
instance_id TEXT NOT NULL
, id TEXT NOT NULL CHECK (id <> '')
, granting_organization_id TEXT NOT NULL
, project_id TEXT NOT NULL
, granted_organization_id TEXT NOT NULL
, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
, state zitadel.project_grant_state NOT NULL
, PRIMARY KEY (instance_id, id)
, FOREIGN KEY (instance_id, granting_organization_id) REFERENCES zitadel.organizations(instance_id, id) ON DELETE CASCADE
, FOREIGN KEY (instance_id, granted_organization_id) REFERENCES zitadel.organizations(instance_id, id) ON DELETE CASCADE
, FOREIGN KEY (instance_id, project_id) REFERENCES zitadel.projects(instance_id, id) ON DELETE CASCADE
, UNIQUE (instance_id, project_id, granted_organization_id)
);
CREATE TRIGGER trg_set_updated_at_project_grants
BEFORE UPDATE ON zitadel.project_grants
FOR EACH ROW
WHEN (NEW.updated_at IS NULL)
EXECUTE FUNCTION zitadel.set_updated_at();
CREATE TABLE zitadel.project_grant_roles(
instance_id TEXT NOT NULL
, grant_id TEXT NOT NULL
, key TEXT NOT NULL CHECK (key <> '')
, project_id TEXT NOT NULL
, PRIMARY KEY (instance_id, grant_id, key)
, FOREIGN KEY (instance_id, grant_id) REFERENCES zitadel.project_grants(instance_id, id) ON DELETE CASCADE
, FOREIGN KEY (instance_id, project_id, key) REFERENCES zitadel.project_roles(instance_id, project_id, key) ON DELETE CASCADE
);
@@ -0,0 +1,154 @@
//go:build integration
package events_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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/integration"
org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
)
func TestServer_ProjectGrantReduces(t *testing.T) {
instanceID := Instance.ID()
orgID := Instance.DefaultOrg.Id
projectGrantRepo := repository.ProjectGrantRepository()
projectName := integration.ProjectName()
createProjectRes, err := ProjectClient.CreateProject(CTX, &v2beta_project.CreateProjectRequest{
OrganizationId: orgID,
Name: projectName,
ProjectRoleAssertion: true,
AuthorizationRequired: true,
ProjectAccessRequired: true,
})
require.NoError(t, err)
keyName := "key"
_, err = ProjectClient.AddProjectRole(CTX, &v2beta_project.AddProjectRoleRequest{
ProjectId: createProjectRes.GetId(),
RoleKey: keyName,
DisplayName: "display",
Group: nil,
})
require.NoError(t, err)
grantedOrgRes, err := OrgClient.CreateOrganization(CTX, &org.CreateOrganizationRequest{
Name: integration.OrganizationName(),
})
require.NoError(t, err)
_, err = ProjectClient.CreateProjectGrant(CTX, &v2beta_project.CreateProjectGrantRequest{
ProjectId: createProjectRes.GetId(),
GrantedOrganizationId: grantedOrgRes.GetId(),
})
require.NoError(t, err)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
t.Run("create project grant reduces", func(t *testing.T) {
require.EventuallyWithT(t, func(collect *assert.CollectT) {
dbProjectGrant, err := projectGrantRepo.Get(CTX, pool, database.WithCondition(
database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.ProjectIDCondition(createProjectRes.GetId()),
projectGrantRepo.GrantedOrganizationIDCondition(grantedOrgRes.GetId()),
),
))
require.NoError(collect, err)
assert.Equal(collect, createProjectRes.GetId(), dbProjectGrant.ProjectID)
assert.Equal(collect, orgID, dbProjectGrant.GrantingOrganizationID)
assert.Equal(collect, grantedOrgRes.GetId(), dbProjectGrant.GrantedOrganizationID)
assert.Equal(collect, domain.ProjectGrantStateActive, dbProjectGrant.State)
assert.NotNil(collect, dbProjectGrant.CreatedAt)
assert.NotNil(collect, dbProjectGrant.UpdatedAt)
}, retryDuration, tick, "project grant not found within %v: %v", retryDuration, err)
})
t.Run("update project grant reduces", func(t *testing.T) {
_, err := ProjectClient.UpdateProjectGrant(CTX, &v2beta_project.UpdateProjectGrantRequest{
ProjectId: createProjectRes.GetId(),
GrantedOrganizationId: grantedOrgRes.GetId(),
RoleKeys: []string{keyName},
})
require.NoError(t, err)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
dbProjectGrant, err := projectGrantRepo.Get(CTX, pool, database.WithCondition(
database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.ProjectIDCondition(createProjectRes.GetId()),
projectGrantRepo.GrantedOrganizationIDCondition(grantedOrgRes.GetId()),
),
))
require.NoError(collect, err)
if !assert.Len(collect, dbProjectGrant.RoleKeys, 1) {
return
}
assert.Equal(collect, dbProjectGrant.RoleKeys[0], keyName)
}, retryDuration, tick, "project grant not updated within %v: %v", retryDuration, err)
})
t.Run("(de)activate project grant reduces", func(t *testing.T) {
_, err := ProjectClient.DeactivateProjectGrant(CTX, &v2beta_project.DeactivateProjectGrantRequest{
ProjectId: createProjectRes.GetId(),
GrantedOrganizationId: grantedOrgRes.GetId(),
})
require.NoError(t, err)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
dbProject, err := projectGrantRepo.Get(CTX, pool, database.WithCondition(
database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.ProjectIDCondition(createProjectRes.GetId()),
projectGrantRepo.GrantedOrganizationIDCondition(grantedOrgRes.GetId()),
),
))
require.NoError(collect, err)
assert.Equal(collect, domain.ProjectGrantStateInactive, dbProject.State)
}, retryDuration, tick, "project grant not deactivated within %v: %v", retryDuration, err)
_, err = ProjectClient.ActivateProjectGrant(CTX, &v2beta_project.ActivateProjectGrantRequest{
ProjectId: createProjectRes.GetId(),
GrantedOrganizationId: grantedOrgRes.GetId(),
})
require.NoError(t, err)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
dbProject, err := projectGrantRepo.Get(CTX, pool, database.WithCondition(
database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.ProjectIDCondition(createProjectRes.GetId()),
projectGrantRepo.GrantedOrganizationIDCondition(grantedOrgRes.GetId()),
),
))
require.NoError(collect, err)
assert.Equal(collect, domain.ProjectGrantStateActive, dbProject.State)
}, retryDuration, tick, "project grant not activated within %v: %v", retryDuration, err)
})
t.Run("delete project grant reduces", func(t *testing.T) {
_, err := ProjectClient.DeleteProjectGrant(CTX, &v2beta_project.DeleteProjectGrantRequest{
ProjectId: createProjectRes.GetId(),
GrantedOrganizationId: grantedOrgRes.GetId(),
})
require.NoError(t, err)
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
_, err := projectGrantRepo.Get(CTX, pool, database.WithCondition(
database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.ProjectIDCondition(createProjectRes.GetId()),
projectGrantRepo.GrantedOrganizationIDCondition(grantedOrgRes.GetId()),
),
))
require.ErrorIs(collect, err, database.NewNoRowFoundError(nil))
}, retryDuration, tick, "project grant not deleted within %v: %v", retryDuration, err)
})
}
@@ -24,7 +24,7 @@ func (project) Role() domain.ProjectRoleRepository {
}
func (p project) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Project, error) {
builder, err := p.prepareQuery(opts)
builder, err := p.prepareGetQuery(opts)
if err != nil {
return nil, err
}
@@ -32,7 +32,7 @@ func (p project) Get(ctx context.Context, client database.QueryExecutor, opts ..
}
func (p project) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Project, error) {
builder, err := p.prepareQuery(opts)
builder, err := p.prepareListQuery(opts)
if err != nil {
return nil, err
}
@@ -62,31 +62,11 @@ func (project) Create(ctx context.Context, client database.QueryExecutor, projec
}
func (p project) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
if len(changes) == 0 {
return 0, database.ErrNoChanges
}
if err := checkPKCondition(p, condition); err != nil {
return 0, err
}
if !database.Changes(changes).IsOnColumn(p.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(p.UpdatedAtColumn(), database.NullInstruction))
}
builder := database.NewStatementBuilder(`UPDATE zitadel.projects SET `)
database.Changes(changes).Write(builder)
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
return updateOne(ctx, client, p, condition, changes...)
}
func (p project) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
if err := checkPKCondition(p, condition); err != nil {
return 0, err
}
builder := database.NewStatementBuilder(`DELETE FROM zitadel.projects `)
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
return deleteOne(ctx, client, p, condition)
}
// -------------------------------------------------------------
@@ -140,6 +120,10 @@ func (p project) OrganizationIDCondition(organizationID string) database.Conditi
return database.NewTextCondition(p.OrganizationIDColumn(), database.TextOperationEqual, organizationID)
}
func (p project) GrantedOrganizationIDCondition(organizationID string) database.Condition {
return database.NewTextCondition(p.GrantedOrganizationIDColumn(), database.TextOperationEqual, organizationID)
}
func (p project) IDCondition(projectID string) database.Condition {
return database.NewTextCondition(p.IDColumn(), database.TextOperationEqual, projectID)
}
@@ -156,6 +140,10 @@ func (p project) StateCondition(state domain.ProjectState) database.Condition {
// columns
// -------------------------------------------------------------
func (p project) qualifiedTableName() string {
return "zitadel." + p.unqualifiedTableName()
}
func (project) unqualifiedTableName() string {
return "projects"
}
@@ -176,6 +164,10 @@ func (p project) OrganizationIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "organization_id")
}
func (p project) GrantedOrganizationIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "granted_organization_id")
}
func (p project) IDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "id")
}
@@ -227,10 +219,13 @@ const queryProjectStmt = `SELECT
projects.should_assert_role,
projects.is_authorization_required,
projects.is_project_access_required,
projects.used_labeling_setting_owner
projects.used_labeling_setting_owner,
NULL::TEXT AS grant_id,
NULL::TEXT AS granted_organization_id,
NULL::zitadel.project_grant_state AS grant_state
FROM zitadel.projects`
func (p project) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) {
func (p project) prepareGetQuery(opts []database.QueryOption) (*database.StatementBuilder, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
@@ -243,3 +238,61 @@ func (p project) prepareQuery(opts []database.QueryOption) (*database.StatementB
return builder, nil
}
const queryGrantedProjectStmt = `SELECT
projects.instance_id,
projects.organization_id,
projects.id as id,
grants.created_at,
grants.updated_at,
projects.name,
projects.state,
projects.should_assert_role,
projects.is_authorization_required,
projects.is_project_access_required,
projects.used_labeling_setting_owner,
grants.id AS grant_id,
grants.granted_organization_id,
grants.state as grant_state
FROM zitadel.projects
INNER JOIN zitadel.project_grants as grants
ON grants.instance_id = projects.instance_id
AND grants.granting_organization_id = projects.organization_id
AND grants.project_id = projects.id`
const queryProjectsUnionStmt = `SELECT
projects.instance_id,
projects.organization_id,
projects.id as id,
projects.created_at,
projects.updated_at,
projects.name,
projects.state,
projects.should_assert_role,
projects.is_authorization_required,
projects.is_project_access_required,
projects.used_labeling_setting_owner,
projects.grant_id,
projects.granted_organization_id,
projects.grant_state
FROM (` +
queryProjectStmt +
` UNION ALL ` +
queryGrantedProjectStmt +
`) AS projects`
func (p project) prepareListQuery(opts []database.QueryOption) (*database.StatementBuilder, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
if err := checkRestrictingColumns(options.Condition, p.InstanceIDColumn()); err != nil {
return nil, err
}
builder := database.NewStatementBuilder(
queryProjectsUnionStmt,
)
options.Write(builder)
return builder, nil
}
@@ -0,0 +1,305 @@
package repository
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
type projectGrant struct{}
// ProjectGrantRepository manages project grants.
func ProjectGrantRepository() domain.ProjectGrantRepository {
return new(projectGrant)
}
func (p projectGrant) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.ProjectGrant, error) {
opts = append(opts,
database.WithGroupBy(p.InstanceIDColumn(), p.IDColumn()),
)
builder, err := p.prepareQuery(opts)
if err != nil {
return nil, err
}
return getOne[domain.ProjectGrant](ctx, client, builder)
}
func (p projectGrant) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.ProjectGrant, error) {
opts = append(opts,
database.WithGroupBy(p.InstanceIDColumn(), p.IDColumn()),
)
builder, err := p.prepareQuery(opts)
if err != nil {
return nil, err
}
return getMany[domain.ProjectGrant](ctx, client, builder)
}
const insertProjectGrantRolesStmt = `WITH added_roles AS (
INSERT INTO zitadel.project_grant_roles (
instance_id, grant_id, project_id, key
)
VALUES ($2, $3, $4, unnest($1::text[]))
) `
func (p projectGrant) Create(ctx context.Context, client database.QueryExecutor, projectGrant *domain.ProjectGrant) error {
var (
createdAt, updatedAt any = database.DefaultInstruction, database.DefaultInstruction
)
if !projectGrant.CreatedAt.IsZero() {
createdAt = projectGrant.CreatedAt
}
if !projectGrant.UpdatedAt.IsZero() {
updatedAt = projectGrant.UpdatedAt
}
// separate statement to add roles to project grant
builder := database.NewStatementBuilder(insertProjectGrantRolesStmt, projectGrant.RoleKeys)
builder.WriteString(`INSERT INTO ` + p.qualifiedTableName() + ` (instance_id, id, project_id, granting_organization_id, granted_organization_id, state, created_at, updated_at) VALUES ( `)
builder.WriteArgs(
projectGrant.InstanceID,
projectGrant.ID,
projectGrant.ProjectID,
projectGrant.GrantingOrganizationID,
projectGrant.GrantedOrganizationID,
projectGrant.State,
createdAt,
updatedAt,
)
builder.WriteString(` ) RETURNING created_at, updated_at`)
return client.QueryRow(ctx, builder.String(), builder.Args()...).
Scan(&projectGrant.CreatedAt, &projectGrant.UpdatedAt)
}
const queryUpdateProjectGrantRoleStmt = `SELECT instance_id, id, project_id, $1::text[] as keys from zitadel.project_grants`
const updateProjectGrantRoleStmt = `removed_roles AS (
DELETE FROM zitadel.project_grant_roles as pgr
USING pg
WHERE pg.instance_id = pgr.instance_id AND pg.id = pgr.grant_id AND NOT(pgr.key = ANY(pg.keys))
), added_roles AS (
INSERT INTO zitadel.project_grant_roles (
instance_id, grant_id, project_id, key
)
SELECT instance_id, id, project_id, unnest(pg.keys) FROM pg
ON CONFLICT (instance_id, grant_id, key) DO NOTHING
)
UPDATE zitadel.project_grants SET `
const updateProjectGrantRoleStmtWhere = ` FROM pg
WHERE pg.instance_id = project_grants.instance_id
AND pg.id = project_grants.id`
func (p projectGrant) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, roleKeys []string, changes ...database.Change) (int64, error) {
// if no role keys set we only have to update the project grant table
if roleKeys == nil {
return updateOne(ctx, client, p, condition, changes...)
}
// if you want to update the roles you have to have the primary key, otherwise multiple project grants get updated
if err := checkPKCondition(p, condition); err != nil {
return 0, err
}
if !database.Changes(changes).IsOnColumn(p.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(p.UpdatedAtColumn(), database.NullInstruction))
}
// the statement begins with getting the project grant we want to update
builder := database.NewStatementBuilder("WITH pg AS (")
builder.WriteString(queryUpdateProjectGrantRoleStmt)
builder.AppendArg(roleKeys)
writeCondition(builder, condition)
builder.WriteString(" ), ")
// now we add the logic to do the required changes based on the given project grant
builder.WriteString(updateProjectGrantRoleStmt)
database.Changes(changes).Write(builder)
builder.WriteString(updateProjectGrantRoleStmtWhere)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
func (p projectGrant) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
return deleteOne(ctx, client, p, condition)
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
func (p projectGrant) SetUpdatedAt(updatedAt time.Time) database.Change {
return database.NewChange(p.UpdatedAtColumn(), updatedAt)
}
func (p projectGrant) SetState(state domain.ProjectGrantState) database.Change {
return database.NewChange(p.StateColumn(), state)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
func (p projectGrant) PrimaryKeyCondition(instanceID, projectGrantID string) database.Condition {
return database.And(
p.InstanceIDCondition(instanceID),
p.IDCondition(projectGrantID),
)
}
func (p projectGrant) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(p.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
func (p projectGrant) OrganizationCondition(organizationID string) database.Condition {
return database.Or(
p.GrantingOrganizationIDCondition(organizationID),
p.GrantedOrganizationIDCondition(organizationID),
)
}
func (p projectGrant) IDCondition(projectGrantID string) database.Condition {
return database.NewTextCondition(p.IDColumn(), database.TextOperationEqual, projectGrantID)
}
func (p projectGrant) ProjectIDCondition(projectID string) database.Condition {
return database.NewTextCondition(p.ProjectIDColumn(), database.TextOperationEqual, projectID)
}
func (p projectGrant) GrantingOrganizationIDCondition(organizationID string) database.Condition {
return database.NewTextCondition(p.GrantingOrganizationIDColumn(), database.TextOperationEqual, organizationID)
}
func (p projectGrant) GrantedOrganizationIDCondition(organizationID string) database.Condition {
return database.NewTextCondition(p.GrantedOrganizationIDColumn(), database.TextOperationEqual, organizationID)
}
func (p projectGrant) StateCondition(state domain.ProjectGrantState) database.Condition {
return database.NewTextCondition(p.StateColumn(), database.TextOperationEqual, state.String())
}
func (p projectGrant) RoleKeyCondition(op database.TextOperation, role string) database.Condition {
return database.NewTextCondition(database.NewColumn(p.unqualifiedRolesTableName(), "key"), op, role)
}
// ExistsRoleKey creates a correlated [database.Exists] condition on project_grant_roles.
// Use this filter to make sure the project grant returned contains a specific project grant role.
// Example usage:
//
// projectGrant, _ := projectGrantRepo.Get(ctx,
// database.WithCondition(
// database.And(
// projectGrantRepo.InstanceIDCondition(instanceID),
// projectGrantRepo.ExistsRoleKey(projectGrantRepo.RoleKeyCondition(database.TextOperationEqual, "admin")),
// ),
// ),
// )
func (p projectGrant) ExistsRoleKey(cond database.Condition) database.Condition {
return database.Exists(
p.qualifiedRolesTableName(),
database.And(
database.NewColumnCondition(p.InstanceIDColumn(), database.NewColumn(p.unqualifiedRolesTableName(), "instance_id")),
database.NewColumnCondition(p.IDColumn(), database.NewColumn(p.unqualifiedRolesTableName(), "grant_id")),
cond,
),
)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
func (p projectGrant) qualifiedTableName() string {
return "zitadel." + p.unqualifiedTableName()
}
func (projectGrant) unqualifiedTableName() string {
return "project_grants"
}
func (p projectGrant) qualifiedRolesTableName() string {
return "zitadel." + p.unqualifiedRolesTableName()
}
func (projectGrant) unqualifiedRolesTableName() string {
return "project_grant_roles"
}
func (p projectGrant) PrimaryKeyColumns() []database.Column {
return []database.Column{
p.InstanceIDColumn(),
p.IDColumn(),
}
}
func (p projectGrant) InstanceIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "instance_id")
}
func (p projectGrant) IDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "id")
}
func (p projectGrant) ProjectIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "project_id")
}
func (p projectGrant) GrantingOrganizationIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "granting_organization_id")
}
func (p projectGrant) GrantedOrganizationIDColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "granted_organization_id")
}
func (p projectGrant) CreatedAtColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "created_at")
}
func (p projectGrant) UpdatedAtColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "updated_at")
}
func (p projectGrant) StateColumn() database.Column {
return database.NewColumn(p.unqualifiedTableName(), "state")
}
// -------------------------------------------------------------
// helpers
// -------------------------------------------------------------
const queryProjectGrantStmt = `SELECT
zitadel.project_grants.instance_id,
zitadel.project_grants.id,
zitadel.project_grants.project_id,
zitadel.project_grants.granting_organization_id,
zitadel.project_grants.granted_organization_id,
zitadel.project_grants.created_at,
zitadel.project_grants.updated_at,
zitadel.project_grants.state,
ARRAY_AGG(project_grant_roles.key) FILTER (WHERE project_grant_roles.grant_id IS NOT NULL) AS role_keys
FROM zitadel.project_grants
LEFT JOIN zitadel.project_grant_roles ON zitadel.project_grant_roles.instance_id = zitadel.project_grants.instance_id AND zitadel.project_grant_roles.grant_id = zitadel.project_grants.id`
func (p projectGrant) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
if err := checkRestrictingColumns(options.Condition, p.InstanceIDColumn()); err != nil {
return nil, err
}
builder := database.NewStatementBuilder(queryProjectGrantStmt)
options.Write(builder)
return builder, nil
}
@@ -0,0 +1,639 @@
package repository_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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/integration"
)
func TestGetProjectGrant(t *testing.T) {
tx, rollback := transactionForRollback(t)
defer rollback()
instanceID := createInstance(t, tx)
grantingOrgID := createOrganization(t, tx, instanceID)
firstGrantedOrgID := createOrganization(t, tx, instanceID)
secondGrantedOrgID := createOrganization(t, tx, instanceID)
projectID := createProject(t, tx, instanceID, grantingOrgID)
firstRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
secondRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
thirdRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
projectGrantRepo := repository.ProjectGrantRepository()
firstProjectGrant := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
GrantingOrganizationID: grantingOrgID,
ProjectID: projectID,
GrantedOrganizationID: firstGrantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey},
}
err := projectGrantRepo.Create(t.Context(), tx, firstProjectGrant)
require.NoError(t, err)
secondProjectGrant := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
GrantingOrganizationID: grantingOrgID,
ProjectID: projectID,
GrantedOrganizationID: secondGrantedOrgID,
State: domain.ProjectGrantStateInactive,
RoleKeys: []string{firstRoleKey, secondRoleKey, thirdRoleKey},
}
err = projectGrantRepo.Create(t.Context(), tx, secondProjectGrant)
require.NoError(t, err)
tests := []struct {
name string
condition database.Condition
want *domain.ProjectGrant
wantErr error
}{
{
name: "incomplete condition",
condition: projectGrantRepo.IDCondition(firstProjectGrant.ID),
wantErr: database.NewMissingConditionError(projectGrantRepo.IDColumn()),
},
{
name: "not found",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, "nix"),
wantErr: database.NewNoRowFoundError(nil),
},
{
name: "too many",
condition: projectGrantRepo.InstanceIDCondition(instanceID),
wantErr: database.NewMultipleRowsFoundError(nil),
},
{
name: "ok first",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, firstProjectGrant.ID),
want: firstProjectGrant,
},
{
name: "ok second",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, secondProjectGrant.ID),
want: secondProjectGrant,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := projectGrantRepo.Get(t.Context(), tx, database.WithCondition(tt.condition))
require.ErrorIs(t, err, tt.wantErr)
assertProjectGrant(t, tt.want, got)
})
}
}
func TestListProjectGrants(t *testing.T) {
tx, rollback := transactionForRollback(t)
defer rollback()
instanceID := createInstance(t, tx)
firstGrantingOrgID := createOrganization(t, tx, instanceID)
firstProjectID := createProject(t, tx, instanceID, firstGrantingOrgID)
firstRoleKey := createProjectRole(t, tx, instanceID, firstGrantingOrgID, firstProjectID, "")
secondRoleKey := createProjectRole(t, tx, instanceID, firstGrantingOrgID, firstProjectID, "")
thirdRoleKey := createProjectRole(t, tx, instanceID, firstGrantingOrgID, firstProjectID, "")
secondGrantingOrgID := createOrganization(t, tx, instanceID)
secondProjectID := createProject(t, tx, instanceID, secondGrantingOrgID)
firstGrantedOrgID := createOrganization(t, tx, instanceID)
secondGrantedOrgID := createOrganization(t, tx, instanceID)
projectGrantRepo := repository.ProjectGrantRepository()
projectGrants := [...]*domain.ProjectGrant{
{
InstanceID: instanceID,
ID: "1",
ProjectID: firstProjectID,
GrantedOrganizationID: firstGrantedOrgID,
GrantingOrganizationID: firstGrantingOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey},
},
{
InstanceID: instanceID,
ID: "2",
ProjectID: firstProjectID,
GrantedOrganizationID: secondGrantedOrgID,
GrantingOrganizationID: firstGrantingOrgID,
State: domain.ProjectGrantStateInactive,
RoleKeys: []string{secondRoleKey, thirdRoleKey},
},
{
InstanceID: instanceID,
ID: "3",
ProjectID: secondProjectID,
GrantedOrganizationID: firstGrantedOrgID,
GrantingOrganizationID: secondGrantingOrgID,
State: domain.ProjectGrantStateInactive,
},
{
InstanceID: instanceID,
ID: "4",
ProjectID: secondProjectID,
GrantedOrganizationID: secondGrantedOrgID,
GrantingOrganizationID: secondGrantingOrgID,
State: domain.ProjectGrantStateActive,
},
}
for _, projectGrant := range projectGrants {
err := projectGrantRepo.Create(t.Context(), tx, projectGrant)
require.NoError(t, err)
}
tests := []struct {
name string
condition database.Condition
want []*domain.ProjectGrant
wantErr error
}{
{
name: "incomplete condition",
condition: projectGrantRepo.GrantedOrganizationIDCondition(firstGrantedOrgID),
wantErr: database.NewMissingConditionError(projectGrantRepo.IDColumn()),
},
{
name: "no results, ok",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, "nix"),
},
{
name: "all from instance",
condition: projectGrantRepo.InstanceIDCondition(instanceID),
want: projectGrants[:],
},
{
name: "all granting from first org",
condition: database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.GrantingOrganizationIDCondition(firstGrantingOrgID),
),
want: projectGrants[0:2],
},
{
name: "all granting from second org",
condition: database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.GrantingOrganizationIDCondition(secondGrantingOrgID),
),
want: projectGrants[2:4],
},
{
name: "all granted from first org",
condition: database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.GrantingOrganizationIDCondition(firstGrantingOrgID),
),
want: []*domain.ProjectGrant{projectGrants[0], projectGrants[1]},
},
{
name: "all granted to second org",
condition: database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.GrantedOrganizationIDCondition(secondGrantedOrgID),
),
want: []*domain.ProjectGrant{projectGrants[1], projectGrants[3]},
},
{
name: "state active",
condition: database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.StateCondition(domain.ProjectGrantStateActive),
),
want: []*domain.ProjectGrant{projectGrants[0], projectGrants[3]},
},
{
name: "exists role key",
condition: database.And(
projectGrantRepo.InstanceIDCondition(instanceID),
projectGrantRepo.ExistsRoleKey(projectGrantRepo.RoleKeyCondition(database.TextOperationEqual, firstRoleKey)),
),
want: []*domain.ProjectGrant{projectGrants[0]},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := projectGrantRepo.List(t.Context(), tx,
database.WithCondition(tt.condition),
database.WithOrderByAscending(projectGrantRepo.PrimaryKeyColumns()...),
)
require.ErrorIs(t, err, tt.wantErr)
if !assert.Len(t, got, len(tt.want)) {
return
}
for i, want := range tt.want {
assertProjectGrant(t, want, got[i])
}
})
}
}
func TestCreateProjectGrant(t *testing.T) {
beforeCreate := time.Now()
tx, rollback := transactionForRollback(t)
defer rollback()
instanceID := createInstance(t, tx)
grantingOrgID := createOrganization(t, tx, instanceID)
firstGrantedOrgID := createOrganization(t, tx, instanceID)
secondGrantedOrgID := createOrganization(t, tx, instanceID)
thirdGrantedOrgID := createOrganization(t, tx, instanceID)
fourthGrantedOrgID := createOrganization(t, tx, instanceID)
projectID := createProject(t, tx, instanceID, grantingOrgID)
firstRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
secondRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
thirdRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
projectGrantRepo := repository.ProjectGrantRepository()
existingProjectGrant := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: firstGrantedOrgID,
State: domain.ProjectGrantStateActive,
}
err := projectGrantRepo.Create(t.Context(), tx, existingProjectGrant)
require.NoError(t, err)
tests := []struct {
name string
projectGrant *domain.ProjectGrant
wantErr error
}{
{
name: "add project grant",
projectGrant: &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: secondGrantedOrgID,
State: domain.ProjectGrantStateActive,
},
},
{
name: "non-existing instance",
projectGrant: &domain.ProjectGrant{
InstanceID: "foo",
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: secondGrantedOrgID,
State: domain.ProjectGrantStateActive,
},
wantErr: new(database.ForeignKeyError),
},
{
name: "non-existing org",
projectGrant: &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: "foo",
GrantedOrganizationID: secondGrantedOrgID,
State: domain.ProjectGrantStateActive,
},
wantErr: new(database.ForeignKeyError),
},
{
name: "empty id error",
projectGrant: &domain.ProjectGrant{
InstanceID: instanceID,
ID: "",
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: secondGrantedOrgID,
State: domain.ProjectGrantStateActive,
},
wantErr: new(database.CheckError),
},
{
name: "duplicate project grant",
projectGrant: existingProjectGrant,
wantErr: new(database.UniqueError),
},
{
name: "add project grant with role",
projectGrant: &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: thirdGrantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey},
},
},
{
name: "add project grant with multiple roles",
projectGrant: &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: fourthGrantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey, secondRoleKey, thirdRoleKey},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
savepoint, rollback := savepointForRollback(t, tx)
defer rollback()
err := projectGrantRepo.Create(t.Context(), savepoint, tt.projectGrant)
require.ErrorIs(t, err, tt.wantErr)
if err != nil {
return
}
afterCreate := time.Now()
// check instance values
projectGrant, err := projectGrantRepo.Get(t.Context(), tx,
database.WithCondition(
projectGrantRepo.PrimaryKeyCondition(tt.projectGrant.InstanceID, tt.projectGrant.ID),
),
)
require.NoError(t, err)
assertProjectGrant(t, tt.projectGrant, projectGrant)
assert.WithinRange(t, projectGrant.CreatedAt, beforeCreate, afterCreate)
assert.WithinRange(t, projectGrant.UpdatedAt, beforeCreate, afterCreate)
})
}
}
func assertProjectGrant(t assert.TestingT, expected *domain.ProjectGrant, actual *domain.ProjectGrant) {
if expected == nil {
assert.Nil(t, actual)
return
}
assert.Equal(t, expected.ID, actual.ID)
assert.Equal(t, expected.ProjectID, actual.ProjectID)
assert.Equal(t, expected.GrantingOrganizationID, actual.GrantingOrganizationID)
assert.Equal(t, expected.GrantedOrganizationID, actual.GrantedOrganizationID)
assert.Equal(t, expected.State, actual.State)
assert.ElementsMatch(t, expected.RoleKeys, actual.RoleKeys)
}
func TestUpdateProjectGrant(t *testing.T) {
tx, rollback := transactionForRollback(t)
defer rollback()
instanceID := createInstance(t, tx)
grantingOrgID := createOrganization(t, tx, instanceID)
firstGrantedOrgID := createOrganization(t, tx, instanceID)
secondGrantedOrgID := createOrganization(t, tx, instanceID)
thirdGrantedOrgID := createOrganization(t, tx, instanceID)
fourthGrantedOrgID := createOrganization(t, tx, instanceID)
fifthGrantedOrgID := createOrganization(t, tx, instanceID)
projectID := createProject(t, tx, instanceID, grantingOrgID)
firstRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
secondRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
thirdRoleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
projectGrantRepo := repository.ProjectGrantRepository()
existingProjectGrant := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: firstGrantedOrgID,
State: domain.ProjectGrantStateActive,
}
err := projectGrantRepo.Create(t.Context(), tx, existingProjectGrant)
require.NoError(t, err)
existingProjectGrantWithRole := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: secondGrantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey},
}
err = projectGrantRepo.Create(t.Context(), tx, existingProjectGrantWithRole)
require.NoError(t, err)
existingProjectGrantWithRoles := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: thirdGrantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey, secondRoleKey, thirdRoleKey},
}
err = projectGrantRepo.Create(t.Context(), tx, existingProjectGrantWithRoles)
require.NoError(t, err)
existingProjectGrantWithAllRoles := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: fourthGrantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey, secondRoleKey, thirdRoleKey},
}
err = projectGrantRepo.Create(t.Context(), tx, existingProjectGrantWithAllRoles)
require.NoError(t, err)
existingProjectGrantToRemoveRoles := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: fifthGrantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{firstRoleKey, secondRoleKey, thirdRoleKey},
}
err = projectGrantRepo.Create(t.Context(), tx, existingProjectGrantToRemoveRoles)
require.NoError(t, err)
tests := []struct {
name string
condition database.Condition
roleKeys []string
changes []database.Change
wantRowsAffected int64
wantErr error
assertChanges func(t *testing.T, project *domain.ProjectGrant)
}{
{
name: "no changes",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrant.ID),
changes: []database.Change{},
wantRowsAffected: 0,
wantErr: database.ErrNoChanges,
},
{
name: "incomplete condition",
condition: projectGrantRepo.InstanceIDCondition(instanceID),
changes: []database.Change{
projectGrantRepo.SetState(domain.ProjectGrantStateActive),
},
wantRowsAffected: 0,
wantErr: database.NewMissingConditionError(projectGrantRepo.IDColumn()),
},
{
name: "set state",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrant.ID),
changes: []database.Change{
projectGrantRepo.SetState(domain.ProjectGrantStateInactive),
},
wantRowsAffected: 1,
assertChanges: func(t *testing.T, updatedProject *domain.ProjectGrant) {
assert.Equal(t, domain.ProjectGrantStateInactive, updatedProject.State)
},
},
{
name: "incomplete condition for set rolekey",
condition: projectGrantRepo.InstanceIDCondition(instanceID),
roleKeys: []string{firstRoleKey},
changes: []database.Change{
projectGrantRepo.SetState(domain.ProjectGrantStateActive),
},
wantRowsAffected: 0,
wantErr: database.NewMissingConditionError(projectGrantRepo.IDColumn()),
},
{
name: "set rolekey",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrant.ID),
roleKeys: []string{firstRoleKey},
wantRowsAffected: 1,
assertChanges: func(t *testing.T, updatedProjectGrant *domain.ProjectGrant) {
assert.WithinRange(t, updatedProjectGrant.CreatedAt, existingProjectGrant.CreatedAt, existingProjectGrant.CreatedAt)
assert.WithinRange(t, updatedProjectGrant.UpdatedAt, existingProjectGrant.UpdatedAt, existingProjectGrant.UpdatedAt.Add(time.Second))
assert.ElementsMatch(t, []string{firstRoleKey}, updatedProjectGrant.RoleKeys)
},
},
{
name: "add rolekeys",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrantWithRole.ID),
roleKeys: []string{firstRoleKey, secondRoleKey, thirdRoleKey},
wantRowsAffected: 1,
assertChanges: func(t *testing.T, updatedProjectGrant *domain.ProjectGrant) {
assert.WithinRange(t, updatedProjectGrant.CreatedAt, existingProjectGrantWithRole.CreatedAt, existingProjectGrantWithRole.CreatedAt)
assert.WithinRange(t, updatedProjectGrant.UpdatedAt, existingProjectGrantWithRole.UpdatedAt, existingProjectGrantWithRole.UpdatedAt.Add(time.Second))
assert.ElementsMatch(t, []string{firstRoleKey, secondRoleKey, thirdRoleKey}, updatedProjectGrant.RoleKeys)
},
},
{
name: "remove rolekeys",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrantWithRoles.ID),
roleKeys: []string{firstRoleKey},
wantRowsAffected: 1,
assertChanges: func(t *testing.T, updatedProjectGrant *domain.ProjectGrant) {
assert.WithinRange(t, updatedProjectGrant.CreatedAt, existingProjectGrantWithRoles.CreatedAt, existingProjectGrantWithRoles.CreatedAt)
assert.WithinRange(t, updatedProjectGrant.UpdatedAt, existingProjectGrantWithRoles.UpdatedAt, existingProjectGrantWithRoles.UpdatedAt.Add(time.Second))
assert.ElementsMatch(t, []string{firstRoleKey}, updatedProjectGrant.RoleKeys)
},
},
{
name: "remove all rolekeys",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrantToRemoveRoles.ID),
roleKeys: []string{},
wantRowsAffected: 1,
assertChanges: func(t *testing.T, updatedProjectGrant *domain.ProjectGrant) {
assert.WithinRange(t, updatedProjectGrant.CreatedAt, existingProjectGrantToRemoveRoles.CreatedAt, existingProjectGrantToRemoveRoles.CreatedAt)
assert.WithinRange(t, updatedProjectGrant.UpdatedAt, existingProjectGrantToRemoveRoles.UpdatedAt, existingProjectGrantToRemoveRoles.UpdatedAt.Add(time.Second))
assert.ElementsMatch(t, []string{}, updatedProjectGrant.RoleKeys)
},
},
{
name: "no changes role keys",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrantWithAllRoles.ID),
roleKeys: []string{firstRoleKey, secondRoleKey, thirdRoleKey},
wantRowsAffected: 1,
assertChanges: func(t *testing.T, updatedProjectGrant *domain.ProjectGrant) {
assert.WithinRange(t, updatedProjectGrant.CreatedAt, existingProjectGrantWithAllRoles.CreatedAt, existingProjectGrantWithAllRoles.CreatedAt)
assert.WithinRange(t, updatedProjectGrant.UpdatedAt, existingProjectGrantWithAllRoles.UpdatedAt, existingProjectGrantWithAllRoles.UpdatedAt.Add(time.Second))
assert.ElementsMatch(t, []string{firstRoleKey, secondRoleKey, thirdRoleKey}, updatedProjectGrant.RoleKeys)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
savepoint, rollback := savepointForRollback(t, tx)
defer rollback()
rowsAffected, err := projectGrantRepo.Update(t.Context(), savepoint, tt.condition, tt.roleKeys, tt.changes...)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantRowsAffected, rowsAffected)
if tt.assertChanges != nil {
updatedProjectGrant, err := projectGrantRepo.Get(t.Context(), savepoint, database.WithCondition(tt.condition))
require.NoError(t, err)
tt.assertChanges(t, updatedProjectGrant)
}
})
}
}
func TestDeleteProjectGrant(t *testing.T) {
tx, rollback := transactionForRollback(t)
defer rollback()
instanceID := createInstance(t, tx)
grantingOrgID := createOrganization(t, tx, instanceID)
grantedOrgID := createOrganization(t, tx, instanceID)
projectID := createProject(t, tx, instanceID, grantingOrgID)
roleKey := createProjectRole(t, tx, instanceID, grantingOrgID, projectID, "")
projectGrantRepo := repository.ProjectGrantRepository()
existingProjectGrant := &domain.ProjectGrant{
InstanceID: instanceID,
ID: integration.ID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: grantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: []string{roleKey},
}
err := projectGrantRepo.Create(t.Context(), tx, existingProjectGrant)
require.NoError(t, err)
tests := []struct {
name string
condition database.Condition
wantRowsAffected int64
wantErr error
}{
{
name: "incomplete condition",
condition: projectGrantRepo.InstanceIDCondition(instanceID),
wantRowsAffected: 0,
wantErr: database.NewMissingConditionError(projectGrantRepo.IDColumn()),
},
{
name: "not found",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, "foo"),
wantRowsAffected: 0,
},
{
name: "delete project grant",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrant.ID),
wantRowsAffected: 1,
},
{
name: "delete project grant twice",
condition: projectGrantRepo.PrimaryKeyCondition(instanceID, existingProjectGrant.ID),
wantRowsAffected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rowsAffected, err := projectGrantRepo.Delete(t.Context(), tx, tt.condition)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantRowsAffected, rowsAffected)
})
}
}
@@ -5,6 +5,7 @@ import (
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -89,7 +90,10 @@ func TestListProjects(t *testing.T) {
instanceID := createInstance(t, tx)
firstOrgID := createOrganization(t, tx, instanceID)
secondOrgID := createOrganization(t, tx, instanceID)
thirdOrgID := createOrganization(t, tx, instanceID)
grantedOrgID := createOrganization(t, tx, instanceID)
projectRepo := repository.ProjectRepository()
projectGrantRepo := repository.ProjectGrantRepository()
projects := [...]*domain.Project{
{
@@ -136,10 +140,59 @@ func TestListProjects(t *testing.T) {
IsProjectAccessRequired: true,
UsedLabelingSettingOwner: 1,
},
{
InstanceID: instanceID,
OrganizationID: thirdOrgID,
ID: "5",
Name: "granted",
State: domain.ProjectStateActive,
ShouldAssertRole: true,
IsAuthorizationRequired: true,
IsProjectAccessRequired: true,
UsedLabelingSettingOwner: 1,
},
{
InstanceID: instanceID,
OrganizationID: thirdOrgID,
ID: "5",
Name: "granted",
State: domain.ProjectStateActive,
ShouldAssertRole: true,
IsAuthorizationRequired: true,
IsProjectAccessRequired: true,
UsedLabelingSettingOwner: 1,
GrantID: gu.Ptr("1"),
GrantedOrganizationID: gu.Ptr(grantedOrgID),
GrantState: gu.Ptr(domain.ProjectGrantStateActive),
},
{
InstanceID: instanceID,
OrganizationID: grantedOrgID,
ID: "6",
Name: "owned",
State: domain.ProjectStateActive,
ShouldAssertRole: true,
IsAuthorizationRequired: true,
IsProjectAccessRequired: true,
UsedLabelingSettingOwner: 1,
},
}
for _, project := range projects {
err := projectRepo.Create(t.Context(), tx, project)
require.NoError(t, err)
// Granted Projects
if project.GrantedOrganizationID != nil {
err := projectGrantRepo.Create(t.Context(), tx, &domain.ProjectGrant{
InstanceID: project.InstanceID,
ID: *project.GrantID,
ProjectID: project.ID,
GrantedOrganizationID: *project.GrantedOrganizationID,
GrantingOrganizationID: project.OrganizationID,
State: domain.ProjectGrantStateActive,
})
require.NoError(t, err)
} else {
err := projectRepo.Create(t.Context(), tx, project)
require.NoError(t, err)
}
}
tests := []struct {
@@ -179,13 +232,28 @@ func TestListProjects(t *testing.T) {
want: projects[1:3],
},
{
name: "state active",
name: "state active from first or second org",
condition: database.And(
projectRepo.InstanceIDCondition(instanceID),
database.Or(
projectRepo.OrganizationIDCondition(firstOrgID),
projectRepo.OrganizationIDCondition(secondOrgID),
),
projectRepo.StateCondition(domain.ProjectStateActive),
),
want: []*domain.Project{projects[0], projects[2]},
},
{
name: "all from granted org or granted to granted org",
condition: database.And(
projectRepo.InstanceIDCondition(instanceID),
database.Or(
projectRepo.OrganizationIDCondition(grantedOrgID),
projectRepo.GrantedOrganizationIDCondition(grantedOrgID),
),
),
want: []*domain.Project{projects[5], projects[6]},
},
}
for _, tt := range tests {
@@ -195,7 +263,7 @@ func TestListProjects(t *testing.T) {
database.WithOrderByAscending(projectRepo.PrimaryKeyColumns()...),
)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
assert.EqualExportedValues(t, tt.want, got)
})
}
}
@@ -65,3 +65,46 @@ func getMany[Target any](ctx context.Context, querier database.Querier, builder
}
return targets, nil
}
type updatable interface {
PrimaryKeyColumns() []database.Column
UpdatedAtColumn() database.Column
qualifiedTableName() string
}
func updateOne[Target updatable](ctx context.Context, client database.QueryExecutor, target Target, condition database.Condition, changes ...database.Change) (int64, error) {
if len(changes) == 0 {
return 0, database.ErrNoChanges
}
if err := checkPKCondition(target, condition); err != nil {
return 0, err
}
if !database.Changes(changes).IsOnColumn(target.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(target.UpdatedAtColumn(), database.NullInstruction))
}
builder := database.NewStatementBuilder("UPDATE ")
builder.WriteString(target.qualifiedTableName())
builder.WriteString(" SET ")
database.Changes(changes).Write(builder)
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
type deletable interface {
PrimaryKeyColumns() []database.Column
qualifiedTableName() string
}
func deleteOne[Target deletable](ctx context.Context, client database.QueryExecutor, target Target, condition database.Condition) (int64, error) {
if err := checkPKCondition(target, condition); err != nil {
return 0, err
}
builder := database.NewStatementBuilder("DELETE FROM ")
builder.WriteString(target.qualifiedTableName())
builder.WriteRune(' ')
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
@@ -14,6 +14,7 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres/embedded"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/integration"
)
func TestMain(m *testing.M) {
@@ -123,3 +124,22 @@ func createProject(t *testing.T, tx database.Transaction, instanceID, orgID stri
return project.ID
}
func createProjectRole(t *testing.T, tx database.Transaction, instanceID, orgID, projectID, key string) string {
t.Helper()
if key == "" {
key = integration.RoleKey()
}
projectRole := domain.ProjectRole{
InstanceID: instanceID,
OrganizationID: orgID,
ProjectID: projectID,
Key: key,
DisplayName: integration.RoleDisplayName(),
}
projectRoleRepo := repository.ProjectRepository().Role()
err := projectRoleRepo.Create(t.Context(), tx, &projectRole)
require.NoError(t, err)
return projectRole.Key
}
@@ -0,0 +1,183 @@
package projection
import (
"context"
"database/sql"
repoDomain "github.com/zitadel/zitadel/backend/v3/domain"
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/project"
"github.com/zitadel/zitadel/internal/zerrors"
)
type projectGrantRelationalProjection struct{}
func (*projectGrantRelationalProjection) Name() string {
return "zitadel.project_grants"
}
func newProjectGrantRelationalProjection(ctx context.Context, config handler.Config) *handler.Handler {
return handler.NewHandler(ctx, &config, new(projectGrantRelationalProjection))
}
func (p *projectGrantRelationalProjection) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: project.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: project.GrantAddedType,
Reduce: p.reduceProjectGrantAdded,
},
{
Event: project.GrantChangedType,
Reduce: p.reduceProjectGrantChanged,
},
{
Event: project.GrantCascadeChangedType,
Reduce: p.reduceProjectGrantCascadeChanged,
},
{
Event: project.GrantDeactivatedType,
Reduce: p.reduceProjectGrantDeactivated,
},
{
Event: project.GrantReactivatedType,
Reduce: p.reduceProjectGrantReactivated,
},
{
Event: project.GrantRemovedType,
Reduce: p.reduceProjectGrantRemoved,
},
},
},
}
}
func (p *projectGrantRelationalProjection) reduceProjectGrantAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*project.GrantAddedEvent)
if !ok {
return nil, zerrors.ThrowInternalf(nil, "HANDL-5l2bWQrkKf", "reduce.wrong.event.type %s", project.GrantAddedType)
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-5w96sjaQ16", "reduce.wrong.db.pool %T", ex)
}
repo := repository.ProjectGrantRepository()
return repo.Create(ctx, v3_sql.SQLTx(tx), &repoDomain.ProjectGrant{
InstanceID: e.Aggregate().InstanceID,
ID: e.GrantID,
ProjectID: e.Aggregate().ID,
GrantedOrganizationID: e.GrantedOrgID,
GrantingOrganizationID: e.Aggregate().ResourceOwner,
CreatedAt: e.CreationDate(),
UpdatedAt: e.CreationDate(),
State: repoDomain.ProjectGrantStateActive,
RoleKeys: e.RoleKeys,
})
}), nil
}
func (p *projectGrantRelationalProjection) reduceProjectGrantChanged(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*project.GrantChangedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-I2ciunHcy7", "reduce.wrong.event.type %s", project.GrantChangedType)
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-ANaKzWKAUc", "reduce.wrong.db.pool %T", ex)
}
repo := repository.ProjectGrantRepository()
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.GrantID),
e.RoleKeys,
repo.SetUpdatedAt(e.CreationDate()),
)
return err
}), nil
}
func (p *projectGrantRelationalProjection) reduceProjectGrantCascadeChanged(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*project.GrantCascadeChangedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-osXtu6jnWa", "reduce.wrong.event.type %s", project.GrantCascadeChangedType)
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-aI2o6NlWpv", "reduce.wrong.db.pool %T", ex)
}
repo := repository.ProjectGrantRepository()
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.GrantID),
e.RoleKeys,
repo.SetUpdatedAt(e.CreationDate()),
)
return err
}), nil
}
func (p *projectGrantRelationalProjection) reduceProjectGrantDeactivated(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*project.GrantDeactivateEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-XraL17MUkr", "reduce.wrong.event.type %s", project.GrantDeactivatedType)
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
repo := repository.ProjectGrantRepository()
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-rIjAqCzj67", "reduce.wrong.db.pool %T", ex)
}
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.GrantID),
nil,
repo.SetUpdatedAt(e.CreationDate()),
repo.SetState(repoDomain.ProjectGrantStateInactive),
)
return err
}), nil
}
func (p *projectGrantRelationalProjection) reduceProjectGrantReactivated(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*project.GrantReactivatedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-vGUHp6uHJ7", "reduce.wrong.event.type %s", project.GrantReactivatedType)
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
repo := repository.ProjectGrantRepository()
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-8milgIP7BS", "reduce.wrong.db.pool %T", ex)
}
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.GrantID),
nil,
repo.SetUpdatedAt(e.CreationDate()),
repo.SetState(repoDomain.ProjectGrantStateActive),
)
return err
}), nil
}
func (p *projectGrantRelationalProjection) reduceProjectGrantRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*project.GrantRemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-MVPgtdg1w5", "reduce.wrong.event.type %s", project.GrantRemovedType)
}
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
repo := repository.ProjectGrantRepository()
tx, ok := ex.(*sql.Tx)
if !ok {
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-gYyqxBv5d0", "reduce.wrong.db.pool %T", ex)
}
_, err := repo.Delete(ctx, v3_sql.SQLTx(tx),
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.GrantID),
)
return err
}), nil
}
+3
View File
@@ -97,6 +97,7 @@ var (
ProjectRelationalProjection *handler.Handler
ProjectRoleRelationalProjection *handler.Handler
OrganizationMetadataRelationalProjection *handler.Handler
ProjectGrantRelationalProjection *handler.Handler
ProjectGrantFields *handler.FieldHandler
OrgDomainVerifiedFields *handler.FieldHandler
@@ -222,6 +223,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
ProjectRelationalProjection = newProjectRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects_relational"]))
ProjectRoleRelationalProjection = newProjectRoleRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_roles_relational"]))
OrganizationMetadataRelationalProjection = newOrgMetadataRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organization_metadata_relational"]))
ProjectGrantRelationalProjection = newProjectGrantRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_grant_relational"]))
newProjectionsList()
newFieldsList()
@@ -415,5 +417,6 @@ func newProjectionsList() {
ProjectRelationalProjection,
ProjectRoleRelationalProjection,
OrganizationMetadataRelationalProjection,
ProjectGrantRelationalProjection,
}
}