mirror of
https://github.com/zitadel/zitadel.git
synced 2025-11-02 03:38:46 +00:00
feat(rt): project repository (#10789)
# Which Problems Are Solved Add projects to the relational tables # How the Problems Are Solved - Define table migrations - Define and implement Project and Project Role repositories. - Provide projection handlers to populate the relational tables. # Additional Changes - Statement Builder now has a constructor which allows setting of a base query with arguments. - Certain operations, like Get, Update and Delete require the Primary Key to be set as conditions. However, this requires knowledge of the implementation and table definition. This PR proposes an additional condition for repositories: `PrimaryKeyCondition`. This gives clarity on the required IDs for these operations. - Added couple of helpers to the repository package, to allow more DRY code. - getOne / getMany: generic functions for query execution and scanning. - checkRestrictingColumns, checkPkCondition: simplify condition checking, instead of using ladders of conditionals. - Added a couple of helpers to the repository test package: - Transaction, savepoint and rollback helpers. - Create instance and organization helpers for objects that depend on them (like projects). # Additional Context - after https://github.com/zitadel/zitadel/pull/10809 - closes #10765
This commit is contained in:
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
||||
httphelper "github.com/zitadel/oidc/v3/pkg/http"
|
||||
|
||||
191
backend/v3/domain/project.go
Normal file
191
backend/v3/domain/project.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
//go:generate enumer -type ProjectState -transform lower -trimprefix ProjectState -sql
|
||||
type ProjectState uint8
|
||||
|
||||
const (
|
||||
ProjectStateActive ProjectState = iota
|
||||
ProjectStateInactive
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
|
||||
OrganizationID string `json:"organizationId,omitempty" db:"organization_id"`
|
||||
ID string `json:"id,omitempty" db:"id"`
|
||||
CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"`
|
||||
Name string `json:"name,omitempty" db:"name"`
|
||||
State ProjectState `json:"state,omitempty" db:"state"`
|
||||
ShouldAssertRole bool `json:"shouldAssertRole,omitempty" db:"should_assert_role"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type projectColumns 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
|
||||
// OrganizationIDColumn returns the column for the organization id field
|
||||
OrganizationIDColumn() database.Column
|
||||
// IDColumn returns the column for the id field.
|
||||
IDColumn() 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
|
||||
// NameColumn returns the column for the name field.
|
||||
NameColumn() database.Column
|
||||
// StateColumn returns the column for the state field.
|
||||
StateColumn() database.Column
|
||||
// ShouldAssertRoleColumn returns the column for the should assert role field.
|
||||
ShouldAssertRoleColumn() database.Column
|
||||
// IsAuthorizationRequiredColumn returns the column for the is authorization required field.
|
||||
IsAuthorizationRequiredColumn() database.Column
|
||||
// IsProjectAccessRequiredColumn returns the column for the is project access required field.
|
||||
IsProjectAccessRequiredColumn() database.Column
|
||||
// UsedLabelingSettingOwnerColumn returns the column for the used labeling setting owner field.
|
||||
UsedLabelingSettingOwnerColumn() database.Column
|
||||
}
|
||||
|
||||
type projectConditions interface {
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(instanceID, projectID string) database.Condition
|
||||
// InstanceIDCondition returns a filter on the instance id field.
|
||||
InstanceIDCondition(instanceID string) database.Condition
|
||||
// OrganizationIDCondition returns a filter on the organization id field.
|
||||
OrganizationIDCondition(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.
|
||||
NameCondition(op database.TextOperation, name string) database.Condition
|
||||
// StateCondition returns a filter on the state field.
|
||||
StateCondition(state ProjectState) database.Condition
|
||||
}
|
||||
|
||||
type projectChanges 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
|
||||
// SetName sets the name column.
|
||||
SetName(name string) database.Change
|
||||
// SetState sets the state column.
|
||||
SetState(state ProjectState) database.Change
|
||||
// SetShouldAssertRole sets the should assert role column.
|
||||
SetShouldAssertRole(shouldAssertRole bool) database.Change
|
||||
// SetIsAuthorizationRequired sets the is authorization required column.
|
||||
SetIsAuthorizationRequired(isAuthorizationRequired bool) database.Change
|
||||
// SetIsProjectAccessRequired sets the is project access required column.
|
||||
SetIsProjectAccessRequired(isProjectAccessRequired bool) database.Change
|
||||
// SetUsedLabelingSettingOwner sets the used labeling setting owner column.
|
||||
SetUsedLabelingSettingOwner(usedLabelingSettingOwner int16) database.Change
|
||||
}
|
||||
|
||||
// ProjectRepository manages projects and project roles.
|
||||
type ProjectRepository interface {
|
||||
projectColumns
|
||||
projectConditions
|
||||
projectChanges
|
||||
|
||||
// Get a single project. An error is returned if not exactly one project is found.
|
||||
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*Project, error)
|
||||
// List projects. An empty list is returned if no projects are found.
|
||||
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*Project, error)
|
||||
// Create a new project.
|
||||
Create(ctx context.Context, client database.QueryExecutor, project *Project) error
|
||||
// Update an existing project.
|
||||
// The condition must include the instanceID and ID of the project to update.
|
||||
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
|
||||
// Delete an existing project.
|
||||
// The condition must include the instanceID and ID of the project to delete.
|
||||
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
|
||||
|
||||
// Role returns the sub-repository for project roles.
|
||||
Role() ProjectRoleRepository
|
||||
}
|
||||
|
||||
type ProjectRole struct {
|
||||
InstanceID string `json:"instanceId,omitempty" db:"instance_id"`
|
||||
OrganizationID string `json:"organizationId,omitempty" db:"organization_id"`
|
||||
ProjectID string `json:"projectId,omitempty" db:"project_id"`
|
||||
CreatedAt time.Time `json:"createdAt,omitzero" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt,omitzero" db:"updated_at"`
|
||||
Key string `json:"key,omitempty" db:"key"`
|
||||
DisplayName string `json:"displayName,omitempty" db:"display_name"`
|
||||
RoleGroup *string `json:"roleGroup,omitempty" db:"role_group"`
|
||||
}
|
||||
|
||||
type projectRoleColumns 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
|
||||
// OrganizationIDColumn returns the column for the organization id field
|
||||
OrganizationIDColumn() database.Column
|
||||
// ProjectIDColumn returns the column for the project id field
|
||||
ProjectIDColumn() 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
|
||||
// KeyColumn returns the column for the key field.
|
||||
KeyColumn() database.Column
|
||||
// DisplayNameColumn returns the column for the display name field.
|
||||
DisplayNameColumn() database.Column
|
||||
// RoleGroupColumn returns the column for the role group field.
|
||||
RoleGroupColumn() database.Column
|
||||
}
|
||||
|
||||
type projectRoleConditions interface {
|
||||
// PrimaryKeyCondition returns a filter on the primary key fields.
|
||||
PrimaryKeyCondition(instanceID, projectID, key string) database.Condition
|
||||
// InstanceIDCondition returns an equal filter on the instance id field.
|
||||
InstanceIDCondition(instanceID string) database.Condition
|
||||
// ProjectIDCondition returns an equal filter on the project id field.
|
||||
ProjectIDCondition(projectID string) database.Condition
|
||||
// KeyCondition returns an equal filter on the key field.
|
||||
KeyCondition(key string) database.Condition
|
||||
// DisplayNameCondition returns a filter on the display name field.
|
||||
DisplayNameCondition(op database.TextOperation, displayName string) database.Condition
|
||||
// RoleGroupCondition returns a filter on the role group field.
|
||||
RoleGroupCondition(op database.TextOperation, roleGroup string) database.Condition
|
||||
}
|
||||
|
||||
type projectRoleChanges 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
|
||||
// SetDisplayName sets the display name column.
|
||||
SetDisplayName(displayName string) database.Change
|
||||
// SetRoleGroup sets the role group column.
|
||||
SetRoleGroup(roleGroup string) database.Change
|
||||
}
|
||||
|
||||
type ProjectRoleRepository interface {
|
||||
projectRoleColumns
|
||||
projectRoleConditions
|
||||
projectRoleChanges
|
||||
|
||||
// Get a single project role. An error is returned if not exactly one project role is found.
|
||||
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*ProjectRole, error)
|
||||
// List project roles. An empty list is returned if no project roles are found.
|
||||
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*ProjectRole, error)
|
||||
// Create a new project role.
|
||||
Create(ctx context.Context, client database.QueryExecutor, role *ProjectRole) error
|
||||
// Update an existing project role.
|
||||
// The condition must include the instanceID, projectID and key of the project role to update.
|
||||
Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error)
|
||||
// Delete an existing project role.
|
||||
// The condition must include the instanceID, projectID and key of the project role to delete.
|
||||
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error)
|
||||
}
|
||||
109
backend/v3/domain/projectstate_enumer.go
Normal file
109
backend/v3/domain/projectstate_enumer.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Code generated by "enumer -type ProjectState -transform lower -trimprefix ProjectState -sql"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _ProjectStateName = "activeinactive"
|
||||
|
||||
var _ProjectStateIndex = [...]uint8{0, 6, 14}
|
||||
|
||||
const _ProjectStateLowerName = "activeinactive"
|
||||
|
||||
func (i ProjectState) String() string {
|
||||
if i >= ProjectState(len(_ProjectStateIndex)-1) {
|
||||
return fmt.Sprintf("ProjectState(%d)", i)
|
||||
}
|
||||
return _ProjectStateName[_ProjectStateIndex[i]:_ProjectStateIndex[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 _ProjectStateNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[ProjectStateActive-(0)]
|
||||
_ = x[ProjectStateInactive-(1)]
|
||||
}
|
||||
|
||||
var _ProjectStateValues = []ProjectState{ProjectStateActive, ProjectStateInactive}
|
||||
|
||||
var _ProjectStateNameToValueMap = map[string]ProjectState{
|
||||
_ProjectStateName[0:6]: ProjectStateActive,
|
||||
_ProjectStateLowerName[0:6]: ProjectStateActive,
|
||||
_ProjectStateName[6:14]: ProjectStateInactive,
|
||||
_ProjectStateLowerName[6:14]: ProjectStateInactive,
|
||||
}
|
||||
|
||||
var _ProjectStateNames = []string{
|
||||
_ProjectStateName[0:6],
|
||||
_ProjectStateName[6:14],
|
||||
}
|
||||
|
||||
// ProjectStateString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func ProjectStateString(s string) (ProjectState, error) {
|
||||
if val, ok := _ProjectStateNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _ProjectStateNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to ProjectState values", s)
|
||||
}
|
||||
|
||||
// ProjectStateValues returns all values of the enum
|
||||
func ProjectStateValues() []ProjectState {
|
||||
return _ProjectStateValues
|
||||
}
|
||||
|
||||
// ProjectStateStrings returns a slice of all String values of the enum
|
||||
func ProjectStateStrings() []string {
|
||||
strs := make([]string, len(_ProjectStateNames))
|
||||
copy(strs, _ProjectStateNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAProjectState returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i ProjectState) IsAProjectState() bool {
|
||||
for _, v := range _ProjectStateValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i ProjectState) Value() (driver.Value, error) {
|
||||
return i.String(), nil
|
||||
}
|
||||
|
||||
func (i *ProjectState) 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 ProjectState: %[1]T(%[1]v)", value)
|
||||
}
|
||||
|
||||
val, err := ProjectStateString(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i = val
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package migration
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed 006_projects_table/up.sql
|
||||
up006ProjectsTable string
|
||||
//go:embed 006_projects_table/down.sql
|
||||
down006ProjectsTable string
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerSQLMigration(6, up006ProjectsTable, down006ProjectsTable)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP TRIGGER trigger_set_updated_at ON zitadel.project_roles;
|
||||
DROP TRIGGER trigger_set_updated_at ON zitadel.projects;
|
||||
DROP TABLE zitadel.project_roles;
|
||||
DROP TABLE zitadel.projects;
|
||||
DROP TYPE zitadel.project_state;
|
||||
@@ -0,0 +1,61 @@
|
||||
CREATE TYPE zitadel.project_state AS ENUM (
|
||||
'active',
|
||||
'inactive'
|
||||
);
|
||||
|
||||
CREATE TABLE zitadel.projects(
|
||||
instance_id TEXT NOT NULL
|
||||
, organization_id TEXT NOT NULL
|
||||
, id TEXT NOT NULL CHECK (id <> '')
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
|
||||
, name TEXT NOT NULL CHECK (name <> '')
|
||||
, state zitadel.project_state NOT NULL
|
||||
-- API: project_role_assertion
|
||||
, should_assert_role BOOLEAN NOT NULL DEFAULT FALSE
|
||||
-- API: authorization_required
|
||||
, is_authorization_required BOOLEAN NOT NULL DEFAULT FALSE
|
||||
-- API: project_access_required
|
||||
, is_project_access_required BOOLEAN NOT NULL DEFAULT FALSE
|
||||
--API: private_labeling_setting
|
||||
, used_labeling_setting_owner SMALLINT
|
||||
|
||||
, PRIMARY KEY (instance_id, id)
|
||||
, UNIQUE (instance_id, organization_id, id)
|
||||
, FOREIGN KEY (instance_id, organization_id) REFERENCES zitadel.organizations(instance_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE zitadel.project_roles(
|
||||
instance_id TEXT NOT NULL
|
||||
, organization_id TEXT NOT NULL
|
||||
, project_id TEXT NOT NULL
|
||||
|
||||
, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
|
||||
-- API: role_key
|
||||
, key TEXT NOT NULL CHECK (key <> '')
|
||||
-- API: display_name
|
||||
, display_name TEXT NOT NULL CHECK (display_name <> '')
|
||||
-- API: group
|
||||
-- group is a reserved keyword in PostgreSQL
|
||||
, role_group TEXT
|
||||
|
||||
, PRIMARY KEY (instance_id, project_id, key)
|
||||
, UNIQUE (instance_id, organization_id, project_id, key)
|
||||
, FOREIGN KEY (instance_id, organization_id, project_id) REFERENCES zitadel.projects(instance_id, organization_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TRIGGER trigger_set_updated_at
|
||||
BEFORE UPDATE ON zitadel.projects
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.updated_at IS NULL)
|
||||
EXECUTE FUNCTION zitadel.set_updated_at();
|
||||
|
||||
CREATE TRIGGER trigger_set_updated_at
|
||||
BEFORE UPDATE ON zitadel.project_roles
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.updated_at IS NULL)
|
||||
EXECUTE FUNCTION zitadel.set_updated_at();
|
||||
@@ -19,20 +19,22 @@ import (
|
||||
v2beta "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
|
||||
v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/system"
|
||||
)
|
||||
|
||||
const ConnString = "host=localhost port=5432 user=zitadel password=zitadel dbname=zitadel sslmode=disable"
|
||||
|
||||
var (
|
||||
dbPool *pgxpool.Pool
|
||||
CTX context.Context
|
||||
IAMCTX context.Context
|
||||
Instance *integration.Instance
|
||||
SystemClient system.SystemServiceClient
|
||||
OrgClient v2beta_org.OrganizationServiceClient
|
||||
AdminClient admin.AdminServiceClient
|
||||
MgmtClient mgmt.ManagementServiceClient
|
||||
dbPool *pgxpool.Pool
|
||||
CTX context.Context
|
||||
IAMCTX context.Context
|
||||
Instance *integration.Instance
|
||||
SystemClient system.SystemServiceClient
|
||||
OrgClient v2beta_org.OrganizationServiceClient
|
||||
ProjectClient v2beta_project.ProjectServiceClient
|
||||
AdminClient admin.AdminServiceClient
|
||||
MgmtClient mgmt.ManagementServiceClient
|
||||
)
|
||||
|
||||
var pool database.Pool
|
||||
@@ -48,6 +50,7 @@ func TestMain(m *testing.M) {
|
||||
IAMCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner)
|
||||
SystemClient = integration.SystemClient()
|
||||
OrgClient = Instance.Client.OrgV2beta
|
||||
ProjectClient = Instance.Client.Projectv2Beta
|
||||
AdminClient = Instance.Client.Admin
|
||||
MgmtClient = Instance.Client.Mgmt
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
//go:build integration
|
||||
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_ProjectRoleReduces(t *testing.T) {
|
||||
instanceID := Instance.ID()
|
||||
orgID := Instance.DefaultOrg.Id
|
||||
roleRepo := repository.ProjectRepository().Role()
|
||||
|
||||
projectRes, err := ProjectClient.CreateProject(CTX, &v2beta_project.CreateProjectRequest{
|
||||
OrganizationId: orgID,
|
||||
Name: integration.ProjectName(),
|
||||
ProjectRoleAssertion: true,
|
||||
AuthorizationRequired: true,
|
||||
ProjectAccessRequired: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
projectID := projectRes.GetId()
|
||||
_, err = ProjectClient.AddProjectRole(CTX, &v2beta_project.AddProjectRoleRequest{
|
||||
ProjectId: projectID,
|
||||
RoleKey: "key",
|
||||
DisplayName: "display name",
|
||||
Group: proto.String("group"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
|
||||
t.Run("add project role reduces", func(t *testing.T) {
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbRole, err := roleRepo.Get(CTX, pool, database.WithCondition(
|
||||
roleRepo.PrimaryKeyCondition(instanceID, projectID, "key"),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
require.NotNil(collect, dbRole)
|
||||
assert.Equal(collect, projectID, dbRole.ProjectID)
|
||||
assert.Equal(collect, "key", dbRole.Key)
|
||||
assert.Equal(collect, "display name", dbRole.DisplayName)
|
||||
assert.Equal(collect, gu.Ptr("group"), dbRole.RoleGroup)
|
||||
assert.NotNil(collect, dbRole.CreatedAt)
|
||||
assert.NotNil(collect, dbRole.UpdatedAt)
|
||||
}, retryDuration, tick, "project role not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("update project role reduces", func(t *testing.T) {
|
||||
_, err := ProjectClient.UpdateProjectRole(CTX, &v2beta_project.UpdateProjectRoleRequest{
|
||||
ProjectId: projectID,
|
||||
RoleKey: "key",
|
||||
DisplayName: proto.String("new display name"),
|
||||
Group: proto.String("new group"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbRole, err := roleRepo.Get(CTX, pool, database.WithCondition(
|
||||
roleRepo.PrimaryKeyCondition(instanceID, projectID, "key"),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
require.NotNil(collect, dbRole)
|
||||
assert.Equal(collect, "new display name", dbRole.DisplayName)
|
||||
assert.Equal(collect, gu.Ptr("new group"), dbRole.RoleGroup)
|
||||
}, retryDuration, tick, "project role not updated within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("remove project role reduces", func(t *testing.T) {
|
||||
_, err := ProjectClient.RemoveProjectRole(CTX, &v2beta_project.RemoveProjectRoleRequest{
|
||||
ProjectId: projectID,
|
||||
RoleKey: "key",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
_, err := roleRepo.Get(CTX, pool, database.WithCondition(
|
||||
roleRepo.PrimaryKeyCondition(instanceID, projectID, "key"),
|
||||
))
|
||||
require.ErrorIs(collect, err, database.NewNoRowFoundError(nil))
|
||||
}, retryDuration, tick, "project role not deleted within %v: %v", retryDuration, err)
|
||||
})
|
||||
}
|
||||
120
backend/v3/storage/database/events_testing/project_test.go
Normal file
120
backend/v3/storage/database/events_testing/project_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
//go:build integration
|
||||
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"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"
|
||||
v2beta_project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
|
||||
)
|
||||
|
||||
func TestServer_ProjectReduces(t *testing.T) {
|
||||
instanceID := Instance.ID()
|
||||
orgID := Instance.DefaultOrg.Id
|
||||
projectRepo := repository.ProjectRepository()
|
||||
|
||||
projectName := integration.ProjectName()
|
||||
createRes, err := ProjectClient.CreateProject(CTX, &v2beta_project.CreateProjectRequest{
|
||||
OrganizationId: orgID,
|
||||
Name: projectName,
|
||||
ProjectRoleAssertion: true,
|
||||
AuthorizationRequired: true,
|
||||
ProjectAccessRequired: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
|
||||
t.Run("create project reduces", func(t *testing.T) {
|
||||
require.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbProject, err := projectRepo.Get(CTX, pool, database.WithCondition(
|
||||
projectRepo.PrimaryKeyCondition(instanceID, createRes.GetId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assert.Equal(collect, createRes.GetId(), dbProject.ID)
|
||||
assert.Equal(collect, orgID, dbProject.OrganizationID)
|
||||
assert.Equal(collect, projectName, dbProject.Name)
|
||||
assert.Equal(collect, domain.ProjectStateActive, dbProject.State)
|
||||
assert.True(collect, dbProject.ShouldAssertRole)
|
||||
assert.True(collect, dbProject.IsAuthorizationRequired)
|
||||
assert.True(collect, dbProject.IsProjectAccessRequired)
|
||||
assert.NotNil(collect, dbProject.CreatedAt)
|
||||
assert.NotNil(collect, dbProject.UpdatedAt)
|
||||
}, retryDuration, tick, "project not found within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("update project reduces", func(t *testing.T) {
|
||||
_, err := ProjectClient.UpdateProject(CTX, &v2beta_project.UpdateProjectRequest{
|
||||
Id: createRes.GetId(),
|
||||
Name: gu.Ptr("new name"),
|
||||
ProjectRoleAssertion: gu.Ptr(false),
|
||||
ProjectRoleCheck: gu.Ptr(false),
|
||||
HasProjectCheck: gu.Ptr(false),
|
||||
PrivateLabelingSetting: gu.Ptr(v2beta_project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbProject, err := projectRepo.Get(CTX, pool, database.WithCondition(
|
||||
projectRepo.PrimaryKeyCondition(instanceID, createRes.GetId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assert.Equal(collect, "new name", dbProject.Name)
|
||||
assert.False(collect, dbProject.ShouldAssertRole)
|
||||
assert.False(collect, dbProject.IsAuthorizationRequired)
|
||||
assert.False(collect, dbProject.IsProjectAccessRequired)
|
||||
assert.Equal(collect, int16(2), dbProject.UsedLabelingSettingOwner)
|
||||
}, retryDuration, tick, "project not updated within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("(de)activate project reduces", func(t *testing.T) {
|
||||
_, err := ProjectClient.DeactivateProject(CTX, &v2beta_project.DeactivateProjectRequest{
|
||||
Id: createRes.GetId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbProject, err := projectRepo.Get(CTX, pool, database.WithCondition(
|
||||
projectRepo.PrimaryKeyCondition(instanceID, createRes.GetId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assert.Equal(collect, domain.ProjectStateInactive, dbProject.State)
|
||||
}, retryDuration, tick, "project not deactivated within %v: %v", retryDuration, err)
|
||||
|
||||
_, err = ProjectClient.ActivateProject(CTX, &v2beta_project.ActivateProjectRequest{
|
||||
Id: createRes.GetId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
dbProject, err := projectRepo.Get(CTX, pool, database.WithCondition(
|
||||
projectRepo.PrimaryKeyCondition(instanceID, createRes.GetId()),
|
||||
))
|
||||
require.NoError(collect, err)
|
||||
assert.Equal(collect, domain.ProjectStateActive, dbProject.State)
|
||||
}, retryDuration, tick, "project not activated within %v: %v", retryDuration, err)
|
||||
})
|
||||
|
||||
t.Run("delete project reduces", func(t *testing.T) {
|
||||
_, err := ProjectClient.DeleteProject(CTX, &v2beta_project.DeleteProjectRequest{
|
||||
Id: createRes.GetId(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EventuallyWithT(t, func(collect *assert.CollectT) {
|
||||
_, err := projectRepo.Get(CTX, pool, database.WithCondition(
|
||||
projectRepo.PrimaryKeyCondition(instanceID, createRes.GetId()),
|
||||
))
|
||||
require.ErrorIs(collect, err, database.NewNoRowFoundError(nil))
|
||||
}, retryDuration, tick, "project not deleted within %v: %v", retryDuration, err)
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
// Package repository implements the repositories defined in the domain package.
|
||||
// The repositories are used by the domain package to access the database.
|
||||
// the inheritance.sql file is me over-engineering table inheritance.
|
||||
//
|
||||
// Repository methods may require a minimal set of columns in the [database.Condition] to be passed.
|
||||
// This is to ensure that the repository methods are used correctly and do not affect more rows than intended.
|
||||
// For example:
|
||||
// - The instance ID is almost always required to ensure that only rows of the correct instance are affected.
|
||||
// - Update and Delete methods often require the columns from the primary key to ensure that only one row is affected.
|
||||
// The required columns are checked during the call and an error is returned if they are not present.
|
||||
|
||||
// the inheritance.sql file is me(silvan) over-engineering table inheritance.
|
||||
// I would create a user table which is inherited by human_user and machine_user and the same for objects like idps.
|
||||
package repository
|
||||
|
||||
245
backend/v3/storage/database/repository/project.go
Normal file
245
backend/v3/storage/database/repository/project.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// repository
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type project struct{}
|
||||
|
||||
// ProjectRepository manages projects and project roles.
|
||||
func ProjectRepository() domain.ProjectRepository {
|
||||
return project{}
|
||||
}
|
||||
|
||||
func (project) Role() domain.ProjectRoleRepository {
|
||||
return projectRole{}
|
||||
}
|
||||
|
||||
func (p project) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Project, error) {
|
||||
builder, err := p.prepareQuery(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getOne[domain.Project](ctx, client, builder)
|
||||
}
|
||||
|
||||
func (p project) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Project, error) {
|
||||
builder, err := p.prepareQuery(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getMany[domain.Project](ctx, client, builder)
|
||||
}
|
||||
|
||||
const insertProjectStmt = `INSERT INTO zitadel.projects(
|
||||
instance_id, organization_id, id, name, state, should_assert_role, is_authorization_required, is_project_access_required, used_labeling_setting_owner
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING created_at, updated_at`
|
||||
|
||||
func (project) Create(ctx context.Context, client database.QueryExecutor, project *domain.Project) error {
|
||||
builder := database.NewStatementBuilder(insertProjectStmt,
|
||||
project.InstanceID,
|
||||
project.OrganizationID,
|
||||
project.ID,
|
||||
project.Name,
|
||||
project.State,
|
||||
project.ShouldAssertRole,
|
||||
project.IsAuthorizationRequired,
|
||||
project.IsProjectAccessRequired,
|
||||
project.UsedLabelingSettingOwner,
|
||||
)
|
||||
return client.QueryRow(ctx, builder.String(), builder.Args()...).
|
||||
Scan(&project.CreatedAt, &project.UpdatedAt)
|
||||
}
|
||||
|
||||
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()...)
|
||||
}
|
||||
|
||||
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()...)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (p project) SetUpdatedAt(updatedAt time.Time) database.Change {
|
||||
return database.NewChange(p.UpdatedAtColumn(), updatedAt)
|
||||
}
|
||||
|
||||
func (p project) SetName(name string) database.Change {
|
||||
return database.NewChange(p.NameColumn(), name)
|
||||
}
|
||||
|
||||
func (p project) SetState(state domain.ProjectState) database.Change {
|
||||
return database.NewChange(p.StateColumn(), state)
|
||||
}
|
||||
|
||||
func (p project) SetShouldAssertRole(shouldAssertRole bool) database.Change {
|
||||
return database.NewChange(p.ShouldAssertRoleColumn(), shouldAssertRole)
|
||||
}
|
||||
|
||||
func (p project) SetIsAuthorizationRequired(isAuthorizationRequired bool) database.Change {
|
||||
return database.NewChange(p.IsAuthorizationRequiredColumn(), isAuthorizationRequired)
|
||||
}
|
||||
|
||||
func (p project) SetIsProjectAccessRequired(isProjectAccessRequired bool) database.Change {
|
||||
return database.NewChange(p.IsProjectAccessRequiredColumn(), isProjectAccessRequired)
|
||||
}
|
||||
|
||||
func (p project) SetUsedLabelingSettingOwner(usedLabelingSettingOwner int16) database.Change {
|
||||
return database.NewChange(p.UsedLabelingSettingOwnerColumn(), usedLabelingSettingOwner)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (p project) PrimaryKeyCondition(instanceID, projectID string) database.Condition {
|
||||
return database.And(
|
||||
p.InstanceIDCondition(instanceID),
|
||||
p.IDCondition(projectID),
|
||||
)
|
||||
}
|
||||
|
||||
func (p project) InstanceIDCondition(instanceID string) database.Condition {
|
||||
return database.NewTextCondition(p.InstanceIDColumn(), database.TextOperationEqual, instanceID)
|
||||
}
|
||||
|
||||
func (p project) OrganizationIDCondition(organizationID string) database.Condition {
|
||||
return database.NewTextCondition(p.OrganizationIDColumn(), database.TextOperationEqual, organizationID)
|
||||
}
|
||||
|
||||
func (p project) IDCondition(projectID string) database.Condition {
|
||||
return database.NewTextCondition(p.IDColumn(), database.TextOperationEqual, projectID)
|
||||
}
|
||||
|
||||
func (p project) NameCondition(op database.TextOperation, name string) database.Condition {
|
||||
return database.NewTextCondition(p.NameColumn(), op, name)
|
||||
}
|
||||
|
||||
func (p project) StateCondition(state domain.ProjectState) database.Condition {
|
||||
return database.NewTextCondition(p.StateColumn(), database.TextOperationEqual, state.String())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (project) unqualifiedTableName() string {
|
||||
return "projects"
|
||||
}
|
||||
|
||||
// PrimaryKeyColumns implements the [pkRepository] interface
|
||||
func (p project) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
p.InstanceIDColumn(),
|
||||
p.IDColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p project) InstanceIDColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (p project) OrganizationIDColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "organization_id")
|
||||
}
|
||||
|
||||
func (p project) IDColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "id")
|
||||
}
|
||||
|
||||
func (p project) CreatedAtColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "created_at")
|
||||
}
|
||||
|
||||
func (p project) UpdatedAtColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "updated_at")
|
||||
}
|
||||
|
||||
func (p project) NameColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "name")
|
||||
}
|
||||
|
||||
func (p project) StateColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "state")
|
||||
}
|
||||
|
||||
func (p project) ShouldAssertRoleColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "should_assert_role")
|
||||
}
|
||||
|
||||
func (p project) IsAuthorizationRequiredColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "is_authorization_required")
|
||||
}
|
||||
|
||||
func (p project) IsProjectAccessRequiredColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "is_project_access_required")
|
||||
}
|
||||
|
||||
func (p project) UsedLabelingSettingOwnerColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "used_labeling_setting_owner")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// helpers
|
||||
// -------------------------------------------------------------
|
||||
|
||||
const queryProjectStmt = `SELECT
|
||||
projects.instance_id,
|
||||
projects.organization_id,
|
||||
projects.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
|
||||
FROM zitadel.projects`
|
||||
|
||||
func (p project) 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(queryProjectStmt)
|
||||
options.Write(builder)
|
||||
|
||||
return builder, nil
|
||||
}
|
||||
203
backend/v3/storage/database/repository/project_role.go
Normal file
203
backend/v3/storage/database/repository/project_role.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// repository
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type projectRole struct{}
|
||||
|
||||
func (p projectRole) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.ProjectRole, error) {
|
||||
builder, err := p.prepareQuery(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getOne[domain.ProjectRole](ctx, client, builder)
|
||||
}
|
||||
|
||||
func (p projectRole) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.ProjectRole, error) {
|
||||
builder, err := p.prepareQuery(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getMany[domain.ProjectRole](ctx, client, builder)
|
||||
}
|
||||
|
||||
const insertProjectRoleStmt = `INSERT INTO zitadel.project_roles(
|
||||
instance_id, organization_id, project_id, key, display_name, role_group
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING created_at, updated_at`
|
||||
|
||||
func (p projectRole) Create(ctx context.Context, client database.QueryExecutor, role *domain.ProjectRole) error {
|
||||
builder := database.NewStatementBuilder(insertProjectRoleStmt,
|
||||
role.InstanceID,
|
||||
role.OrganizationID,
|
||||
role.ProjectID,
|
||||
role.Key,
|
||||
role.DisplayName,
|
||||
role.RoleGroup,
|
||||
)
|
||||
return client.QueryRow(ctx, builder.String(), builder.Args()...).
|
||||
Scan(&role.CreatedAt, &role.UpdatedAt)
|
||||
}
|
||||
|
||||
func (p projectRole) 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.project_roles SET `)
|
||||
database.Changes(changes).Write(builder)
|
||||
writeCondition(builder, condition)
|
||||
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
}
|
||||
|
||||
func (p projectRole) 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.project_roles`)
|
||||
writeCondition(builder, condition)
|
||||
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (p projectRole) SetUpdatedAt(updatedAt time.Time) database.Change {
|
||||
return database.NewChange(p.UpdatedAtColumn(), updatedAt)
|
||||
}
|
||||
|
||||
func (p projectRole) SetDisplayName(displayName string) database.Change {
|
||||
return database.NewChange(p.DisplayNameColumn(), displayName)
|
||||
}
|
||||
|
||||
func (p projectRole) SetRoleGroup(roleGroup string) database.Change {
|
||||
return database.NewChange(p.RoleGroupColumn(), roleGroup)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (p projectRole) PrimaryKeyCondition(instanceID, projectID, key string) database.Condition {
|
||||
return database.And(
|
||||
p.InstanceIDCondition(instanceID),
|
||||
p.ProjectIDCondition(projectID),
|
||||
p.KeyCondition(key),
|
||||
)
|
||||
}
|
||||
|
||||
func (p projectRole) InstanceIDCondition(instanceID string) database.Condition {
|
||||
return database.NewTextCondition(p.InstanceIDColumn(), database.TextOperationEqual, instanceID)
|
||||
}
|
||||
|
||||
func (p projectRole) ProjectIDCondition(projectID string) database.Condition {
|
||||
return database.NewTextCondition(p.ProjectIDColumn(), database.TextOperationEqual, projectID)
|
||||
}
|
||||
|
||||
func (p projectRole) KeyCondition(key string) database.Condition {
|
||||
return database.NewTextCondition(p.KeyColumn(), database.TextOperationEqual, key)
|
||||
}
|
||||
|
||||
func (p projectRole) DisplayNameCondition(op database.TextOperation, displayName string) database.Condition {
|
||||
return database.NewTextCondition(p.DisplayNameColumn(), op, displayName)
|
||||
}
|
||||
|
||||
func (p projectRole) RoleGroupCondition(op database.TextOperation, roleGroup string) database.Condition {
|
||||
return database.NewTextCondition(p.RoleGroupColumn(), op, roleGroup)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (projectRole) unqualifiedTableName() string {
|
||||
return "project_roles"
|
||||
}
|
||||
|
||||
// PrimaryKeyColumns implements the [pkRepository] interface
|
||||
func (p projectRole) PrimaryKeyColumns() []database.Column {
|
||||
return []database.Column{
|
||||
p.InstanceIDColumn(),
|
||||
p.ProjectIDColumn(),
|
||||
p.KeyColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p projectRole) InstanceIDColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (p projectRole) OrganizationIDColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "organization_id")
|
||||
}
|
||||
|
||||
func (p projectRole) ProjectIDColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "project_id")
|
||||
}
|
||||
|
||||
func (p projectRole) CreatedAtColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "created_at")
|
||||
}
|
||||
|
||||
func (p projectRole) UpdatedAtColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "updated_at")
|
||||
}
|
||||
|
||||
func (p projectRole) KeyColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "key")
|
||||
}
|
||||
|
||||
func (p projectRole) DisplayNameColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "display_name")
|
||||
}
|
||||
|
||||
func (p projectRole) RoleGroupColumn() database.Column {
|
||||
return database.NewColumn(p.unqualifiedTableName(), "role_group")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// helpers
|
||||
// -------------------------------------------------------------
|
||||
|
||||
const queryProjectRoleStmt = `SELECT
|
||||
project_roles.instance_id,
|
||||
project_roles.organization_id,
|
||||
project_roles.project_id,
|
||||
project_roles.created_at,
|
||||
project_roles.updated_at,
|
||||
project_roles.key,
|
||||
project_roles.display_name,
|
||||
project_roles.role_group
|
||||
FROM zitadel.project_roles`
|
||||
|
||||
func (p projectRole) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) {
|
||||
options := new(database.QueryOpts)
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
if err := checkRestrictingColumns(options.Condition, p.InstanceIDColumn(), p.ProjectIDColumn()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
builder := database.NewStatementBuilder(queryProjectRoleStmt)
|
||||
options.Write(builder)
|
||||
|
||||
return builder, nil
|
||||
}
|
||||
467
backend/v3/storage/database/repository/project_role_test.go
Normal file
467
backend/v3/storage/database/repository/project_role_test.go
Normal file
@@ -0,0 +1,467 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestGetProjectRole(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectID := createProject(t, tx, instanceID, orgID)
|
||||
roleRepo := repository.ProjectRepository().Role()
|
||||
|
||||
firstRole := &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key1",
|
||||
DisplayName: "Role 1",
|
||||
RoleGroup: gu.Ptr("group1"),
|
||||
}
|
||||
err := roleRepo.Create(t.Context(), tx, firstRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
secondRole := &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key2",
|
||||
DisplayName: "Role 2",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
}
|
||||
err = roleRepo.Create(t.Context(), tx, secondRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
wantRole *domain.ProjectRole
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: roleRepo.KeyCondition(firstRole.Key),
|
||||
wantErr: database.NewMissingConditionError(roleRepo.InstanceIDColumn()),
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
condition: roleRepo.PrimaryKeyCondition(instanceID, projectID, "foo"),
|
||||
wantErr: database.NewNoRowFoundError(nil),
|
||||
},
|
||||
{
|
||||
name: "too many",
|
||||
condition: database.And(roleRepo.InstanceIDCondition(instanceID), roleRepo.ProjectIDCondition(projectID)),
|
||||
wantErr: database.NewMultipleRowsFoundError(nil),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
condition: roleRepo.PrimaryKeyCondition(instanceID, projectID, firstRole.Key),
|
||||
wantRole: firstRole,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotRole, err := roleRepo.Get(t.Context(), tx, database.WithCondition(tt.condition))
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.wantRole, gotRole)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProjectRoles(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
firstProjectID := createProject(t, tx, instanceID, orgID)
|
||||
secondProjectID := createProject(t, tx, instanceID, orgID)
|
||||
roleRepo := repository.ProjectRepository().Role()
|
||||
|
||||
roles := [...]*domain.ProjectRole{
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: firstProjectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key1",
|
||||
DisplayName: "Role 1",
|
||||
RoleGroup: gu.Ptr("group1"),
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: firstProjectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key2",
|
||||
DisplayName: "Role 2",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: secondProjectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key3",
|
||||
DisplayName: "Role 3",
|
||||
RoleGroup: gu.Ptr("group3"),
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: secondProjectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key4",
|
||||
DisplayName: "foobar",
|
||||
RoleGroup: gu.Ptr("group3"),
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: secondProjectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key5",
|
||||
DisplayName: "foobaz",
|
||||
RoleGroup: gu.Ptr("group4"),
|
||||
},
|
||||
}
|
||||
for _, r := range roles {
|
||||
err := roleRepo.Create(t.Context(), tx, r)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
wantRoles []*domain.ProjectRole
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: roleRepo.KeyCondition("key1"),
|
||||
wantErr: database.NewMissingConditionError(roleRepo.InstanceIDColumn()),
|
||||
},
|
||||
{
|
||||
name: "no results, ok",
|
||||
condition: roleRepo.PrimaryKeyCondition(instanceID, firstProjectID, "foo"),
|
||||
},
|
||||
{
|
||||
name: "all from project 1",
|
||||
condition: database.And(
|
||||
roleRepo.InstanceIDCondition(instanceID),
|
||||
roleRepo.ProjectIDCondition(firstProjectID),
|
||||
),
|
||||
wantRoles: roles[0:2],
|
||||
},
|
||||
{
|
||||
name: "group 3 from project 2",
|
||||
condition: database.And(
|
||||
roleRepo.InstanceIDCondition(instanceID),
|
||||
roleRepo.ProjectIDCondition(secondProjectID),
|
||||
roleRepo.RoleGroupCondition(database.TextOperationEqual, "group3"),
|
||||
),
|
||||
wantRoles: roles[2:4],
|
||||
},
|
||||
{
|
||||
name: "name starts with 'foo' from project 2",
|
||||
condition: database.And(
|
||||
roleRepo.InstanceIDCondition(instanceID),
|
||||
roleRepo.ProjectIDCondition(secondProjectID),
|
||||
roleRepo.DisplayNameCondition(database.TextOperationStartsWith, "foo"),
|
||||
),
|
||||
wantRoles: roles[3:5],
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotRoles, err := roleRepo.List(t.Context(), tx,
|
||||
database.WithCondition(tt.condition),
|
||||
database.WithOrderByAscending(roleRepo.PrimaryKeyColumns()...),
|
||||
)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.wantRoles, gotRoles)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProjectRole(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectID := createProject(t, tx, instanceID, orgID)
|
||||
roleRepo := repository.ProjectRepository().Role()
|
||||
|
||||
existingRole := &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key1",
|
||||
DisplayName: "Role 1",
|
||||
RoleGroup: gu.Ptr("group1"),
|
||||
}
|
||||
err := roleRepo.Create(t.Context(), tx, existingRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
role *domain.ProjectRole
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "add role",
|
||||
role: &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key2",
|
||||
DisplayName: "Role 2",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "add role, no group",
|
||||
role: &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key2",
|
||||
DisplayName: "Role 2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-existing instance",
|
||||
role: &domain.ProjectRole{
|
||||
InstanceID: "foo",
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key2",
|
||||
DisplayName: "Role 2",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
},
|
||||
wantErr: new(database.ForeignKeyError),
|
||||
},
|
||||
{
|
||||
name: "non-existing project",
|
||||
role: &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: "foo",
|
||||
OrganizationID: orgID,
|
||||
Key: "key2",
|
||||
DisplayName: "Role 2",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
},
|
||||
wantErr: new(database.ForeignKeyError),
|
||||
},
|
||||
{
|
||||
name: "non-existing organization",
|
||||
role: &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: "foo",
|
||||
Key: "key2",
|
||||
DisplayName: "Role 2",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
},
|
||||
wantErr: new(database.ForeignKeyError),
|
||||
},
|
||||
{
|
||||
name: "empty key error",
|
||||
role: &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "",
|
||||
DisplayName: "Role 2",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
},
|
||||
wantErr: new(database.CheckError),
|
||||
},
|
||||
{
|
||||
name: "empty display name error",
|
||||
role: &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key2",
|
||||
DisplayName: "",
|
||||
RoleGroup: gu.Ptr("group2"),
|
||||
},
|
||||
wantErr: new(database.CheckError),
|
||||
},
|
||||
{
|
||||
name: "duplicate key",
|
||||
role: existingRole,
|
||||
wantErr: new(database.UniqueError),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
savepoint, rollback := savepointForRollback(t, tx)
|
||||
defer rollback()
|
||||
|
||||
err := roleRepo.Create(t.Context(), savepoint, tt.role)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProjectRoles(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectID := createProject(t, tx, instanceID, orgID)
|
||||
roleRepo := repository.ProjectRepository().Role()
|
||||
|
||||
existingRole := &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key1",
|
||||
DisplayName: "Role 1",
|
||||
RoleGroup: gu.Ptr("group1"),
|
||||
}
|
||||
err := roleRepo.Create(t.Context(), tx, existingRole)
|
||||
require.NoError(t, err)
|
||||
lastUpdatedAt := existingRole.UpdatedAt
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
changes []database.Change
|
||||
wantRowsAffected int64
|
||||
wantErr error
|
||||
assertChanges func(t *testing.T, project *domain.ProjectRole)
|
||||
}{
|
||||
{
|
||||
name: "no changes",
|
||||
condition: roleRepo.PrimaryKeyCondition(existingRole.InstanceID, existingRole.ProjectID, existingRole.Key),
|
||||
wantErr: database.ErrNoChanges,
|
||||
},
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: roleRepo.KeyCondition(existingRole.Key),
|
||||
changes: []database.Change{
|
||||
roleRepo.SetDisplayName("Role 1 Updated"),
|
||||
},
|
||||
wantErr: database.NewMissingConditionError(roleRepo.InstanceIDColumn()),
|
||||
},
|
||||
{
|
||||
name: "update display name",
|
||||
condition: roleRepo.PrimaryKeyCondition(existingRole.InstanceID, existingRole.ProjectID, existingRole.Key),
|
||||
changes: []database.Change{
|
||||
roleRepo.SetDisplayName("Role 1 Updated"),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, role *domain.ProjectRole) {
|
||||
assert.Equal(t, "Role 1 Updated", role.DisplayName)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update role group",
|
||||
condition: roleRepo.PrimaryKeyCondition(existingRole.InstanceID, existingRole.ProjectID, existingRole.Key),
|
||||
changes: []database.Change{
|
||||
roleRepo.SetRoleGroup("group1 Updated"),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, role *domain.ProjectRole) {
|
||||
assert.Equal(t, gu.Ptr("group1 Updated"), role.RoleGroup)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set empty display name, not allowed",
|
||||
condition: roleRepo.PrimaryKeyCondition(existingRole.InstanceID, existingRole.ProjectID, existingRole.Key),
|
||||
changes: []database.Change{
|
||||
roleRepo.SetDisplayName(""),
|
||||
},
|
||||
wantRowsAffected: 0,
|
||||
wantErr: new(database.CheckError),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
savepoint, rollback := savepointForRollback(t, tx)
|
||||
defer rollback()
|
||||
gotRowsAffected, err := roleRepo.Update(t.Context(), savepoint, tt.condition, tt.changes...)
|
||||
assert.Equal(t, tt.wantRowsAffected, gotRowsAffected)
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
|
||||
if tt.assertChanges != nil {
|
||||
updatedRole, err := roleRepo.Get(t.Context(), savepoint, database.WithCondition(
|
||||
roleRepo.PrimaryKeyCondition(existingRole.InstanceID, existingRole.ProjectID, existingRole.Key),
|
||||
))
|
||||
require.NoError(t, err)
|
||||
assert.WithinRange(t, updatedRole.CreatedAt, existingRole.CreatedAt, existingRole.CreatedAt)
|
||||
assert.WithinRange(t, updatedRole.UpdatedAt, lastUpdatedAt, lastUpdatedAt.Add(time.Second))
|
||||
lastUpdatedAt = updatedRole.UpdatedAt
|
||||
tt.assertChanges(t, updatedRole)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteProjectRole(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectID := createProject(t, tx, instanceID, orgID)
|
||||
roleRepo := repository.ProjectRepository().Role()
|
||||
|
||||
existingRole := &domain.ProjectRole{
|
||||
InstanceID: instanceID,
|
||||
ProjectID: projectID,
|
||||
OrganizationID: orgID,
|
||||
Key: "key1",
|
||||
DisplayName: "Role 1",
|
||||
RoleGroup: gu.Ptr("group1"),
|
||||
}
|
||||
err := roleRepo.Create(t.Context(), tx, existingRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
wantRowsAffected int64
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: roleRepo.KeyCondition(existingRole.Key),
|
||||
wantErr: database.NewMissingConditionError(roleRepo.InstanceIDColumn()),
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
condition: roleRepo.PrimaryKeyCondition(instanceID, projectID, "baz"),
|
||||
wantRowsAffected: 0,
|
||||
},
|
||||
{
|
||||
name: "delete role",
|
||||
condition: roleRepo.PrimaryKeyCondition(existingRole.InstanceID, existingRole.ProjectID, existingRole.Key),
|
||||
wantRowsAffected: 1,
|
||||
},
|
||||
{
|
||||
name: "delete role twice",
|
||||
condition: roleRepo.PrimaryKeyCondition(existingRole.InstanceID, existingRole.ProjectID, existingRole.Key),
|
||||
wantRowsAffected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotRowsAffected, err := roleRepo.Delete(t.Context(), tx, tt.condition)
|
||||
assert.Equal(t, tt.wantRowsAffected, gotRowsAffected)
|
||||
assert.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
521
backend/v3/storage/database/repository/project_test.go
Normal file
521
backend/v3/storage/database/repository/project_test.go
Normal file
@@ -0,0 +1,521 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestGetProject(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectRepo := repository.ProjectRepository()
|
||||
|
||||
firstProject := &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
}
|
||||
err := projectRepo.Create(t.Context(), tx, firstProject)
|
||||
require.NoError(t, err)
|
||||
secondProject := &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
}
|
||||
err = projectRepo.Create(t.Context(), tx, secondProject)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
want *domain.Project
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: projectRepo.IDCondition(firstProject.ID),
|
||||
wantErr: database.NewMissingConditionError(projectRepo.IDColumn()),
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, "nix"),
|
||||
wantErr: database.NewNoRowFoundError(nil),
|
||||
},
|
||||
{
|
||||
name: "too many",
|
||||
condition: projectRepo.InstanceIDCondition(instanceID),
|
||||
wantErr: database.NewMultipleRowsFoundError(nil),
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, firstProject.ID),
|
||||
want: firstProject,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := projectRepo.Get(t.Context(), tx, database.WithCondition(tt.condition))
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProjects(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
firstOrgID := createOrganization(t, tx, instanceID)
|
||||
secondOrgID := createOrganization(t, tx, instanceID)
|
||||
projectRepo := repository.ProjectRepository()
|
||||
|
||||
projects := [...]*domain.Project{
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: firstOrgID,
|
||||
ID: "1",
|
||||
Name: "spanac",
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: firstOrgID,
|
||||
ID: "2",
|
||||
Name: "foobar",
|
||||
State: domain.ProjectStateInactive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: secondOrgID,
|
||||
ID: "3",
|
||||
Name: "foobaz",
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: secondOrgID,
|
||||
ID: "4",
|
||||
Name: "bazqux",
|
||||
State: domain.ProjectStateInactive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
}
|
||||
for _, project := range projects {
|
||||
err := projectRepo.Create(t.Context(), tx, project)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
want []*domain.Project
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: projectRepo.OrganizationIDCondition(firstOrgID),
|
||||
wantErr: database.NewMissingConditionError(projectRepo.IDColumn()),
|
||||
},
|
||||
{
|
||||
name: "no results, ok",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, "nix"),
|
||||
},
|
||||
{
|
||||
name: "all from instance",
|
||||
condition: projectRepo.InstanceIDCondition(instanceID),
|
||||
want: projects[:],
|
||||
},
|
||||
{
|
||||
name: "all from first org",
|
||||
condition: database.And(
|
||||
projectRepo.InstanceIDCondition(instanceID),
|
||||
projectRepo.OrganizationIDCondition(firstOrgID),
|
||||
),
|
||||
want: projects[0:2],
|
||||
},
|
||||
{
|
||||
name: "name starts with 'foo'",
|
||||
condition: database.And(
|
||||
projectRepo.InstanceIDCondition(instanceID),
|
||||
projectRepo.NameCondition(database.TextOperationStartsWith, "foo"),
|
||||
),
|
||||
want: projects[1:3],
|
||||
},
|
||||
{
|
||||
name: "state active",
|
||||
condition: database.And(
|
||||
projectRepo.InstanceIDCondition(instanceID),
|
||||
projectRepo.StateCondition(domain.ProjectStateActive),
|
||||
),
|
||||
want: []*domain.Project{projects[0], projects[2]},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := projectRepo.List(t.Context(), tx,
|
||||
database.WithCondition(tt.condition),
|
||||
database.WithOrderByAscending(projectRepo.PrimaryKeyColumns()...),
|
||||
)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateProject(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectRepo := repository.ProjectRepository()
|
||||
|
||||
existingProject := &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
}
|
||||
err := projectRepo.Create(t.Context(), tx, existingProject)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
project *domain.Project
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "add project",
|
||||
project: &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-existing instance",
|
||||
project: &domain.Project{
|
||||
InstanceID: "foo",
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
wantErr: new(database.ForeignKeyError),
|
||||
},
|
||||
{
|
||||
name: "non-existing org",
|
||||
project: &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: "foo",
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
wantErr: new(database.ForeignKeyError),
|
||||
},
|
||||
{
|
||||
name: "empty id error",
|
||||
project: &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: "",
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
wantErr: new(database.CheckError),
|
||||
},
|
||||
{
|
||||
name: "empty name error",
|
||||
project: &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: "",
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
},
|
||||
wantErr: new(database.CheckError),
|
||||
},
|
||||
{
|
||||
name: "duplicate project",
|
||||
project: existingProject,
|
||||
wantErr: new(database.UniqueError),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
savepoint, rollback := savepointForRollback(t, tx)
|
||||
defer rollback()
|
||||
err := projectRepo.Create(t.Context(), savepoint, tt.project)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProject(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectRepo := repository.ProjectRepository()
|
||||
|
||||
existingProject := &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
}
|
||||
err := projectRepo.Create(t.Context(), tx, existingProject)
|
||||
require.NoError(t, err)
|
||||
lastUpdatedAt := existingProject.UpdatedAt
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
changes []database.Change
|
||||
wantRowsAffected int64
|
||||
wantErr error
|
||||
assertChanges func(t *testing.T, project *domain.Project)
|
||||
}{
|
||||
{
|
||||
name: "no changes",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{},
|
||||
wantRowsAffected: 0,
|
||||
wantErr: database.ErrNoChanges,
|
||||
},
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: projectRepo.InstanceIDCondition(instanceID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetName("new name"),
|
||||
},
|
||||
wantRowsAffected: 0,
|
||||
wantErr: database.NewMissingConditionError(projectRepo.IDColumn()),
|
||||
},
|
||||
{
|
||||
name: "set name",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetName("new name"),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, updatedProject *domain.Project) {
|
||||
assert.Equal(t, "new name", updatedProject.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set state",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetState(domain.ProjectStateInactive),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, updatedProject *domain.Project) {
|
||||
assert.Equal(t, domain.ProjectStateInactive, updatedProject.State)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set should_assert_role",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetShouldAssertRole(false),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, updatedProject *domain.Project) {
|
||||
assert.False(t, updatedProject.ShouldAssertRole)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set is_authorization_required",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetIsAuthorizationRequired(false),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, updatedProject *domain.Project) {
|
||||
assert.False(t, updatedProject.IsAuthorizationRequired)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set is_project_access_required",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetIsProjectAccessRequired(false),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, updatedProject *domain.Project) {
|
||||
assert.False(t, updatedProject.IsProjectAccessRequired)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set used_labeling_setting_owner",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetUsedLabelingSettingOwner(2),
|
||||
},
|
||||
wantRowsAffected: 1,
|
||||
assertChanges: func(t *testing.T, updatedProject *domain.Project) {
|
||||
assert.Equal(t, int16(2), updatedProject.UsedLabelingSettingOwner)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set empty name, not allowed",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
changes: []database.Change{
|
||||
projectRepo.SetName(""),
|
||||
},
|
||||
wantRowsAffected: 0,
|
||||
wantErr: new(database.CheckError),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
savepoint, rollback := savepointForRollback(t, tx)
|
||||
defer rollback()
|
||||
rowsAffected, err := projectRepo.Update(t.Context(), savepoint, tt.condition, tt.changes...)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.wantRowsAffected, rowsAffected)
|
||||
|
||||
if tt.assertChanges != nil {
|
||||
updatedProject, err := projectRepo.Get(t.Context(), savepoint, database.WithCondition(
|
||||
projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
))
|
||||
require.NoError(t, err)
|
||||
assert.WithinRange(t, updatedProject.CreatedAt, existingProject.CreatedAt, existingProject.CreatedAt)
|
||||
assert.WithinRange(t, updatedProject.UpdatedAt, lastUpdatedAt, lastUpdatedAt.Add(time.Second))
|
||||
lastUpdatedAt = updatedProject.UpdatedAt
|
||||
tt.assertChanges(t, updatedProject)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteProject(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
defer rollback()
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
projectRepo := repository.ProjectRepository()
|
||||
|
||||
existingProject := &domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
ShouldAssertRole: true,
|
||||
IsAuthorizationRequired: true,
|
||||
IsProjectAccessRequired: true,
|
||||
UsedLabelingSettingOwner: 1,
|
||||
}
|
||||
err := projectRepo.Create(t.Context(), tx, existingProject)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
condition database.Condition
|
||||
wantRowsAffected int64
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "incomplete condition",
|
||||
condition: projectRepo.InstanceIDCondition(instanceID),
|
||||
wantRowsAffected: 0,
|
||||
wantErr: database.NewMissingConditionError(projectRepo.IDColumn()),
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, "foo"),
|
||||
wantRowsAffected: 0,
|
||||
},
|
||||
{
|
||||
name: "delete project",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
wantRowsAffected: 1,
|
||||
},
|
||||
{
|
||||
name: "delete project twice",
|
||||
condition: projectRepo.PrimaryKeyCondition(instanceID, existingProject.ID),
|
||||
wantRowsAffected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
rowsAffected, err := projectRepo.Delete(t.Context(), tx, tt.condition)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.wantRowsAffected, rowsAffected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
@@ -14,3 +16,55 @@ func writeCondition(
|
||||
builder.WriteString(" WHERE ")
|
||||
condition.Write(builder)
|
||||
}
|
||||
|
||||
func checkRestrictingColumns(
|
||||
condition database.Condition,
|
||||
requiredColumns ...database.Column,
|
||||
) error {
|
||||
for _, col := range requiredColumns {
|
||||
if !condition.IsRestrictingColumn(col) {
|
||||
return database.NewMissingConditionError(col)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type pkRepository interface {
|
||||
PrimaryKeyColumns() []database.Column
|
||||
}
|
||||
|
||||
// checkPKCondition checks if the Primary Key columns are part of the condition.
|
||||
// This can ensure only a single row is affected by updates and deletes.
|
||||
func checkPKCondition(
|
||||
repo pkRepository,
|
||||
condition database.Condition,
|
||||
) error {
|
||||
return checkRestrictingColumns(
|
||||
condition,
|
||||
repo.PrimaryKeyColumns()...,
|
||||
)
|
||||
}
|
||||
|
||||
func getOne[Target any](ctx context.Context, querier database.Querier, builder *database.StatementBuilder) (*Target, error) {
|
||||
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var target Target
|
||||
if err := rows.(database.CollectableRows).CollectExactlyOneRow(&target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &target, nil
|
||||
}
|
||||
|
||||
func getMany[Target any](ctx context.Context, querier database.Querier, builder *database.StatementBuilder) ([]*Target, error) {
|
||||
rows, err := querier.Query(ctx, builder.String(), builder.Args()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var targets []*Target
|
||||
if err := rows.(database.CollectableRows).Collect(&targets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,13 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"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/dialect/postgres/embedded"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -49,3 +54,72 @@ func newEmbeddedDB(ctx context.Context) (pool database.PoolTest, stop func(), er
|
||||
}
|
||||
return pool, stop, err
|
||||
}
|
||||
|
||||
func transactionForRollback(t *testing.T) (tx database.Transaction, rollback func()) {
|
||||
t.Helper()
|
||||
tx, err := pool.Begin(t.Context(), nil)
|
||||
require.NoError(t, err)
|
||||
return tx, func() {
|
||||
err := tx.Rollback(t.Context())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func savepointForRollback(t *testing.T, tx database.Transaction) (savepoint database.Transaction, rollback func()) {
|
||||
t.Helper()
|
||||
savepoint, err := tx.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
return savepoint, func() {
|
||||
err := savepoint.Rollback(t.Context())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func createInstance(t *testing.T, tx database.Transaction) (instanceID string) {
|
||||
t.Helper()
|
||||
instance := domain.Instance{
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleClient",
|
||||
ConsoleAppID: "consoleApp",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
}
|
||||
instanceRepo := repository.InstanceRepository()
|
||||
err := instanceRepo.Create(t.Context(), tx, &instance)
|
||||
require.NoError(t, err)
|
||||
|
||||
return instance.ID
|
||||
}
|
||||
|
||||
func createOrganization(t *testing.T, tx database.Transaction, instanceID string) (orgID string) {
|
||||
t.Helper()
|
||||
org := domain.Organization{
|
||||
InstanceID: instanceID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.OrgStateActive,
|
||||
}
|
||||
orgRepo := repository.OrganizationRepository()
|
||||
err := orgRepo.Create(t.Context(), tx, &org)
|
||||
require.NoError(t, err)
|
||||
|
||||
return org.ID
|
||||
}
|
||||
|
||||
func createProject(t *testing.T, tx database.Transaction, instanceID, orgID string) (projectID string) {
|
||||
t.Helper()
|
||||
project := domain.Project{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Name: gofakeit.Name(),
|
||||
State: domain.ProjectStateActive,
|
||||
}
|
||||
projectRepo := repository.ProjectRepository()
|
||||
err := projectRepo.Create(t.Context(), tx, &project)
|
||||
require.NoError(t, err)
|
||||
|
||||
return project.ID
|
||||
}
|
||||
|
||||
@@ -23,6 +23,13 @@ type argWriter interface {
|
||||
WriteArg(builder *StatementBuilder)
|
||||
}
|
||||
|
||||
func NewStatementBuilder(baseQuery string, baseArgs ...any) *StatementBuilder {
|
||||
b := &StatementBuilder{}
|
||||
b.WriteString(baseQuery)
|
||||
b.AppendArgs(baseArgs...)
|
||||
return b
|
||||
}
|
||||
|
||||
// WriteArgs adds the argument to the statement and writes the placeholder to the query.
|
||||
func (b *StatementBuilder) WriteArg(arg any) {
|
||||
if writer, ok := arg.(argWriter); ok {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
var (
|
||||
CTX, IAMOwnerCTX, OrgCTX context.Context
|
||||
Instance *integration.Instance
|
||||
Client mgmt_pb.ManagementServiceClient
|
||||
Instance *integration.Instance
|
||||
Client mgmt_pb.ManagementServiceClient
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
180
internal/query/projection/project_relational.go
Normal file
180
internal/query/projection/project_relational.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
repoDomain "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/project"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type projectRelationalProjection struct{}
|
||||
|
||||
func (*projectRelationalProjection) Name() string {
|
||||
return "zitadel.projects"
|
||||
}
|
||||
|
||||
func newProjectRelationalProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(projectRelationalProjection))
|
||||
}
|
||||
|
||||
func (p *projectRelationalProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: project.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: project.ProjectAddedType,
|
||||
Reduce: p.reduceProjectAdded,
|
||||
},
|
||||
{
|
||||
Event: project.ProjectChangedType,
|
||||
Reduce: p.reduceProjectChanged,
|
||||
},
|
||||
{
|
||||
Event: project.ProjectDeactivatedType,
|
||||
Reduce: p.reduceProjectDeactivated,
|
||||
},
|
||||
{
|
||||
Event: project.ProjectReactivatedType,
|
||||
Reduce: p.reduceProjectReactivated,
|
||||
},
|
||||
{
|
||||
Event: project.ProjectRemovedType,
|
||||
Reduce: p.reduceProjectRemoved,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *projectRelationalProjection) reduceProjectAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.ProjectAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInternalf(nil, "HANDL-Oox5e", "reduce.wrong.event.type %s", project.ProjectAddedType)
|
||||
}
|
||||
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-kGokE", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
repo := repository.ProjectRepository()
|
||||
return repo.Create(ctx, v3_sql.SQLTx(tx), &repoDomain.Project{
|
||||
InstanceID: e.Aggregate().InstanceID,
|
||||
OrganizationID: e.Aggregate().ResourceOwner,
|
||||
ID: e.Aggregate().ID,
|
||||
CreatedAt: e.CreationDate(),
|
||||
UpdatedAt: e.CreationDate(),
|
||||
Name: e.Name,
|
||||
State: repoDomain.ProjectStateActive,
|
||||
ShouldAssertRole: e.ProjectRoleAssertion,
|
||||
IsAuthorizationRequired: e.ProjectRoleCheck,
|
||||
IsProjectAccessRequired: e.HasProjectCheck,
|
||||
UsedLabelingSettingOwner: int16(e.PrivateLabelingSetting),
|
||||
})
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *projectRelationalProjection) reduceProjectChanged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.ProjectChangeEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Oox5e", "reduce.wrong.event.type %s", project.ProjectChangedType)
|
||||
}
|
||||
if e.Name == nil && e.HasProjectCheck == nil && e.ProjectRoleAssertion == nil && e.ProjectRoleCheck == nil && e.PrivateLabelingSetting == nil {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
repo := repository.ProjectRepository()
|
||||
changes := make([]database.Change, 0, 6)
|
||||
changes = append(changes, repo.SetUpdatedAt(e.CreationDate()))
|
||||
if e.Name != nil {
|
||||
changes = append(changes, repo.SetName(*e.Name))
|
||||
}
|
||||
if e.ProjectRoleAssertion != nil {
|
||||
changes = append(changes, repo.SetShouldAssertRole(*e.ProjectRoleAssertion))
|
||||
}
|
||||
if e.ProjectRoleCheck != nil {
|
||||
changes = append(changes, repo.SetIsAuthorizationRequired(*e.ProjectRoleCheck))
|
||||
}
|
||||
if e.HasProjectCheck != nil {
|
||||
changes = append(changes, repo.SetIsProjectAccessRequired(*e.HasProjectCheck))
|
||||
}
|
||||
if e.PrivateLabelingSetting != nil {
|
||||
changes = append(changes, repo.SetUsedLabelingSettingOwner(int16(*e.PrivateLabelingSetting)))
|
||||
}
|
||||
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-kGokE", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
|
||||
changes...,
|
||||
)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *projectRelationalProjection) reduceProjectDeactivated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.ProjectDeactivatedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Oox5e", "reduce.wrong.event.type %s", project.ProjectDeactivatedType)
|
||||
}
|
||||
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
|
||||
repo := repository.ProjectRepository()
|
||||
tx, ok := ex.(*sql.Tx)
|
||||
if !ok {
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-kGokE", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
|
||||
repo.SetUpdatedAt(e.CreationDate()),
|
||||
repo.SetState(repoDomain.ProjectStateInactive),
|
||||
)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *projectRelationalProjection) reduceProjectReactivated(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.ProjectReactivatedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-oof4U", "reduce.wrong.event.type %s", project.ProjectReactivatedType)
|
||||
}
|
||||
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
|
||||
repo := repository.ProjectRepository()
|
||||
tx, ok := ex.(*sql.Tx)
|
||||
if !ok {
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-kGokE", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
|
||||
repo.SetUpdatedAt(e.CreationDate()),
|
||||
repo.SetState(repoDomain.ProjectStateActive),
|
||||
)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *projectRelationalProjection) reduceProjectRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.ProjectRemovedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Xae7w", "reduce.wrong.event.type %s", project.ProjectRemovedType)
|
||||
}
|
||||
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
|
||||
repo := repository.ProjectRepository()
|
||||
tx, ok := ex.(*sql.Tx)
|
||||
if !ok {
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-kGokE", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
_, err := repo.Delete(ctx, v3_sql.SQLTx(tx),
|
||||
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID),
|
||||
)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
137
internal/query/projection/project_role_relational.go
Normal file
137
internal/query/projection/project_role_relational.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package projection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
repoDomain "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/project"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
ProjectRoleRelationalTable = "zitadel.project_roles"
|
||||
ProjectRoleRelationalKeyCol = "key"
|
||||
ProjectRoleRelationalDisplayNameCol = "display_name"
|
||||
ProjectRoleRelationalRoleGroupCol = "role_group"
|
||||
)
|
||||
|
||||
type projectRoleRelationalProjection struct{}
|
||||
|
||||
func (*projectRoleRelationalProjection) Name() string {
|
||||
return ProjectRoleRelationalTable
|
||||
}
|
||||
|
||||
func newProjectRoleRelationalProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(projectRoleRelationalProjection))
|
||||
}
|
||||
|
||||
func (p *projectRoleRelationalProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: project.AggregateType,
|
||||
EventReducers: []handler.EventReducer{
|
||||
{
|
||||
Event: project.RoleAddedType,
|
||||
Reduce: p.reduceProjectRoleAdded,
|
||||
},
|
||||
{
|
||||
Event: project.RoleChangedType,
|
||||
Reduce: p.reduceProjectRoleChanged,
|
||||
},
|
||||
{
|
||||
Event: project.RoleRemovedType,
|
||||
Reduce: p.reduceProjectRoleRemoved,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *projectRoleRelationalProjection) reduceProjectRoleAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.RoleAddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-EiPa5", "reduce.wrong.event.type %s", project.RoleAddedType)
|
||||
}
|
||||
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-ohR0u", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
|
||||
// Group is optional and nullable but not a pointer in the event
|
||||
// so we need to convert the empty string to a nil pointer
|
||||
var group *string
|
||||
if e.Group != "" {
|
||||
group = &e.Group
|
||||
}
|
||||
|
||||
repo := repository.ProjectRepository().Role()
|
||||
return repo.Create(ctx, v3_sql.SQLTx(tx), &repoDomain.ProjectRole{
|
||||
InstanceID: e.Aggregate().InstanceID,
|
||||
OrganizationID: e.Aggregate().ResourceOwner,
|
||||
ProjectID: e.Aggregate().ID,
|
||||
CreatedAt: e.CreationDate(),
|
||||
UpdatedAt: e.CreationDate(),
|
||||
Key: e.Key,
|
||||
DisplayName: e.DisplayName,
|
||||
RoleGroup: group,
|
||||
})
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *projectRoleRelationalProjection) reduceProjectRoleChanged(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.RoleChangedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-PieJ9", "reduce.wrong.event.type %s", project.RoleChangedType)
|
||||
}
|
||||
if e.DisplayName == nil && e.Group == nil {
|
||||
return handler.NewNoOpStatement(e), nil
|
||||
}
|
||||
|
||||
repo := repository.ProjectRepository().Role()
|
||||
changes := make([]database.Change, 0, 3)
|
||||
changes = append(changes,
|
||||
repo.SetUpdatedAt(e.CreationDate()),
|
||||
)
|
||||
if e.DisplayName != nil {
|
||||
changes = append(changes, repo.SetDisplayName(*e.DisplayName))
|
||||
}
|
||||
if e.Group != nil {
|
||||
changes = append(changes, repo.SetRoleGroup(*e.Group))
|
||||
}
|
||||
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-ooCo1", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
_, err := repo.Update(ctx, v3_sql.SQLTx(tx),
|
||||
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.Key),
|
||||
changes...,
|
||||
)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *projectRoleRelationalProjection) reduceProjectRoleRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*project.RoleRemovedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-Ei9po", "reduce.wrong.event.type %s", project.RoleRemovedType)
|
||||
}
|
||||
return handler.NewStatement(e, func(ctx context.Context, ex handler.Executer, _ string) error {
|
||||
repo := repository.ProjectRepository().Role()
|
||||
tx, ok := ex.(*sql.Tx)
|
||||
if !ok {
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "HANDL-Voh4a", "reduce.wrong.db.pool %T", ex)
|
||||
}
|
||||
_, err := repo.Delete(ctx, v3_sql.SQLTx(tx),
|
||||
repo.PrimaryKeyCondition(e.Aggregate().InstanceID, e.Aggregate().ID, e.Key),
|
||||
)
|
||||
return err
|
||||
}), nil
|
||||
}
|
||||
@@ -94,6 +94,8 @@ var (
|
||||
InstanceDomainRelationalProjection *handler.Handler
|
||||
OrganizationDomainRelationalProjection *handler.Handler
|
||||
IDPTemplateRelationalProjection *handler.Handler
|
||||
ProjectRelationalProjection *handler.Handler
|
||||
ProjectRoleRelationalProjection *handler.Handler
|
||||
|
||||
ProjectGrantFields *handler.FieldHandler
|
||||
OrgDomainVerifiedFields *handler.FieldHandler
|
||||
@@ -210,6 +212,8 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
|
||||
InstanceDomainRelationalProjection = newInstanceDomainRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["instance_domains_relational"]))
|
||||
OrganizationDomainRelationalProjection = newOrgDomainRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["organization_domains_relational"]))
|
||||
IDPTemplateRelationalProjection = newIDPTemplateRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["idp_templates_relational"]))
|
||||
ProjectRelationalProjection = newProjectRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["projects_relational"]))
|
||||
ProjectRoleRelationalProjection = newProjectRoleRelationalProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["project_roles_relational"]))
|
||||
|
||||
newProjectionsList()
|
||||
newFieldsList()
|
||||
@@ -398,5 +402,7 @@ func newProjectionsList() {
|
||||
InstanceDomainRelationalProjection,
|
||||
OrganizationDomainRelationalProjection,
|
||||
IDPTemplateRelationalProjection,
|
||||
ProjectRelationalProjection,
|
||||
ProjectRoleRelationalProjection,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user