feat(rt): authorizations (#11066)

# Which Problems Are Solved

This PR implements relational table and repository interface for the
authorizations feature as described in
https://github.com/zitadel/zitadel/issues/10767

# How the Problems Are Solved

By
* adding new `zitadel.authorizations` and `zitadel.authorization_roles`
tables
* implementing the `AuthorizationRepository` interface to create,
update, query, and delete user authorizations and roles
* implementing `authorization_relational.go` projection to update
`zitadel.authorizations` and `zitadel.authorization_roles` tables based
on events

# Additional Changes
N/A

# Additional Context
- Closes #10767

---------

Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Gayathri Vijayan
2025-12-19 07:03:27 +01:00
committed by GitHub
co-authored by Livio Spring
parent 11cf2c252a
commit 8472c7683d
13 changed files with 3490 additions and 9 deletions
@@ -0,0 +1,16 @@
package migration
import (
_ "embed"
)
var (
//go:embed 010_authorizations_table/up.sql
up010AuthorizationsTable string
//go:embed 010_authorizations_table/down.sql
down010AuthorizationsTable string
)
func init() {
registerSQLMigration(10, up010AuthorizationsTable, down010AuthorizationsTable)
}
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS zitadel.authorization_roles;
DROP TABLE IF EXISTS zitadel.authorizations;
DROP TYPE IF EXISTS zitadel.authorization_state;
@@ -0,0 +1,40 @@
CREATE TYPE zitadel.authorization_state AS ENUM (
'active',
'inactive'
);
CREATE TABLE zitadel.authorizations
(
instance_id TEXT NOT NULL,
id TEXT NOT NULL CHECK ( id <> '' ),
state zitadel.authorization_state NOT NULL DEFAULT 'active',
project_id TEXT NOT NULL,
grant_id TEXT,
user_id TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (instance_id, id),
-- FOREIGN KEY (instance_id, user_id) REFERENCES zitadel.users (instance_id, id) ON DELETE CASCADE,
FOREIGN KEY (instance_id, project_id) REFERENCES zitadel.projects (instance_id, id) ON DELETE CASCADE,
FOREIGN KEY (instance_id, grant_id) REFERENCES zitadel.project_grants (instance_id, id) ON DELETE CASCADE
);
CREATE TABLE zitadel.authorization_roles
(
instance_id TEXT NOT NULL,
authorization_id TEXT NOT NULL,
role_key TEXT NOT NULL CHECK ( role_key <> '' ),
project_id TEXT NOT NULL,
grant_id TEXT,
PRIMARY KEY (instance_id, authorization_id, role_key),
FOREIGN KEY (instance_id, authorization_id) REFERENCES zitadel.authorizations (instance_id, id) ON DELETE CASCADE,
FOREIGN KEY (instance_id, project_id, role_key) REFERENCES zitadel.project_roles (instance_id, project_id, key) ON DELETE CASCADE,
FOREIGN KEY (instance_id, grant_id, role_key) REFERENCES zitadel.project_grant_roles (instance_id, grant_id, key) ON DELETE CASCADE
);
CREATE TRIGGER trigger_set_updated_at
BEFORE UPDATE
ON zitadel.authorizations
FOR EACH ROW
WHEN (NEW.updated_at IS NULL)
EXECUTE FUNCTION zitadel.set_updated_at();
@@ -0,0 +1,505 @@
//go:build integration
package events_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/internal/integration"
authorization_v2 "github.com/zitadel/zitadel/pkg/grpc/authorization/v2"
instance_v2 "github.com/zitadel/zitadel/pkg/grpc/instance/v2"
org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta"
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func TestServer_AuthorizationReduces(t *testing.T) {
instanceID := Instance.ID()
orgID := Instance.DefaultOrg.Id
authorizationRepo := repository.AuthorizationRepository()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
t.Run("user grant added reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2})
createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick)
})
t.Run("user grant update reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2})
beforeCreate := time.Now()
// create authorization
createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{
UserId: user.UserId,
ProjectId: projectID,
OrganizationId: orgID,
RoleKeys: []string{role1, role2},
})
require.NoError(t, err)
afterCreate := time.Now().Add(500 * time.Millisecond)
// add a new role to the project
role3 := "role3"
_, err = ProjectClient.AddProjectRole(CTX, &project_v2beta.AddProjectRoleRequest{
ProjectId: projectID,
RoleKey: role3,
DisplayName: "display",
Group: nil,
})
require.NoError(t, err)
beforeUpdate := time.Now()
// update roles to [role1, role2, role3]
_, err = AuthorizationClient.UpdateAuthorization(CTX, &authorization_v2.UpdateAuthorizationRequest{
Id: createdAuthorization.Id,
RoleKeys: []string{role1, role2, role3},
})
require.NoError(t, err)
afterUpdate := time.Now().Add(500 * time.Millisecond)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.NoError(collect, err)
require.Len(collect, az.Roles, 3)
assert.Equal(collect, []string{role1, role2, role3}, az.Roles)
assert.WithinRange(collect, az.UpdatedAt, beforeUpdate, afterUpdate)
assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate)
}, retryDuration, tick, "authorization not updated within %v: %v", retryDuration, err)
})
t.Run("user grant deactivate reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project and project roles
projectID := prepareProjectAndProjectRoles(t, orgID, nil)
// create authorization
beforeCreate := time.Now()
createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{
UserId: user.UserId,
ProjectId: projectID,
OrganizationId: orgID,
})
require.NoError(t, err)
afterCreate := time.Now().Add(500 * time.Millisecond)
before := time.Now()
// deactivate authorization
_, err = AuthorizationClient.DeactivateAuthorization(CTX, &authorization_v2.DeactivateAuthorizationRequest{
Id: createdAuthorization.Id,
})
require.NoError(t, err)
after := time.Now().Add(500 * time.Millisecond)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.NoError(collect, err)
assert.Equal(collect, domain.AuthorizationStateInactive, az.State)
assert.WithinRange(collect, az.UpdatedAt, before, after)
assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate)
}, retryDuration, tick, "authorization not deactivated within %v: %v", retryDuration, err)
})
t.Run("user grant activate reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project
projectID := prepareProjectAndProjectRoles(t, orgID, nil)
// create authorization
beforeCreate := time.Now()
createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{
UserId: user.UserId,
ProjectId: projectID,
OrganizationId: orgID,
})
require.NoError(t, err)
afterCreate := time.Now().Add(500 * time.Millisecond)
// deactivate authorization
_, err = AuthorizationClient.DeactivateAuthorization(CTX, &authorization_v2.DeactivateAuthorizationRequest{
Id: createdAuthorization.Id,
})
require.NoError(t, err)
before := time.Now()
// re-activate authorization
_, err = AuthorizationClient.ActivateAuthorization(CTX, &authorization_v2.ActivateAuthorizationRequest{
Id: createdAuthorization.Id,
})
require.NoError(t, err)
after := time.Now().Add(500 * time.Millisecond)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.NoError(collect, err)
assert.Equal(collect, domain.AuthorizationStateActive, az.State)
assert.WithinRange(collect, az.UpdatedAt, before, after)
assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate)
}, retryDuration, tick, "authorization not activated within %v: %v", retryDuration, err)
})
t.Run("user grant removed reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2})
// create authorization with roles
createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{
UserId: user.UserId,
ProjectId: projectID,
RoleKeys: []string{role1, role2},
OrganizationId: orgID,
})
require.NoError(t, err)
// delete authorization
_, err = AuthorizationClient.DeleteAuthorization(CTX, &authorization_v2.DeleteAuthorizationRequest{
Id: createdAuthorization.Id,
})
require.NoError(t, err)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.Empty(collect, az)
require.ErrorIs(collect, err, new(database.NoRowFoundError))
}, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err)
})
t.Run("user removed reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2})
// create authorization with roles
createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick)
// delete the user
_, err := UserClient.DeleteUser(CTX, &user_v2.DeleteUserRequest{
UserId: user.UserId,
})
require.NoError(t, err)
// ensure authorization is deleted
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.Empty(collect, az)
require.ErrorIs(collect, err, new(database.NoRowFoundError))
}, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err)
})
t.Run("project removed reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2})
// create authorization with roles
createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick)
// delete the project
_, err := ProjectClient.DeleteProject(CTX, &project_v2beta.DeleteProjectRequest{
Id: projectID,
})
require.NoError(t, err)
// ensure authorization is deleted
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.Empty(collect, az)
require.ErrorIs(collect, err, new(database.NoRowFoundError))
}, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err)
})
t.Run("project role removed reduces", func(t *testing.T) {
// create user
user := Instance.CreateHumanUserVerified(CTX, orgID, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2})
// create authorization with roles
createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgID, user.UserId, projectID, []string{role1, role2}, retryDuration, tick)
// delete a project role
before := time.Now()
_, err := ProjectClient.RemoveProjectRole(CTX, &project_v2beta.RemoveProjectRoleRequest{
ProjectId: projectID,
RoleKey: role2,
})
require.NoError(t, err)
after := time.Now()
// ensure authorization is updated
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.NoError(collect, err)
assert.Equal(collect, []string{role1}, az.Roles)
assert.Equal(collect, domain.AuthorizationStateActive, az.State)
assert.WithinRange(collect, az.UpdatedAt, before, after)
}, retryDuration, tick, "authorization not updated within %v: %v", retryDuration, err)
})
t.Run("user grant added for a project grant reduces", func(t *testing.T) {
// prepare project and project roles
role1 := "role1"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1})
// granted organization
grantedOrganizationName := integration.OrganizationName()
grantedOrganization := Instance.CreateOrganization(CTX, grantedOrganizationName, integration.Email())
// create project grant
_, err := ProjectClient.CreateProjectGrant(CTX, &project_v2beta.CreateProjectGrantRequest{
ProjectId: projectID,
RoleKeys: []string{role1},
GrantedOrganizationId: grantedOrganization.OrganizationId,
})
require.NoError(t, err)
// create user
user := Instance.CreateHumanUserVerified(CTX, grantedOrganization.OrganizationId, integration.Email(), integration.Phone())
// create authorization with roles
createAndEnsureAuthorization(t, instanceID, grantedOrganization.OrganizationId, user.UserId, projectID, []string{role1}, retryDuration, tick)
})
t.Run("project grant removed reduces", func(t *testing.T) {
// prepare project and project roles
role1 := "role1"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1})
// granted organization
grantedOrganizationName := integration.OrganizationName()
grantedOrganization := Instance.CreateOrganization(CTX, grantedOrganizationName, integration.Email())
// create project grant
_, err := ProjectClient.CreateProjectGrant(CTX, &project_v2beta.CreateProjectGrantRequest{
ProjectId: projectID,
RoleKeys: []string{role1},
GrantedOrganizationId: grantedOrganization.OrganizationId,
})
require.NoError(t, err)
// create user
user := Instance.CreateHumanUserVerified(CTX, grantedOrganization.OrganizationId, integration.Email(), integration.Phone())
// create authorization with roles
createdAuthorization := createAndEnsureAuthorization(t, instanceID, grantedOrganization.OrganizationId, user.UserId, projectID, []string{role1}, retryDuration, tick)
// delete project grant
_, err = ProjectClient.DeleteProjectGrant(CTX, &project_v2beta.DeleteProjectGrantRequest{
ProjectId: projectID,
GrantedOrganizationId: grantedOrganization.OrganizationId,
})
require.NoError(t, err)
// ensure authorization is deleted
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.Empty(collect, az)
require.ErrorIs(collect, err, new(database.NoRowFoundError))
}, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err)
})
t.Run("project grant updated reduces", func(t *testing.T) {
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgID, []string{role1, role2})
// granted organization
grantedOrganizationName := integration.OrganizationName()
grantedOrganization := Instance.CreateOrganization(CTX, grantedOrganizationName, integration.Email())
// create project grant
_, err := ProjectClient.CreateProjectGrant(CTX, &project_v2beta.CreateProjectGrantRequest{
ProjectId: projectID,
RoleKeys: []string{role1, role2},
GrantedOrganizationId: grantedOrganization.OrganizationId,
})
require.NoError(t, err)
// create user
user := Instance.CreateHumanUserVerified(CTX, grantedOrganization.OrganizationId, integration.Email(), integration.Phone())
// create authorization with roles
createdAuthorization := createAndEnsureAuthorization(t, instanceID, grantedOrganization.OrganizationId, user.UserId, projectID, []string{role1, role2}, retryDuration, tick)
// update project grant
//beforeUpdate := time.Now()
_, err = ProjectClient.UpdateProjectGrant(CTX, &project_v2beta.UpdateProjectGrantRequest{
ProjectId: projectID,
GrantedOrganizationId: grantedOrganization.OrganizationId,
RoleKeys: []string{role1},
})
require.NoError(t, err)
//afterUpdate := time.Now().Add(500 * time.Millisecond) // add some buffer for eventual consistency of cascading changes
// ensure authorization is updated
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.NoError(collect, err)
assert.Equal(collect, []string{role1}, az.Roles)
//assert.WithinRange(collect, az.UpdatedAt, beforeUpdate, afterUpdate)
}, retryDuration, tick, "authorization not updated within %v: %v", retryDuration, err)
})
t.Run("org removed reduces", func(t *testing.T) {
// create a new organization
orgName := integration.OrganizationName()
orgResp := Instance.CreateOrganization(CTX, orgName, integration.Email())
// create user
user := Instance.CreateHumanUserVerified(CTX, orgResp.OrganizationId, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectID := prepareProjectAndProjectRoles(t, orgResp.OrganizationId, []string{role1, role2})
// create authorization with roles
createdAuthorization := createAndEnsureAuthorization(t, instanceID, orgResp.OrganizationId, user.UserId, projectID, []string{role1, role2}, retryDuration, tick)
// delete the organization
_, err := OrgClient.DeleteOrganization(CTX, &org_v2beta.DeleteOrganizationRequest{
Id: orgResp.OrganizationId,
})
require.NoError(t, err)
// ensure authorization is deleted
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.Empty(collect, az)
require.ErrorIs(collect, err, new(database.NoRowFoundError))
}, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err)
})
t.Run("instance removed reduces", func(t *testing.T) {
// create a new instance
instance := integration.NewInstance(CTX)
// create a new organization
orgResp := instance.CreateOrganization(CTX, integration.OrganizationName(), integration.Email())
// create a user
user := instance.CreateHumanUserVerified(CTX, orgResp.OrganizationId, integration.Email(), integration.Phone())
// prepare project and project roles
role1, role2 := "role1", "role2"
projectResp := instance.CreateProject(CTX, t, orgResp.OrganizationId, integration.ProjectName(), false, false)
_ = instance.AddProjectRole(CTX, t, projectResp.Id, role1, "display", "")
_ = instance.AddProjectRole(CTX, t, projectResp.Id, role2, "display", "")
// create authorization with roles
beforeCreate := time.Now()
createdAuthorization, err := instance.Client.AuthorizationV2.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{
UserId: user.UserId,
ProjectId: projectResp.Id,
RoleKeys: []string{role1, role2},
OrganizationId: orgResp.OrganizationId,
})
require.NoError(t, err)
afterCreate := time.Now().Add(500 * time.Millisecond)
// ensure authorization exists
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instance.ID(), createdAuthorization.Id),
))
require.NoError(collect, err)
assert.Equal(collect, []string{role1, role2}, az.Roles)
assert.Equal(collect, domain.AuthorizationStateActive, az.State)
assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate)
assert.WithinRange(collect, az.UpdatedAt, beforeCreate, afterCreate)
}, retryDuration, tick, "authorization not found within %v: %v", retryDuration, err)
// delete the instance
_, err = Instance.Client.InstanceV2.DeleteInstance(CTX, &instance_v2.DeleteInstanceRequest{
InstanceId: instance.ID(),
})
require.NoError(t, err)
// ensure authorization is deleted
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authorizationRepo.Get(CTX, pool, database.WithCondition(
authorizationRepo.PrimaryKeyCondition(instance.ID(), createdAuthorization.Id),
))
require.Empty(collect, az)
require.ErrorIs(collect, err, new(database.NoRowFoundError))
}, retryDuration, tick, "authorization not deleted within %v: %v", retryDuration, err)
})
}
func createAndEnsureAuthorization(t *testing.T, instanceID, orgID, userID, projectID string, roles []string, retryDuration time.Duration, tick time.Duration) *authorization_v2.CreateAuthorizationResponse {
// create authorization
beforeCreate := time.Now()
createdAuthorization, err := AuthorizationClient.CreateAuthorization(CTX, &authorization_v2.CreateAuthorizationRequest{
UserId: userID,
ProjectId: projectID,
RoleKeys: roles,
OrganizationId: orgID,
})
require.NoError(t, err)
afterCreate := time.Now().Add(500 * time.Millisecond)
// ensure authorization exists
authzRepo := repository.AuthorizationRepository()
require.EventuallyWithT(t, func(collect *assert.CollectT) {
az, err := authzRepo.Get(CTX, pool, database.WithCondition(
authzRepo.PrimaryKeyCondition(instanceID, createdAuthorization.Id),
))
require.NoError(collect, err)
assert.Equal(collect, roles, az.Roles)
assert.Equal(collect, domain.AuthorizationStateActive, az.State)
assert.WithinRange(collect, az.CreatedAt, beforeCreate, afterCreate)
assert.WithinRange(collect, az.UpdatedAt, beforeCreate, afterCreate)
}, retryDuration, tick, "authorization not found within %v: %v", retryDuration, err)
return createdAuthorization
}
func prepareProjectAndProjectRoles(t *testing.T, orgID string, roles []string) string {
project, err := ProjectClient.CreateProject(CTX, &project_v2beta.CreateProjectRequest{
OrganizationId: orgID,
Name: integration.ProjectName(),
})
require.NoError(t, err)
if len(roles) == 0 {
return project.Id
}
// Wait for the project to be created in the relational db before adding roles
projectRepo := repository.ProjectRepository()
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
require.EventuallyWithT(t, func(collect *assert.CollectT) {
_, err := projectRepo.Get(CTX, pool, database.WithCondition(
projectRepo.PrimaryKeyCondition(Instance.ID(), project.Id),
))
require.NoError(collect, err)
}, retryDuration, tick, "project not found")
for _, role := range roles {
_, err = ProjectClient.AddProjectRole(CTX, &project_v2beta.AddProjectRoleRequest{
ProjectId: project.GetId(),
RoleKey: role,
DisplayName: "display",
})
require.NoError(t, err)
}
return project.Id
}
@@ -17,11 +17,13 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/authorization/v2"
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"
user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func getEnv(key, fallback string) string {
@@ -42,15 +44,17 @@ var ConnString = fmt.Sprintf(
)
var (
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
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
UserClient user_v2.UserServiceClient
AuthorizationClient authorization.AuthorizationServiceClient
)
var pool database.Pool
@@ -69,6 +73,8 @@ func TestMain(m *testing.M) {
ProjectClient = Instance.Client.Projectv2Beta
AdminClient = Instance.Client.Admin
MgmtClient = Instance.Client.Mgmt
UserClient = Instance.Client.UserV2
AuthorizationClient = Instance.Client.AuthorizationV2
defer func() {
_, err := Instance.Client.InstanceV2Beta.DeleteInstance(CTX, &v2beta.DeleteInstanceRequest{
@@ -0,0 +1,319 @@
package repository
import (
"context"
"time"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
var _ domain.AuthorizationRepository = (*authorization)(nil)
type authorization struct{}
func AuthorizationRepository() domain.AuthorizationRepository {
return new(authorization)
}
func (a authorization) unqualifiedTableName() string {
return "authorizations"
}
func (a authorization) qualifiedTableName() string {
return "zitadel." + a.unqualifiedTableName()
}
func (a authorization) unqualifiedAuthorizationRolesTableName() string {
return "authorization_roles"
}
func (a authorization) qualifiedAuthorizationRolesTableName() string {
return "zitadel." + a.unqualifiedAuthorizationRolesTableName()
}
// -------------------------------------------------------------
// repository
// -------------------------------------------------------------
const insertAuthorizationStmt = `INSERT INTO zitadel.authorizations (
instance_id, id, user_id, grant_id, project_id, state
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING created_at, updated_at`
const insertAuthorizationWithRolesStmt = `WITH roles AS (
INSERT INTO zitadel.authorization_roles
(instance_id, authorization_id, grant_id, project_id, role_key)
VALUES ($1, $2, $4, $5, unnest($7::text[])))` + insertAuthorizationStmt
// Create implements [domain.AuthorizationRepository].
func (a authorization) Create(ctx context.Context, client database.QueryExecutor, authorization *domain.Authorization) error {
builder := database.NewStatementBuilder(insertAuthorizationWithRolesStmt,
authorization.InstanceID,
authorization.ID,
authorization.UserID,
authorization.GrantID,
authorization.ProjectID,
authorization.State,
authorization.Roles,
)
if err := client.QueryRow(
ctx,
builder.String(),
builder.Args()...).
Scan(
&authorization.CreatedAt,
&authorization.UpdatedAt,
); err != nil {
return err
}
return nil
}
const queryAuthorizationStmt = `SELECT zitadel.authorizations.instance_id,
zitadel.authorizations.id,
zitadel.authorizations.user_id,
zitadel.authorizations.grant_id,
zitadel.authorizations.project_id,
zitadel.authorizations.state,
zitadel.authorizations.created_at,
zitadel.authorizations.updated_at,
ARRAY_AGG(authorization_roles.role_key)
FILTER (WHERE authorization_roles.authorization_id IS NOT NULL) AS roles
FROM zitadel.authorizations
LEFT JOIN zitadel.authorization_roles
ON zitadel.authorizations.instance_id = zitadel.authorization_roles.instance_id
AND zitadel.authorizations.id = zitadel.authorization_roles.authorization_id`
// Get implements [domain.AuthorizationRepository].
func (a authorization) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.Authorization, error) {
opts = append(opts,
database.WithGroupBy(a.InstanceIDColumn(), a.IDColumn()),
)
builder, err := a.prepareQuery(opts)
if err != nil {
return nil, err
}
return getOne[domain.Authorization](ctx, client, builder)
}
// List implements [domain.AuthorizationRepository].
func (a authorization) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.Authorization, error) {
opts = append(opts,
database.WithGroupBy(a.InstanceIDColumn(), a.IDColumn()),
)
builder, err := a.prepareQuery(opts)
if err != nil {
return nil, err
}
return getMany[domain.Authorization](ctx, client, builder)
}
func (a authorization) prepareQuery(opts []database.QueryOption) (*database.StatementBuilder, error) {
options := new(database.QueryOpts)
for _, opt := range opts {
opt(options)
}
if err := checkRestrictingColumns(options.Condition, a.InstanceIDColumn()); err != nil {
return nil, err
}
builder := database.NewStatementBuilder(queryAuthorizationStmt)
options.Write(builder)
return builder, nil
}
const queryUpdateAuthorizationRoleStmt = `SELECT instance_id, id, project_id, grant_id, $1::text[] as roles from zitadel.authorizations`
const updateAuthorizationRoleStmt = `deleted_roles AS (
DELETE FROM zitadel.authorization_roles as azr
USING az
WHERE azr.instance_id = az.instance_id
AND azr.authorization_id = az.id
AND NOT azr.role_key = ANY ($1::text[])
RETURNING *
), inserted_roles AS (
INSERT INTO zitadel.authorization_roles (instance_id, authorization_id, project_id, grant_id, role_key)
SELECT instance_id,
id,
project_id,
grant_id,
UNNEST(az.roles) AS role_key
FROM az
ON CONFLICT DO NOTHING
RETURNING *
)
UPDATE zitadel.authorizations SET `
const updateAuthorizationRoleStmtWhere = ` FROM az
WHERE az.instance_id = authorizations.instance_id
AND az.id = authorizations.id`
// Update implements [domain.AuthorizationRepository].
func (a authorization) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, roles []string, changes ...database.Change) (int64, error) {
if roles != nil {
return a.setRoles(ctx, client, condition, roles, changes...)
}
return updateOne(ctx, client, a, condition, changes...)
}
// setRoles sets the roles of an authorization.
func (a authorization) setRoles(ctx context.Context, client database.QueryExecutor, condition database.Condition, roles []string, changes ...database.Change) (int64, error) {
if err := checkPKCondition(a, condition); err != nil {
return 0, err
}
if !database.Changes(changes).IsOnColumn(a.UpdatedAtColumn()) {
changes = append(changes, database.NewChange(a.UpdatedAtColumn(), database.NullInstruction))
}
// get the authorization to be updated
builder := database.NewStatementBuilder("WITH az AS (")
builder.WriteString(queryUpdateAuthorizationRoleStmt)
builder.AppendArg(roles)
writeCondition(builder, condition)
builder.WriteString(" ), ")
// set the roles
builder.WriteString(updateAuthorizationRoleStmt)
err := database.Changes(changes).Write(builder)
if err != nil {
return 0, err
}
builder.WriteString(updateAuthorizationRoleStmtWhere)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
// Delete implements [domain.AuthorizationRepository].
func (a authorization) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
if !condition.IsRestrictingColumn(a.InstanceIDColumn()) {
return 0, database.NewMissingConditionError(a.InstanceIDColumn())
}
builder := database.NewStatementBuilder("DELETE FROM zitadel.authorizations")
writeCondition(builder, condition)
return client.Exec(ctx, builder.String(), builder.Args()...)
}
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
// PrimaryKeyColumns implements [domain.Repository]
func (a authorization) PrimaryKeyColumns() []database.Column {
return []database.Column{
a.InstanceIDColumn(),
a.IDColumn(),
}
}
func (a authorization) IDColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "id")
}
func (a authorization) UserIDColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "user_id")
}
func (a authorization) GrantIDColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "grant_id")
}
func (a authorization) ProjectIDColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "project_id")
}
func (a authorization) InstanceIDColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "instance_id")
}
func (a authorization) StateColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "state")
}
func (a authorization) CreatedAtColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "created_at")
}
func (a authorization) UpdatedAtColumn() database.Column {
return database.NewColumn(a.unqualifiedTableName(), "updated_at")
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
// PrimaryKeyCondition implements [domain.authorizationConditions]
func (a authorization) PrimaryKeyCondition(instanceID, id string) database.Condition {
return database.And(
a.InstanceIDCondition(instanceID),
a.IDCondition(id),
)
}
// IDCondition implements [domain.authorizationConditions]
func (a authorization) IDCondition(id string) database.Condition {
return database.NewTextCondition(a.IDColumn(), database.TextOperationEqual, id)
}
// InstanceIDCondition implements [domain.authorizationConditions]
func (a authorization) InstanceIDCondition(instanceID string) database.Condition {
return database.NewTextCondition(a.InstanceIDColumn(), database.TextOperationEqual, instanceID)
}
// ProjectIDCondition implements [domain.authorizationConditions]
func (a authorization) ProjectIDCondition(projectID string) database.Condition {
return database.NewTextCondition(a.ProjectIDColumn(), database.TextOperationEqual, projectID)
}
// GrantIDCondition implements [domain.authorizationConditions]
func (a authorization) GrantIDCondition(grantID string) database.Condition {
return database.NewTextCondition(a.GrantIDColumn(), database.TextOperationEqual, grantID)
}
// UserIDCondition implements [domain.authorizationConditions]
func (a authorization) UserIDCondition(userID string) database.Condition {
return database.NewTextCondition(a.UserIDColumn(), database.TextOperationEqual, userID)
}
// RoleCondition implements [domain.authorizationConditions]
func (a authorization) RoleCondition(op database.TextOperation, role string) database.Condition {
return database.NewTextCondition(database.NewColumn(a.unqualifiedAuthorizationRolesTableName(), "role_key"), op, role)
}
// ExistsRole implements [domain.authorizationConditions]
// ExistsRole creates a correlated [database.Exists] condition on the authorization_roles table.
// Use this filter to make sure the authorization returned contains a specific role.
func (a authorization) ExistsRole(cond database.Condition) database.Condition {
return database.Exists(
a.qualifiedAuthorizationRolesTableName(),
database.And(
database.NewColumnCondition(a.InstanceIDColumn(), database.NewColumn(a.unqualifiedAuthorizationRolesTableName(), "instance_id")),
database.NewColumnCondition(a.IDColumn(), database.NewColumn(a.unqualifiedAuthorizationRolesTableName(), "authorization_id")),
cond,
),
)
}
// StateCondition implements [domain.authorizationConditions]
func (a authorization) StateCondition(state domain.AuthorizationState) database.Condition {
return database.NewTextCondition(a.StateColumn(), database.TextOperationEqual, state.String())
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetState implements [domain.authorizationChanges]
func (a authorization) SetState(state domain.AuthorizationState) database.Change {
return database.NewChange(a.StateColumn(), state)
}
func (a authorization) SetUpdatedAt(updatedAt time.Time) database.Change {
return database.NewChange(a.UpdatedAtColumn(), updatedAt)
}
File diff suppressed because it is too large Load Diff
@@ -143,3 +143,21 @@ func createProjectRole(t *testing.T, tx database.Transaction, instanceID, orgID,
return projectRole.Key
}
func createProjectGrant(t *testing.T, tx database.Transaction, instanceID, grantingOrgID, grantedOrgID, projectID string, roleKeys []string) string {
t.Helper()
projectGrant := domain.ProjectGrant{
InstanceID: instanceID,
ID: gofakeit.UUID(),
ProjectID: projectID,
GrantingOrganizationID: grantingOrgID,
GrantedOrganizationID: grantedOrgID,
State: domain.ProjectGrantStateActive,
RoleKeys: roleKeys,
}
projectGrantRepo := repository.ProjectGrantRepository()
err := projectGrantRepo.Create(t.Context(), tx, &projectGrant)
require.NoError(t, err)
return projectGrant.ID
}