mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-26 09:12:16 +00:00
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]>
306 lines
11 KiB
Go
306 lines
11 KiB
Go
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
|
|
}
|