mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-09 15:45:12 +00:00
feat(repository): implement user repository (#10967)
## Summary This PR introduces the relational user repository for backend v3, including schema, repository APIs, reducers/projections, and tests for both human and machine users. ## What changed * Added user-focused relational migrations (users, verifications, metadata, PATs, machine keys, passkeys, identity provider links) and related FK wiring. * Implemented/expanded repository domain contracts and storage implementations for human and machine user flows. * Added support for machine secret handling and aligned access token type modeling. * Extended user domain models (verifications, password/email/phone/TOTP state, passkeys, metadata, invite handling, identity provider links). * Updated projection/reduction coverage and integration tests for key user lifecycle events. * Regenerated and updated mocks/enums plus supporting database change/column utilities. ## Notes for reviewers * This PR is large because it establishes the foundational relational user storage layer and related test scaffolding. Sorry for that. ## Closes Closes #10218 Closes #10224 Closes #10225 Closes #10209 Closes #10802 --------- Co-authored-by: Iraq Jaber <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Marco A. <[email protected]> Co-authored-by: Tim Möhlmann <[email protected]>
This commit is contained in:
co-authored by
Iraq Jaber
Copilot
Marco A.
Tim Möhlmann
parent
40c7b38ca1
commit
5df8a97f9d
@@ -36,17 +36,6 @@ func (a authorization) qualifiedAuthorizationRolesTableName() string {
|
||||
// repository
|
||||
// -------------------------------------------------------------
|
||||
|
||||
const insertAuthorizationStmt = `INSERT INTO zitadel.authorizations (
|
||||
instance_id, id, user_id, grant_id, project_id, state, created_at, updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
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($9::text[])))` + insertAuthorizationStmt
|
||||
|
||||
// Create implements [domain.AuthorizationRepository].
|
||||
func (a authorization) Create(ctx context.Context, client database.QueryExecutor, authorization *domain.Authorization) error {
|
||||
var (
|
||||
@@ -59,17 +48,12 @@ func (a authorization) Create(ctx context.Context, client database.QueryExecutor
|
||||
updatedAt = authorization.UpdatedAt
|
||||
}
|
||||
|
||||
builder := database.NewStatementBuilder(insertAuthorizationWithRolesStmt,
|
||||
authorization.InstanceID,
|
||||
authorization.ID,
|
||||
authorization.UserID,
|
||||
authorization.GrantID,
|
||||
authorization.ProjectID,
|
||||
authorization.State,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
authorization.Roles,
|
||||
builder := database.NewStatementBuilder("WITH roles AS (INSERT INTO zitadel.authorization_roles (instance_id, authorization_id, grant_id, project_id, role_key)"+
|
||||
" VALUES ($1, $2, $3, $4, unnest($5::text[]))) INSERT INTO zitadel.authorizations (instance_id, id, grant_id, project_id, user_id, state, created_at, updated_at) VALUES ($1, $2, $3, $4, $6, $7, ",
|
||||
authorization.InstanceID, authorization.ID, authorization.GrantID, authorization.ProjectID, authorization.Roles, authorization.UserID, authorization.State,
|
||||
)
|
||||
builder.WriteArgs(createdAt, updatedAt)
|
||||
builder.WriteString(") RETURNING created_at, updated_at")
|
||||
|
||||
if err := client.QueryRow(
|
||||
ctx,
|
||||
@@ -191,10 +175,10 @@ func (a authorization) setRoles(ctx context.Context, client database.QueryExecut
|
||||
|
||||
// set the roles
|
||||
builder.WriteString(updateAuthorizationRoleStmt)
|
||||
err := database.Changes(changes).Write(builder)
|
||||
if err != nil {
|
||||
if err := database.Changes(changes).Write(builder); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
builder.WriteString(updateAuthorizationRoleStmtWhere)
|
||||
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
|
||||
@@ -29,10 +29,8 @@ func TestCreateAuthorization(t *testing.T) {
|
||||
// create project roles
|
||||
role1 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role1")
|
||||
role2 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role2")
|
||||
// TODO: uncomment when user table is available
|
||||
// userID := createUser(t, tx, instanceID, organizationID)
|
||||
// require.NotNil(t, userID)
|
||||
userID := integration.ID()
|
||||
userID := createHumanUser(t, tx, instanceID, organizationID)
|
||||
require.NotNil(t, userID)
|
||||
|
||||
authorizationRepo := repository.AuthorizationRepository()
|
||||
|
||||
@@ -116,19 +114,18 @@ func TestCreateAuthorization(t *testing.T) {
|
||||
},
|
||||
wantErr: new(database.ForeignKeyError),
|
||||
},
|
||||
// TODO: uncomment when user table is available
|
||||
// {
|
||||
// name: "non-existent user",
|
||||
// authorization: &domain.Authorization{
|
||||
// ID: integration.ID(),
|
||||
// UserID: "random-id",
|
||||
// ProjectID: projectID,
|
||||
// InstanceID: instanceID,
|
||||
// State: domain.AuthorizationStateActive,
|
||||
// Roles: nil,
|
||||
// },
|
||||
// wantErr: new(database.ForeignKeyError),
|
||||
// },
|
||||
{
|
||||
name: "non-existent user",
|
||||
authorization: &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: "random-id",
|
||||
ProjectID: projectID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateActive,
|
||||
Roles: nil,
|
||||
},
|
||||
wantErr: new(database.ForeignKeyError),
|
||||
},
|
||||
{
|
||||
name: "duplicate authorization",
|
||||
authorization: &domain.Authorization{
|
||||
@@ -265,10 +262,8 @@ func TestGetAuthorization(t *testing.T) {
|
||||
role1 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role1")
|
||||
role2 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role2")
|
||||
|
||||
// TODO: uncomment when user table is available
|
||||
// userID := createUser(t, tx, instanceID, organizationID)
|
||||
// require.NotNil(t, userID)
|
||||
userID := integration.ID()
|
||||
userID := createHumanUser(t, tx, instanceID, organizationID)
|
||||
require.NotNil(t, userID)
|
||||
|
||||
// create authorization with roles
|
||||
authorizationRepo := repository.AuthorizationRepository()
|
||||
@@ -458,13 +453,10 @@ func TestListAuthorization(t *testing.T) {
|
||||
project2Role1 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "project2Role1")
|
||||
project2Role2 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "project2Role2")
|
||||
|
||||
// TODO: uncomment when user table is available
|
||||
// user1ID := createUser(t, tx, instanceID, organizationID)
|
||||
// require.NotNil(t, user1ID)
|
||||
user1ID := integration.ID()
|
||||
// user2ID := createUser(t, tx, instanceID, organizationID)
|
||||
// require.NotNil(t, user2ID)
|
||||
user2ID := integration.ID()
|
||||
user1ID := createHumanUser(t, tx, instanceID, organizationID)
|
||||
require.NotNil(t, user1ID)
|
||||
user2ID := createHumanUser(t, tx, instanceID, organizationID)
|
||||
require.NotNil(t, user2ID)
|
||||
|
||||
// create authorization with roles for user1 for project1
|
||||
authorizationRepo := repository.AuthorizationRepository()
|
||||
@@ -890,11 +882,12 @@ func TestUpdateAuthorization(t *testing.T) {
|
||||
instanceID := createInstance(t, tx)
|
||||
organizationID := createOrganization(t, tx, instanceID)
|
||||
projectID := createProject(t, tx, instanceID, organizationID)
|
||||
userID := createHumanUser(t, tx, instanceID, organizationID)
|
||||
|
||||
authorizationRepo := repository.AuthorizationRepository()
|
||||
activeAuthorization := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateActive,
|
||||
@@ -907,7 +900,7 @@ func TestUpdateAuthorization(t *testing.T) {
|
||||
|
||||
inactiveAuthorization := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateInactive,
|
||||
@@ -1012,10 +1005,12 @@ func TestAuthorizationUpdate_setRoles(t *testing.T) {
|
||||
role5 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "role5")
|
||||
role6 := createProjectRole(t, tx, instanceID, organizationID, project2ID, "role6")
|
||||
|
||||
userID := createHumanUser(t, tx, instanceID, organizationID)
|
||||
|
||||
authorizationRepo := repository.AuthorizationRepository()
|
||||
authorization1 := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateActive,
|
||||
@@ -1028,7 +1023,7 @@ func TestAuthorizationUpdate_setRoles(t *testing.T) {
|
||||
|
||||
authorization2 := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: project2ID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateActive,
|
||||
@@ -1132,11 +1127,12 @@ func TestDeleteAuthorization(t *testing.T) {
|
||||
projectID := createProject(t, tx, instanceID, organizationID)
|
||||
role1 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role1")
|
||||
role2 := createProjectRole(t, tx, instanceID, organizationID, projectID, "role2")
|
||||
userID := createHumanUser(t, tx, instanceID, organizationID)
|
||||
|
||||
authorizationRepo := repository.AuthorizationRepository()
|
||||
authorizationWithRoles := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateActive,
|
||||
@@ -1149,7 +1145,7 @@ func TestDeleteAuthorization(t *testing.T) {
|
||||
|
||||
authorizationWithoutRoles := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateActive,
|
||||
@@ -1162,7 +1158,7 @@ func TestDeleteAuthorization(t *testing.T) {
|
||||
|
||||
deletedAuthorization := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
InstanceID: instanceID,
|
||||
State: domain.AuthorizationStateActive,
|
||||
@@ -1182,7 +1178,7 @@ func TestDeleteAuthorization(t *testing.T) {
|
||||
grantID := createProjectGrant(t, tx, instanceID, organizationID, grantedOrganizationID, projectID, []string{role1})
|
||||
authorizationProjectGrant := &domain.Authorization{
|
||||
ID: integration.ID(),
|
||||
UserID: integration.ID(),
|
||||
UserID: userID,
|
||||
ProjectID: projectID,
|
||||
GrantID: gu.Ptr(grantID),
|
||||
InstanceID: instanceID,
|
||||
|
||||
@@ -2,7 +2,6 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
@@ -95,7 +94,6 @@ func (i idpIntentRepository) Get(ctx context.Context, client database.QueryExecu
|
||||
var builder database.StatementBuilder
|
||||
builder.WriteString(queryIDPIntentStmt)
|
||||
options.Write(&builder)
|
||||
fmt.Println(builder.String())
|
||||
|
||||
return scanIDPIntent(ctx, client, &builder)
|
||||
}
|
||||
|
||||
@@ -315,6 +315,8 @@ func TestUpdateIDPIntent(t *testing.T) {
|
||||
err = idpIntentRepo.Create(t.Context(), tx, &intent2)
|
||||
require.NoError(t, err)
|
||||
|
||||
userID := createHumanUser(t, tx, instanceID, orgID)
|
||||
|
||||
now := time.Now()
|
||||
tomorrow := now.AddDate(0, 0, 1)
|
||||
tt := []struct {
|
||||
@@ -344,7 +346,7 @@ func TestUpdateIDPIntent(t *testing.T) {
|
||||
idpIntentRepo.SetIDPUser([]byte("update user")),
|
||||
idpIntentRepo.SetIDPUserID("idp user id updated"),
|
||||
idpIntentRepo.SetIDPUsername("idp username updated"),
|
||||
idpIntentRepo.SetUserID("some-user-id"),
|
||||
idpIntentRepo.SetUserID(userID),
|
||||
idpIntentRepo.SetIDPAccessToken([]byte(`{"secret": "g1g4 s3cr3t 4cc3ss t0k3n"}`)),
|
||||
idpIntentRepo.SetIDPIDToken("idp id token"),
|
||||
idpIntentRepo.SetSucceededAt(now),
|
||||
@@ -355,7 +357,7 @@ func TestUpdateIDPIntent(t *testing.T) {
|
||||
toReturn.IDPUser = []byte("update user")
|
||||
toReturn.IDPUserID = "idp user id updated"
|
||||
toReturn.IDPUsername = "idp username updated"
|
||||
toReturn.UserID = "some-user-id"
|
||||
toReturn.UserID = userID
|
||||
toReturn.IDPAccessToken = []byte(`{"secret": "g1g4 s3cr3t 4cc3ss t0k3n"}`)
|
||||
toReturn.IDPIDToken = "idp id token"
|
||||
toReturn.SucceededAt = &now
|
||||
@@ -373,7 +375,7 @@ func TestUpdateIDPIntent(t *testing.T) {
|
||||
idpIntentRepo.SetIDPUser([]byte("update user")),
|
||||
idpIntentRepo.SetIDPUserID("idp user id updated"),
|
||||
idpIntentRepo.SetIDPUsername("idp username updated"),
|
||||
idpIntentRepo.SetUserID("some-user-id"),
|
||||
idpIntentRepo.SetUserID(userID),
|
||||
idpIntentRepo.SetAssertion([]byte(`{"assertion": "val1"}`)),
|
||||
idpIntentRepo.SetSucceededAt(now),
|
||||
idpIntentRepo.SetExpiresAt(tomorrow),
|
||||
@@ -383,7 +385,7 @@ func TestUpdateIDPIntent(t *testing.T) {
|
||||
toReturn.IDPUser = []byte("update user")
|
||||
toReturn.IDPUserID = "idp user id updated"
|
||||
toReturn.IDPUsername = "idp username updated"
|
||||
toReturn.UserID = "some-user-id"
|
||||
toReturn.UserID = userID
|
||||
toReturn.Assertion = []byte(`{"assertion": "val1"}`)
|
||||
toReturn.SucceededAt = &now
|
||||
toReturn.ExpiresAt = &tomorrow
|
||||
@@ -400,7 +402,7 @@ func TestUpdateIDPIntent(t *testing.T) {
|
||||
idpIntentRepo.SetIDPUser([]byte("update user")),
|
||||
idpIntentRepo.SetIDPUserID("idp user id updated"),
|
||||
idpIntentRepo.SetIDPUsername("idp username updated"),
|
||||
idpIntentRepo.SetUserID("some-user-id"),
|
||||
idpIntentRepo.SetUserID(userID),
|
||||
idpIntentRepo.SetIDPEntryAttributes(marshalledAttrs),
|
||||
idpIntentRepo.SetSucceededAt(now),
|
||||
idpIntentRepo.SetExpiresAt(tomorrow),
|
||||
@@ -410,7 +412,7 @@ func TestUpdateIDPIntent(t *testing.T) {
|
||||
toReturn.IDPUser = []byte("update user")
|
||||
toReturn.IDPUserID = "idp user id updated"
|
||||
toReturn.IDPUsername = "idp username updated"
|
||||
toReturn.UserID = "some-user-id"
|
||||
toReturn.UserID = userID
|
||||
toReturn.EntryAttributes = entryAttrs
|
||||
toReturn.SucceededAt = &now
|
||||
toReturn.ExpiresAt = &tomorrow
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
)
|
||||
|
||||
func assertMetadata(t *testing.T, expected, gotten []*domain.Metadata) {
|
||||
t.Helper()
|
||||
|
||||
assert.Len(t, gotten, len(expected), "metadata length mismatch")
|
||||
for _, exp := range expected {
|
||||
var actual *domain.Metadata
|
||||
gotten = slices.DeleteFunc(gotten, func(m *domain.Metadata) bool {
|
||||
if exp.Key != m.Key || exp.InstanceID != m.InstanceID {
|
||||
return false
|
||||
}
|
||||
actual = m
|
||||
return true
|
||||
})
|
||||
require.NotNil(t, actual, "metadata with key %s and instance %s not found", exp.Key, exp.InstanceID)
|
||||
assert.Equal(t, exp.Value, actual.Value, "metadata value mismatch for key %s and instance %s", exp.Key, exp.InstanceID)
|
||||
assert.NotZero(t, actual.CreatedAt, "metadata created at is zero for key %s and instance %s", exp.Key, exp.InstanceID)
|
||||
assert.NotZero(t, actual.UpdatedAt, "metadata updated at is zero for key %s and instance %s", exp.Key, exp.InstanceID)
|
||||
|
||||
}
|
||||
assert.Empty(t, gotten, "unmatched metadata found")
|
||||
}
|
||||
@@ -123,7 +123,9 @@ func (p projectGrant) Update(ctx context.Context, client database.QueryExecutor,
|
||||
|
||||
// now we add the logic to do the required changes based on the given project grant
|
||||
builder.WriteString(updateProjectGrantRoleStmt)
|
||||
database.Changes(changes).Write(builder)
|
||||
if err := database.Changes(changes).Write(builder); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
builder.WriteString(updateProjectGrantRoleStmtWhere)
|
||||
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
|
||||
@@ -85,7 +85,9 @@ func updateOne[Target updatable](ctx context.Context, client database.QueryExecu
|
||||
builder := database.NewStatementBuilder("UPDATE ")
|
||||
builder.WriteString(target.qualifiedTableName())
|
||||
builder.WriteString(" SET ")
|
||||
database.Changes(changes).Write(builder)
|
||||
if err := database.Changes(changes).Write(builder); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
writeCondition(builder, condition)
|
||||
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"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"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres/embedded"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
@@ -31,30 +32,48 @@ func runTests(m *testing.M) int {
|
||||
var err error
|
||||
ctx := context.Background()
|
||||
pool, stop, err = newEmbeddedDB(ctx)
|
||||
defer stop()
|
||||
if err != nil {
|
||||
log.Printf("error with embedded postgres database: %v", err)
|
||||
return 1
|
||||
}
|
||||
defer stop()
|
||||
defer func() {
|
||||
r := recover()
|
||||
pool.Close(ctx)
|
||||
stop()
|
||||
if r != nil {
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func newEmbeddedDB(ctx context.Context) (pool database.PoolTest, stop func(), err error) {
|
||||
connector, stop, err := embedded.StartEmbedded()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to start embedded postgres: %w", err)
|
||||
var connector database.Connector
|
||||
if url := os.Getenv("ZITADEL_TEST_POSTGRES_URL"); url != "" {
|
||||
log.Println("using database provided by env")
|
||||
connector, err = postgres.DecodeConfig(url)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to connect to provided postgres: %w", err)
|
||||
}
|
||||
stop = func() {}
|
||||
} else {
|
||||
connector, stop, err = embedded.StartEmbedded()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to start embedded postgres: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
pool_, err := connector.Connect(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to connect to embedded postgres: %w", err)
|
||||
return nil, stop, fmt.Errorf("unable to connect to embedded postgres: %w", err)
|
||||
}
|
||||
pool = pool_.(database.PoolTest)
|
||||
|
||||
err = pool.MigrateTest(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to migrate database: %w", err)
|
||||
return nil, stop, fmt.Errorf("unable to migrate database: %w", err)
|
||||
}
|
||||
return pool, stop, err
|
||||
}
|
||||
@@ -64,7 +83,8 @@ func transactionForRollback(t *testing.T) (tx database.Transaction, rollback fun
|
||||
tx, err := pool.Begin(t.Context(), nil)
|
||||
require.NoError(t, err)
|
||||
return tx, func() {
|
||||
err := tx.Rollback(t.Context())
|
||||
// context.Background to ensure rollback does not return an error if test is already done
|
||||
err := tx.Rollback(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@@ -74,12 +94,13 @@ func savepointForRollback(t *testing.T, tx database.Transaction) (savepoint data
|
||||
savepoint, err := tx.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
return savepoint, func() {
|
||||
err := savepoint.Rollback(t.Context())
|
||||
// context.Background to ensure rollback does not return an error if test is already done
|
||||
err := savepoint.Rollback(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func createInstance(t *testing.T, tx database.Transaction) (instanceID string) {
|
||||
func createInstance(t *testing.T, tx database.QueryExecutor) (instanceID string) {
|
||||
t.Helper()
|
||||
instance := domain.Instance{
|
||||
ID: gofakeit.UUID(),
|
||||
@@ -97,7 +118,7 @@ func createInstance(t *testing.T, tx database.Transaction) (instanceID string) {
|
||||
return instance.ID
|
||||
}
|
||||
|
||||
func createOrganization(t *testing.T, tx database.Transaction, instanceID string) (orgID string) {
|
||||
func createOrganization(t *testing.T, tx database.QueryExecutor, instanceID string) (orgID string) {
|
||||
t.Helper()
|
||||
org := domain.Organization{
|
||||
InstanceID: instanceID,
|
||||
@@ -112,7 +133,7 @@ func createOrganization(t *testing.T, tx database.Transaction, instanceID string
|
||||
return org.ID
|
||||
}
|
||||
|
||||
func createProject(t *testing.T, tx database.Transaction, instanceID, orgID string) (projectID string) {
|
||||
func createProject(t *testing.T, tx database.QueryExecutor, instanceID, orgID string) (projectID string) {
|
||||
t.Helper()
|
||||
project := domain.Project{
|
||||
InstanceID: instanceID,
|
||||
@@ -128,7 +149,7 @@ func createProject(t *testing.T, tx database.Transaction, instanceID, orgID stri
|
||||
return project.ID
|
||||
}
|
||||
|
||||
func createProjectRole(t *testing.T, tx database.Transaction, instanceID, orgID, projectID, key string) string {
|
||||
func createProjectRole(t *testing.T, tx database.QueryExecutor, instanceID, orgID, projectID, key string) string {
|
||||
t.Helper()
|
||||
if key == "" {
|
||||
key = integration.RoleKey()
|
||||
@@ -210,3 +231,46 @@ func createIDPIntent(t *testing.T, tx database.Transaction, instanceID, idpID st
|
||||
|
||||
return intent.ID
|
||||
}
|
||||
|
||||
func createMachineUser(t *testing.T, tx database.QueryExecutor, instanceID, orgID string) (userID string) {
|
||||
t.Helper()
|
||||
user := domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Username: gofakeit.Username(),
|
||||
State: domain.UserStateActive,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: gofakeit.Name(),
|
||||
},
|
||||
}
|
||||
userRepo := repository.UserRepository()
|
||||
err := userRepo.Create(t.Context(), tx, &user)
|
||||
require.NoError(t, err)
|
||||
return user.ID
|
||||
}
|
||||
|
||||
func createHumanUser(t *testing.T, tx database.QueryExecutor, instanceID, orgID string) string {
|
||||
t.Helper()
|
||||
user := &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: gofakeit.UUID(),
|
||||
Username: gofakeit.Username(),
|
||||
State: domain.UserStateActive,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: gofakeit.FirstName(),
|
||||
LastName: gofakeit.LastName(),
|
||||
Email: domain.HumanEmail{
|
||||
Address: gofakeit.Email(),
|
||||
},
|
||||
Password: domain.HumanPassword{
|
||||
Hash: "hashed-password",
|
||||
},
|
||||
},
|
||||
}
|
||||
userRepo := repository.UserRepository()
|
||||
err := userRepo.Create(t.Context(), tx, user)
|
||||
require.NoError(t, err)
|
||||
return user.ID
|
||||
}
|
||||
|
||||
@@ -176,7 +176,9 @@ func (s session) Create(ctx context.Context, client database.QueryExecutor, sess
|
||||
)
|
||||
fingerprintID = session.UserAgent.FingerprintID
|
||||
}
|
||||
builder.WriteString(`INSERT INTO ` + s.qualifiedTableName() + ` (instance_id, id, lifetime, creator_id, user_agent_id, created_at, updated_at) VALUES ( `)
|
||||
builder.WriteString(`INSERT INTO `)
|
||||
builder.WriteString(s.qualifiedTableName())
|
||||
builder.WriteString(` (instance_id, id, lifetime, creator_id, user_agent_id, created_at, updated_at) VALUES ( `)
|
||||
builder.WriteArgs(session.InstanceID, session.ID, session.Lifetime, session.CreatorID, fingerprintID, createdAt, updatedAt)
|
||||
builder.WriteString(` ) RETURNING created_at, updated_at`)
|
||||
return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&session.CreatedAt, &session.UpdatedAt)
|
||||
|
||||
@@ -232,6 +232,9 @@ func TestSession_Update(t *testing.T) {
|
||||
err = instanceRepo.Create(t.Context(), tx, &instance)
|
||||
require.NoError(t, err)
|
||||
|
||||
orgID := createOrganization(t, tx, instanceId)
|
||||
userID := createHumanUser(t, tx, instanceId, orgID)
|
||||
|
||||
testNow := time.Now()
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -474,7 +477,7 @@ func TestSession_Update(t *testing.T) {
|
||||
// update with updated values
|
||||
session.Factors = []domain.SessionFactor{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: "user-id",
|
||||
UserID: userID,
|
||||
LastVerifiedAt: testNow,
|
||||
},
|
||||
}
|
||||
@@ -482,7 +485,7 @@ func TestSession_Update(t *testing.T) {
|
||||
},
|
||||
update: []database.Change{
|
||||
sessionRepo.SetFactor(&domain.SessionFactorUser{
|
||||
UserID: "user-id",
|
||||
UserID: userID,
|
||||
LastVerifiedAt: testNow,
|
||||
}),
|
||||
},
|
||||
@@ -748,7 +751,7 @@ func TestSession_Update(t *testing.T) {
|
||||
session.TokenID = "new-token"
|
||||
session.Factors = []domain.SessionFactor{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: "user-id",
|
||||
UserID: userID,
|
||||
LastVerifiedAt: testNow,
|
||||
},
|
||||
&domain.SessionFactorPassword{
|
||||
@@ -794,7 +797,7 @@ func TestSession_Update(t *testing.T) {
|
||||
sessionRepo.SetUpdatedAt(testNow),
|
||||
sessionRepo.SetLifetime(time.Hour * 48),
|
||||
sessionRepo.SetFactor(&domain.SessionFactorUser{
|
||||
UserID: "user-id",
|
||||
UserID: userID,
|
||||
LastVerifiedAt: testNow,
|
||||
}),
|
||||
sessionRepo.SetFactor(&domain.SessionFactorPassword{
|
||||
@@ -846,7 +849,7 @@ func TestSession_Update(t *testing.T) {
|
||||
updatedRows, err := sessionRepo.Update(t.Context(), tx,
|
||||
sessionRepo.PrimaryKeyCondition(session.InstanceID, session.ID),
|
||||
sessionRepo.SetFactor(&domain.SessionFactorUser{
|
||||
UserID: "user-id",
|
||||
UserID: userID,
|
||||
LastVerifiedAt: testNow,
|
||||
}),
|
||||
sessionRepo.SetFactor(&domain.SessionFactorPassword{
|
||||
@@ -859,7 +862,7 @@ func TestSession_Update(t *testing.T) {
|
||||
// update with updated values
|
||||
session.Factors = []domain.SessionFactor{
|
||||
&domain.SessionFactorUser{
|
||||
UserID: "user-id",
|
||||
UserID: userID,
|
||||
LastVerifiedAt: testNow,
|
||||
},
|
||||
}
|
||||
@@ -1101,9 +1104,12 @@ func TestSession_List(t *testing.T) {
|
||||
err = instanceRepo.Create(t.Context(), tx, &instance)
|
||||
require.NoError(t, err)
|
||||
|
||||
orgID := createOrganization(t, tx, instanceId)
|
||||
userIDs := []string{createHumanUser(t, tx, instanceId, orgID), createHumanUser(t, tx, instanceId, orgID)}
|
||||
|
||||
// create sessions
|
||||
sessions := make([]*domain.Session, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := range 5 {
|
||||
session := &domain.Session{
|
||||
InstanceID: instanceId,
|
||||
ID: strconv.Itoa(i),
|
||||
@@ -1138,7 +1144,7 @@ func TestSession_List(t *testing.T) {
|
||||
session.Metadata = metadata
|
||||
changes = append(changes, sessionRepo.SetMetadata(metadata))
|
||||
userFactor := &domain.SessionFactorUser{
|
||||
UserID: "user-" + strconv.Itoa(i%2), // two users: user-0 and user-1
|
||||
UserID: userIDs[i%2],
|
||||
LastVerifiedAt: time.Now(),
|
||||
}
|
||||
session.Factors.AppendTo(userFactor)
|
||||
|
||||
@@ -0,0 +1,479 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// repository
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func UserRepository() domain.UserRepository {
|
||||
return new(user)
|
||||
}
|
||||
|
||||
func MachineUserRepository() domain.MachineUserRepository {
|
||||
return &userMachine{user: user{}}
|
||||
}
|
||||
|
||||
type user struct {
|
||||
verification verification
|
||||
tableName string
|
||||
machineKey
|
||||
userPersonalAccessToken
|
||||
userMetadata
|
||||
userPasskey
|
||||
userIdentityProviderLink
|
||||
}
|
||||
|
||||
func (u user) HumanRepository() domain.HumanUserRepository {
|
||||
return userHuman{user: u}
|
||||
}
|
||||
|
||||
// existingUser is used to get the columns and conditions for the CTE that selects the existing user in update and delete operations.
|
||||
var existingUser = user{tableName: "existing_user"}
|
||||
|
||||
// Create implements [domain.UserRepository].
|
||||
func (u user) Create(ctx context.Context, client database.QueryExecutor, user *domain.User) error {
|
||||
var create func(context.Context, *database.StatementBuilder, database.QueryExecutor, *domain.User) error
|
||||
switch {
|
||||
case user.Human != nil:
|
||||
create = userHuman{user: u}.create
|
||||
case user.Machine != nil:
|
||||
create = userMachine{user: u}.create
|
||||
default:
|
||||
return zerrors.ThrowInvalidArgument(nil, "REPOS-KxMnG", "no user type defined")
|
||||
}
|
||||
// we need to cheat a bit here to be able to use CTEChanges
|
||||
// we pretend we have an existing user to be able to use the existing change mechanisms
|
||||
builder := database.NewStatementBuilder("WITH existing_user AS (SELECT $1 AS instance_id, $2 AS organization_id, $3 AS id)", user.InstanceID, user.OrganizationID, user.ID)
|
||||
return create(ctx, builder, client, user)
|
||||
}
|
||||
|
||||
// Delete implements [domain.UserRepository].
|
||||
func (u user) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
||||
if err := checkPKCondition(u, condition); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
builder := database.NewStatementBuilder("DELETE FROM zitadel.users")
|
||||
writeCondition(builder, condition)
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
}
|
||||
|
||||
var queryUserStmt = "SELECT users.instance_id, users.organization_id, users.id, users.username" +
|
||||
", users.state, users.created_at, users.updated_at" +
|
||||
// metadata
|
||||
`, jsonb_agg(DISTINCT jsonb_build_object('instanceId', user_metadata.instance_id, 'key', user_metadata.key, 'value', encode(user_metadata.value, 'base64'), 'createdAt', user_metadata.created_at, 'updatedAt', user_metadata.updated_at)) FILTER (WHERE user_metadata.user_id IS NOT NULL) AS metadata` +
|
||||
// machine
|
||||
`, CASE WHEN users.type = 'machine' THEN jsonb_build_object('name', users.name` +
|
||||
`, 'description', users.description, 'secret', users.secret` +
|
||||
`, 'accessTokenType', users.access_token_type` +
|
||||
`, 'keys', jsonb_agg(DISTINCT jsonb_build_object('id', machine_keys.id, 'publicKey', encode(machine_keys.public_key, 'base64'), 'createdAt', machine_keys.created_at, 'expiresAt', machine_keys.expires_at, 'type', machine_keys.type)) FILTER (WHERE machine_keys.user_id IS NOT NULL)` +
|
||||
`, 'pats', jsonb_agg(DISTINCT jsonb_build_object('id', user_personal_access_tokens.id, 'createdAt', user_personal_access_tokens.created_at, 'expiresAt', user_personal_access_tokens.expires_at, 'scopes', user_personal_access_tokens.scopes)) FILTER (WHERE user_personal_access_tokens.user_id IS NOT NULL)` +
|
||||
`) END AS machine` +
|
||||
// human
|
||||
`, CASE WHEN users.type = 'human' THEN jsonb_build_object('firstName', users.first_name, 'lastName', users.last_name` +
|
||||
`, 'nickname', users.nickname, 'displayName', users.display_name` +
|
||||
`, 'preferredLanguage', users.preferred_language, 'gender', users.gender` +
|
||||
`, 'avatarKey', users.avatar_key` +
|
||||
`, 'multifactorInitializationSkippedAt', users.multifactor_initialization_skipped_at` +
|
||||
`, 'password', jsonb_build_object('hash', users.password_hash, 'isChangeRequired', users.password_change_required, 'changedAt', users.password_changed_at, 'failedAttempts', users.password_failed_attempts, 'lastSuccessfullyCheckedAt', users.password_last_successful_check, 'pendingVerification', ` + verificationQuery(userHuman{}.passwordVerificationIDColumn()) + `)` +
|
||||
`, 'email', jsonb_build_object('address', users.email, 'unverifiedAddress', users.unverified_email, 'verifiedAt', users.email_verified_at, 'otp', jsonb_build_object('enabledAt', users.email_otp_enabled_at, 'lastSuccessfullyCheckedAt', users.email_otp_last_successful_check, 'failedAttempts', users.email_otp_failed_attempts), 'pendingVerification', ` + verificationQuery(userHuman{}.emailVerificationIDColumn()) + `)` +
|
||||
`, 'phone', CASE WHEN users.phone IS NOT NULL OR users.phone_verification_id IS NOT NULL THEN jsonb_build_object('number', users.phone, 'unverifiedNumber', users.unverified_phone, 'verifiedAt', users.phone_verified_at, 'otp', jsonb_build_object('enabledAt', users.sms_otp_enabled_at, 'lastSuccessfullyCheckedAt', users.sms_otp_last_successful_check, 'failedAttempts', users.sms_otp_failed_attempts), 'pendingVerification', ` + verificationQuery(userHuman{}.phoneVerificationIDColumn()) + `) ELSE NULL END` +
|
||||
`, 'totp', CASE WHEN users.totp_secret IS NOT NULL THEN jsonb_build_object('secret', encode(users.totp_secret, 'escape')::JSONB, 'verifiedAt', users.totp_verified_at, 'lastSuccessfullyCheckedAt', users.totp_last_successful_check, 'failedAttempts', users.totp_failed_attempts) ELSE NULL END` +
|
||||
`, 'passkeys', jsonb_agg(DISTINCT jsonb_build_object('id', user_passkeys.token_id, 'keyId', encode(user_passkeys.key_id, 'base64'), 'type', user_passkeys.type, 'name', user_passkeys.name, 'signCount', user_passkeys.sign_count, 'challenge', encode(user_passkeys.challenge, 'base64'), 'publicKey', encode(user_passkeys.public_key, 'base64'), 'attestationType', user_passkeys.attestation_type, 'aaGuid', encode(user_passkeys.authenticator_attestation_guid, 'base64'), 'rpId', user_passkeys.relying_party_id, 'createdAt', user_passkeys.created_at, 'updatedAt', user_passkeys.updated_at, 'verifiedAt', user_passkeys.verified_at)) FILTER (WHERE user_passkeys.user_id IS NOT NULL)` +
|
||||
`, 'verifications', (SELECT jsonb_agg(jsonb_build_object('id', verifications.id, 'code', encode(verifications.code, 'escape')::JSONB, 'createdAt', verifications.created_at, 'updatedAt', verifications.updated_at, 'expiresAt', verifications.created_at+verifications.expiry, 'failedAttempts', verifications.failed_attempts)) FROM zitadel.verifications WHERE verifications.instance_id = users.instance_id AND verifications.user_id = users.id AND verifications.id NOT IN (COALESCE(users.password_verification_id, ''), COALESCE(users.email_verification_id, ''), COALESCE(users.phone_verification_id, ''), COALESCE(users.invite_verification_id, '')))` +
|
||||
`, 'identityProviderLinks', jsonb_agg(DISTINCT jsonb_build_object('providerId', user_identity_provider_links.identity_provider_id, 'providedUserId', user_identity_provider_links.provided_user_id, 'providedUsername', user_identity_provider_links.provided_username, 'createdAt', user_identity_provider_links.created_at, 'updatedAt', user_identity_provider_links.updated_at)) FILTER (WHERE user_identity_provider_links.user_id IS NOT NULL)` +
|
||||
`, 'invite', CASE WHEN users.invite_verification_id IS NOT NULL OR users.invite_accepted_at IS NOT NULL THEN jsonb_build_object('acceptedAt', users.invite_accepted_at, 'failedAttempts', users.invite_failed_attempts, 'pendingVerification', ` + verificationQuery(userHuman{}.inviteVerificationIDColumn()) + `) ELSE NULL END` +
|
||||
`) END AS human FROM zitadel.users`
|
||||
|
||||
func verificationQuery(column database.Column) string {
|
||||
var builder database.StatementBuilder
|
||||
builder.WriteString("CASE WHEN ")
|
||||
column.WriteQualified(&builder)
|
||||
builder.WriteString(` IS NOT NULL THEN (SELECT row_to_json(res.*) FROM (SELECT verifications.id, encode(verifications.code, 'escape')::JSONB AS code, verifications.created_at as "createdAt", verifications.updated_at as "updatedAt", verifications.created_at+verifications.expiry AS "expiresAt", verifications.failed_attempts as "failedAttempts" FROM zitadel.verifications WHERE verifications.instance_id = users.instance_id AND verifications.id = `)
|
||||
column.WriteQualified(&builder)
|
||||
builder.WriteString(`) AS res) ELSE NULL END`)
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// Get implements [domain.UserRepository].
|
||||
func (u user) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.User, error) {
|
||||
options := new(database.QueryOpts)
|
||||
for _, opt := range u.appendQueryOpts(opts) {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
if !options.Condition.IsRestrictingColumn(u.InstanceIDColumn()) {
|
||||
return nil, database.NewMissingConditionError(u.InstanceIDColumn())
|
||||
}
|
||||
|
||||
builder := database.NewStatementBuilder(queryUserStmt)
|
||||
options.Write(builder)
|
||||
|
||||
return scanUser(ctx, client, builder)
|
||||
}
|
||||
|
||||
// List implements [domain.UserRepository].
|
||||
func (u user) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.User, error) {
|
||||
options := new(database.QueryOpts)
|
||||
for _, opt := range u.appendQueryOpts(opts) {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
if !options.Condition.IsRestrictingColumn(u.InstanceIDColumn()) {
|
||||
return nil, database.NewMissingConditionError(u.InstanceIDColumn())
|
||||
}
|
||||
|
||||
builder := database.NewStatementBuilder(queryUserStmt)
|
||||
options.Write(builder)
|
||||
|
||||
return scanUsers(ctx, client, builder)
|
||||
}
|
||||
|
||||
func (u user) appendQueryOpts(opts []database.QueryOption) []database.QueryOption {
|
||||
return append(opts,
|
||||
u.joinMachineKeys(),
|
||||
u.joinPATs(),
|
||||
u.joinMetadata(),
|
||||
u.joinVerifications(),
|
||||
u.joinPasskeys(),
|
||||
u.joinIdentityProviderLinks(),
|
||||
database.WithGroupBy(u.PrimaryKeyColumns()...),
|
||||
)
|
||||
}
|
||||
|
||||
// Update implements [domain.UserRepository].
|
||||
func (u user) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
|
||||
if err := checkPKCondition(u, condition); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
return 0, database.ErrNoChanges
|
||||
}
|
||||
if !database.Changes(changes).IsOnColumn(u.updatedAtColumn()) {
|
||||
changes = append(changes, u.clearUpdatedAt())
|
||||
}
|
||||
|
||||
builder := database.NewStatementBuilder("WITH existing_user AS (SELECT * FROM zitadel.users")
|
||||
writeCondition(builder, condition)
|
||||
builder.WriteString(") ")
|
||||
for i, change := range changes {
|
||||
sessionCTE(change, i, 0, builder)
|
||||
}
|
||||
|
||||
builder.WriteString("UPDATE zitadel.users SET ")
|
||||
if err := database.Changes(changes).Write(builder); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
builder.WriteString(" FROM existing_user")
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(u.IDColumn(), existingUser.IDColumn()),
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), existingUser.InstanceIDColumn()),
|
||||
))
|
||||
|
||||
return client.Exec(ctx, builder.String(), builder.Args()...)
|
||||
}
|
||||
|
||||
func (u user) unqualifiedTableName() string {
|
||||
if u.tableName != "" {
|
||||
return u.tableName
|
||||
}
|
||||
return "users"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetState implements [domain.UserRepository].
|
||||
func (u user) SetState(state domain.UserState) database.Change {
|
||||
return database.NewChange(u.StateColumn(), state)
|
||||
}
|
||||
|
||||
// SetUpdatedAt implements [domain.UserRepository].
|
||||
func (u user) SetUpdatedAt(updatedAt time.Time) database.Change {
|
||||
return database.NewChange(u.updatedAtColumn(), updatedAt)
|
||||
}
|
||||
|
||||
// SetUsername implements [domain.UserRepository].
|
||||
func (u user) SetUsername(username string) database.Change {
|
||||
return database.NewChange(u.UsernameColumn(), username)
|
||||
}
|
||||
|
||||
func (u user) clearUpdatedAt() database.Change {
|
||||
return database.NewChange(u.updatedAtColumn(), database.NullInstruction)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// IDCondition implements [domain.UserRepository].
|
||||
func (u user) IDCondition(userID string) database.Condition {
|
||||
return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID)
|
||||
}
|
||||
|
||||
// InstanceIDCondition implements [domain.UserRepository].
|
||||
func (u user) InstanceIDCondition(instanceID string) database.Condition {
|
||||
return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID)
|
||||
}
|
||||
|
||||
// LoginNameCondition implements [domain.UserRepository].
|
||||
func (u user) LoginNameCondition(op database.TextOperation, loginName string) database.Condition {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// OrganizationIDCondition implements [domain.UserRepository].
|
||||
func (u user) OrganizationIDCondition(orgID string) database.Condition {
|
||||
return database.NewTextCondition(u.organizationIDColumn(), database.TextOperationEqual, orgID)
|
||||
}
|
||||
|
||||
// PrimaryKeyCondition implements [domain.UserRepository].
|
||||
func (u user) PrimaryKeyCondition(instanceID string, userID string) database.Condition {
|
||||
return database.And(
|
||||
u.InstanceIDCondition(instanceID),
|
||||
u.IDCondition(userID),
|
||||
)
|
||||
}
|
||||
|
||||
// StateCondition implements [domain.UserRepository].
|
||||
func (u user) StateCondition(state domain.UserState) database.Condition {
|
||||
return database.NewNumberCondition(u.StateColumn(), database.NumberOperationEqual, state)
|
||||
}
|
||||
|
||||
// TypeCondition implements [domain.UserRepository].
|
||||
func (u user) TypeCondition(userType domain.UserType) database.Condition {
|
||||
return database.NewNumberCondition(u.TypeColumn(), database.NumberOperationEqual, userType)
|
||||
}
|
||||
|
||||
// UsernameCondition implements [domain.UserRepository].
|
||||
func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition {
|
||||
return database.NewTextCondition(u.UsernameColumn(), op, username)
|
||||
}
|
||||
|
||||
// ExistsMetadata implements [domain.UserRepository].
|
||||
func (u user) ExistsMetadata(condition database.Condition) database.Condition {
|
||||
return database.Exists(
|
||||
u.userMetadata.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.userMetadata.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.userMetadata.userIDColumn()),
|
||||
condition,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ExistsPasskey implements [domain.HumanUserRepository].
|
||||
func (u user) ExistsPasskey(condition database.Condition) database.Condition {
|
||||
return database.Exists(
|
||||
u.userPasskey.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.userPasskey.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.userPasskey.userIDColumn()),
|
||||
condition,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ExistsIdentityProviderLink implements [domain.HumanUserRepository].
|
||||
func (u user) ExistsIdentityProviderLink(condition database.Condition) database.Condition {
|
||||
return database.Exists(
|
||||
u.userIdentityProviderLink.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.userIdentityProviderLink.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.userIdentityProviderLink.userIDColumn()),
|
||||
condition,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PrimaryKeyColumns implements [domain.UserRepository].
|
||||
func (u user) PrimaryKeyColumns() []database.Column {
|
||||
return database.Columns{
|
||||
u.InstanceIDColumn(),
|
||||
u.IDColumn(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreatedAtColumn implements [domain.UserRepository].
|
||||
func (u user) CreatedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "created_at")
|
||||
}
|
||||
|
||||
// StateColumn implements [domain.UserRepository].
|
||||
func (u user) StateColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "state")
|
||||
}
|
||||
|
||||
// TypeColumn implements [domain.UserRepository].
|
||||
func (u user) TypeColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "type")
|
||||
}
|
||||
|
||||
// UsernameColumn implements [domain.UserRepository].
|
||||
func (u user) UsernameColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "username")
|
||||
}
|
||||
|
||||
// IDColumn implements [domain.UserRepository].
|
||||
func (u user) IDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "id")
|
||||
}
|
||||
|
||||
func (u user) InstanceIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (u user) updatedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "updated_at")
|
||||
}
|
||||
|
||||
func (u user) organizationIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "organization_id")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// scanners
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type rawUser struct {
|
||||
domain.User
|
||||
Machine *json.RawMessage `json:"machine,omitempty" db:"machine"`
|
||||
Human *json.RawMessage `json:"human,omitempty" db:"human"`
|
||||
}
|
||||
|
||||
func (u *rawUser) toDomain() (*domain.User, error) {
|
||||
if u.Machine != nil {
|
||||
err := json.Unmarshal(*u.Machine, &u.User.Machine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if u.Human != nil {
|
||||
err := json.Unmarshal(*u.Human, &u.User.Human)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &u.User, nil
|
||||
}
|
||||
|
||||
func scanUser(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) (*domain.User, error) {
|
||||
rows, err := client.Query(ctx, builder.String(), builder.Args()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user rawUser
|
||||
if err = rows.(database.CollectableRows).CollectExactlyOneRow(&user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user.toDomain()
|
||||
}
|
||||
|
||||
func scanUsers(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) ([]*domain.User, error) {
|
||||
rows, err := client.Query(ctx, builder.String(), builder.Args()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var users []rawUser
|
||||
if err = rows.(database.CollectableRows).Collect(&users); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*domain.User, len(users))
|
||||
for i, user := range users {
|
||||
domainUser, err := user.toDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[i] = domainUser
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// sub repositories
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// Human implements [domain.UserRepository].
|
||||
func (u user) Human() domain.HumanUserRepository {
|
||||
return userHuman{user: u}
|
||||
}
|
||||
|
||||
// Machine implements [domain.UserRepository].
|
||||
func (u user) Machine() domain.MachineUserRepository {
|
||||
return userMachine{user: u}
|
||||
}
|
||||
|
||||
func (u user) joinMachineKeys() database.QueryOption {
|
||||
return database.WithLeftJoin(
|
||||
u.machineKey.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.machineKey.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.machineKey.userIDColumn()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (u user) joinPATs() database.QueryOption {
|
||||
return database.WithLeftJoin(
|
||||
u.userPersonalAccessToken.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.userPersonalAccessToken.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.userPersonalAccessToken.userIDColumn()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (u user) joinMetadata() database.QueryOption {
|
||||
return database.WithLeftJoin(
|
||||
u.userMetadata.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.userMetadata.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.userMetadata.userIDColumn()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (u user) joinPasskeys() database.QueryOption {
|
||||
return database.WithLeftJoin(
|
||||
u.userPasskey.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.userPasskey.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.userPasskey.userIDColumn()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (u user) joinVerifications() database.QueryOption {
|
||||
return database.WithLeftJoin(
|
||||
u.verification.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.verification.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.verification.userIDColumn()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (u user) joinIdentityProviderLinks() database.QueryOption {
|
||||
return database.WithLeftJoin(
|
||||
u.userIdentityProviderLink.qualifiedTableName(),
|
||||
database.And(
|
||||
database.NewColumnCondition(u.InstanceIDColumn(), u.userIdentityProviderLink.instanceIDColumn()),
|
||||
database.NewColumnCondition(u.IDColumn(), u.userIdentityProviderLink.userIDColumn()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// repository
|
||||
// -------------------------------------------------------------
|
||||
|
||||
type userHuman struct {
|
||||
user
|
||||
}
|
||||
|
||||
var existingHumanUser = userHuman{
|
||||
user: user{
|
||||
tableName: "existing_user",
|
||||
},
|
||||
}
|
||||
|
||||
func HumanUserRepository() domain.HumanUserRepository {
|
||||
return &userHuman{
|
||||
user: user{},
|
||||
}
|
||||
}
|
||||
|
||||
func (u userHuman) create(ctx context.Context, builder *database.StatementBuilder, client database.QueryExecutor, user *domain.User) error {
|
||||
var createdAt any = database.NowInstruction
|
||||
if !user.CreatedAt.IsZero() {
|
||||
createdAt = user.CreatedAt
|
||||
}
|
||||
columnValues := map[string]any{
|
||||
"instance_id": user.InstanceID,
|
||||
"organization_id": user.OrganizationID,
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"state": user.State,
|
||||
"type": "human",
|
||||
"first_name": user.Human.FirstName,
|
||||
"last_name": user.Human.LastName,
|
||||
"created_at": createdAt,
|
||||
"updated_at": createdAt,
|
||||
}
|
||||
if !user.Human.MultifactorInitializationSkippedAt.IsZero() {
|
||||
columnValues["multifactor_initialization_skipped_at"] = user.Human.MultifactorInitializationSkippedAt
|
||||
}
|
||||
columnValues = u.setOptionalProfileColumns(columnValues, user.Human)
|
||||
|
||||
ctes := make(map[string]database.CTEChange)
|
||||
|
||||
columnValues, ctes = u.setPasswordColumns(columnValues, ctes, user, createdAt)
|
||||
columnValues, ctes = u.setEmailColumns(columnValues, ctes, user, createdAt)
|
||||
columnValues, ctes = u.setPhoneColumns(columnValues, ctes, user, createdAt)
|
||||
columnValues, ctes = u.setInviteColumns(columnValues, ctes, user, createdAt)
|
||||
|
||||
for i, passkey := range user.Human.Passkeys {
|
||||
ctes[fmt.Sprintf("passkey_%d", i)] = u.AddPasskey(passkey).(database.CTEChange)
|
||||
}
|
||||
|
||||
if user.Human.TOTP != nil {
|
||||
columnValues["totp_secret"] = user.Human.TOTP.Secret
|
||||
if !user.Human.TOTP.VerifiedAt.IsZero() {
|
||||
columnValues["totp_verified_at"] = user.Human.TOTP.VerifiedAt
|
||||
}
|
||||
}
|
||||
|
||||
for i, link := range user.Human.IdentityProviderLinks {
|
||||
name := fmt.Sprintf("idp_link_%d", i)
|
||||
ctes[name] = u.AddIdentityProviderLink(link).(database.CTEChange)
|
||||
}
|
||||
|
||||
for i, verification := range user.Human.Verifications {
|
||||
ctes[fmt.Sprintf("verification_%d", i)] = u.SetVerification(&domain.VerificationTypeInit{
|
||||
ID: gu.Ptr(verification.ID),
|
||||
CreatedAt: user.CreatedAt,
|
||||
Code: verification.Code,
|
||||
}).(database.CTEChange)
|
||||
}
|
||||
|
||||
for i, metadata := range user.Metadata {
|
||||
ctes[fmt.Sprintf("metadata_%d", i)] = u.SetMetadata(metadata).(database.CTEChange)
|
||||
}
|
||||
|
||||
// write CTE changes
|
||||
for name, cte := range ctes {
|
||||
cte.SetName(name)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString(name)
|
||||
builder.WriteString(" AS (")
|
||||
cte.WriteCTE(builder)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
columns := slices.Sorted(maps.Keys(columnValues))
|
||||
|
||||
// write final insert
|
||||
builder.WriteString(" INSERT INTO zitadel.users (")
|
||||
builder.WriteString(strings.Join(columns, ", "))
|
||||
builder.WriteString(") VALUES (")
|
||||
for i, column := range columns {
|
||||
if i > 0 {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
value := columnValues[column]
|
||||
if fn, ok := value.(func(builder *database.StatementBuilder)); ok {
|
||||
fn(builder)
|
||||
continue
|
||||
}
|
||||
builder.WriteArg(value)
|
||||
}
|
||||
builder.WriteString(") RETURNING created_at, updated_at")
|
||||
|
||||
return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt)
|
||||
}
|
||||
|
||||
func (u userHuman) setPasswordColumns(columnValues map[string]any, ctes map[string]database.CTEChange, user *domain.User, createdAt any) (map[string]any, map[string]database.CTEChange) {
|
||||
if user.Human.Password.PendingVerification != nil {
|
||||
verification := &domain.VerificationTypeInit{
|
||||
CreatedAt: user.CreatedAt,
|
||||
Code: user.Human.Password.PendingVerification.Code,
|
||||
}
|
||||
if user.Human.Password.PendingVerification.ID != "" {
|
||||
verification.ID = &user.Human.Password.PendingVerification.ID
|
||||
}
|
||||
if user.Human.Password.PendingVerification.ExpiresAt != nil && !user.Human.Password.PendingVerification.ExpiresAt.IsZero() {
|
||||
verification.Expiry = gu.Ptr(time.Since(*user.Human.Password.PendingVerification.ExpiresAt))
|
||||
}
|
||||
|
||||
ctes["password_verification"] = u.SetResetPasswordVerification(verification).(database.CTEChange)
|
||||
columnValues["password_verification_id"] = func(builder *database.StatementBuilder) {
|
||||
builder.WriteString(`(SELECT id FROM password_verification)`)
|
||||
}
|
||||
} else if user.Human.Password.Hash != "" {
|
||||
columnValues["password_hash"] = user.Human.Password.Hash
|
||||
columnValues["password_changed_at"] = createdAt
|
||||
if !user.Human.Password.ChangedAt.IsZero() {
|
||||
columnValues["password_changed_at"] = user.Human.Password.ChangedAt
|
||||
}
|
||||
}
|
||||
return columnValues, ctes
|
||||
}
|
||||
|
||||
func (u userHuman) setEmailColumns(columnValues map[string]any, ctes map[string]database.CTEChange, user *domain.User, createdAt any) (map[string]any, map[string]database.CTEChange) {
|
||||
if user.Human.Email.PendingVerification != nil {
|
||||
verification := &domain.VerificationTypeInit{
|
||||
CreatedAt: user.CreatedAt,
|
||||
Code: user.Human.Email.PendingVerification.Code,
|
||||
}
|
||||
if user.Human.Email.PendingVerification.ID != "" {
|
||||
verification.ID = &user.Human.Email.PendingVerification.ID
|
||||
}
|
||||
if user.Human.Email.PendingVerification.ExpiresAt != nil && !user.Human.Email.PendingVerification.ExpiresAt.IsZero() {
|
||||
verification.Expiry = gu.Ptr(time.Since(*user.Human.Email.PendingVerification.ExpiresAt))
|
||||
}
|
||||
|
||||
ctes["email_verification"] = u.SetEmailVerification(verification).(database.CTEChange)
|
||||
columnValues["email_verification_id"] = func(builder *database.StatementBuilder) {
|
||||
builder.WriteString(`(SELECT id FROM email_verification)`)
|
||||
}
|
||||
}
|
||||
if user.Human.Email.Address != "" {
|
||||
columnValues["email"] = user.Human.Email.Address
|
||||
columnValues["unverified_email"] = user.Human.Email.Address
|
||||
columnValues["email_verified_at"] = createdAt
|
||||
if !user.Human.Email.VerifiedAt.IsZero() {
|
||||
columnValues["email_verified_at"] = user.Human.Email.VerifiedAt
|
||||
}
|
||||
}
|
||||
if user.Human.Email.UnverifiedAddress != "" {
|
||||
columnValues["unverified_email"] = user.Human.Email.UnverifiedAddress
|
||||
}
|
||||
return columnValues, ctes
|
||||
}
|
||||
|
||||
func (u userHuman) setPhoneColumns(columnValues map[string]any, ctes map[string]database.CTEChange, user *domain.User, createdAt any) (map[string]any, map[string]database.CTEChange) {
|
||||
if user.Human.Phone == nil {
|
||||
return columnValues, ctes
|
||||
}
|
||||
if user.Human.Phone.PendingVerification != nil {
|
||||
verification := &domain.VerificationTypeInit{
|
||||
CreatedAt: user.CreatedAt,
|
||||
Code: user.Human.Phone.PendingVerification.Code,
|
||||
}
|
||||
if user.Human.Phone.PendingVerification.ID != "" {
|
||||
verification.ID = &user.Human.Phone.PendingVerification.ID
|
||||
}
|
||||
if user.Human.Phone.PendingVerification.ExpiresAt != nil && !user.Human.Phone.PendingVerification.ExpiresAt.IsZero() {
|
||||
verification.Expiry = gu.Ptr(time.Since(*user.Human.Phone.PendingVerification.ExpiresAt))
|
||||
}
|
||||
|
||||
ctes["phone_verification"] = u.SetPhoneVerification(verification).(database.CTEChange)
|
||||
columnValues["phone_verification_id"] = func(builder *database.StatementBuilder) {
|
||||
builder.WriteString(`(SELECT id FROM phone_verification)`)
|
||||
}
|
||||
}
|
||||
if user.Human.Phone.Number != "" {
|
||||
columnValues["phone"] = user.Human.Phone.Number
|
||||
columnValues["unverified_phone"] = user.Human.Phone.Number
|
||||
columnValues["phone_verified_at"] = createdAt
|
||||
if !user.Human.Phone.VerifiedAt.IsZero() {
|
||||
columnValues["phone_verified_at"] = user.Human.Phone.VerifiedAt
|
||||
}
|
||||
}
|
||||
if user.Human.Phone.UnverifiedNumber != "" {
|
||||
columnValues["unverified_phone"] = user.Human.Phone.UnverifiedNumber
|
||||
}
|
||||
return columnValues, ctes
|
||||
}
|
||||
|
||||
func (u userHuman) setInviteColumns(columnValues map[string]any, ctes map[string]database.CTEChange, user *domain.User, createdAt any) (map[string]any, map[string]database.CTEChange) {
|
||||
if user.Human.Invite == nil {
|
||||
return columnValues, ctes
|
||||
}
|
||||
if user.Human.Invite.PendingVerification != nil {
|
||||
verification := &domain.VerificationTypeInit{
|
||||
CreatedAt: user.CreatedAt,
|
||||
Code: user.Human.Invite.PendingVerification.Code,
|
||||
}
|
||||
if user.Human.Invite.PendingVerification.ID != "" {
|
||||
verification.ID = &user.Human.Invite.PendingVerification.ID
|
||||
}
|
||||
if user.Human.Invite.PendingVerification.ExpiresAt != nil && !user.Human.Invite.PendingVerification.ExpiresAt.IsZero() {
|
||||
verification.Expiry = gu.Ptr(time.Since(*user.Human.Invite.PendingVerification.ExpiresAt))
|
||||
}
|
||||
ctes["invite_verification"] = u.SetInviteVerification(verification).(database.CTEChange)
|
||||
}
|
||||
if !user.Human.Invite.AcceptedAt.IsZero() {
|
||||
columnValues["invite_accepted_at"] = createdAt
|
||||
}
|
||||
return columnValues, ctes
|
||||
}
|
||||
|
||||
func (u userHuman) setOptionalProfileColumns(columnValues map[string]any, human *domain.HumanUser) map[string]any {
|
||||
if !human.PreferredLanguage.IsRoot() {
|
||||
columnValues["preferred_language"] = human.PreferredLanguage
|
||||
}
|
||||
if human.DisplayName != "" {
|
||||
columnValues["display_name"] = human.DisplayName
|
||||
}
|
||||
if human.Nickname != "" {
|
||||
columnValues["nickname"] = human.Nickname
|
||||
}
|
||||
if human.Gender != domain.HumanGenderUnspecified {
|
||||
columnValues["gender"] = human.Gender
|
||||
}
|
||||
if human.AvatarKey != "" {
|
||||
columnValues["avatar_key"] = human.AvatarKey
|
||||
}
|
||||
if human.Password.IsChangeRequired {
|
||||
columnValues["password_change_required"] = true
|
||||
}
|
||||
return columnValues
|
||||
}
|
||||
|
||||
func (u userHuman) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
|
||||
if !condition.IsRestrictingColumn(u.TypeColumn()) {
|
||||
condition = database.And(condition, u.TypeCondition(domain.UserTypeHuman))
|
||||
}
|
||||
return u.user.Update(ctx, client, condition, changes...)
|
||||
}
|
||||
|
||||
var _ domain.HumanUserRepository = (*userHuman)(nil)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetAvatarKey implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetAvatarKey(avatarKey *string) database.Change {
|
||||
return database.NewChangePtr(u.avatarKeyColumn(), avatarKey)
|
||||
}
|
||||
|
||||
// SetDisplayName implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetDisplayName(displayName string) database.Change {
|
||||
return database.NewChange(u.DisplayNameColumn(), displayName)
|
||||
}
|
||||
|
||||
// SetFirstName implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetFirstName(firstName string) database.Change {
|
||||
return database.NewChange(u.FirstNameColumn(), firstName)
|
||||
}
|
||||
|
||||
// SetGender implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetGender(gender domain.HumanGender) database.Change {
|
||||
return database.NewChange(u.genderColumn(), gender)
|
||||
}
|
||||
|
||||
// SetLastName implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetLastName(lastName string) database.Change {
|
||||
return database.NewChange(u.LastNameColumn(), lastName)
|
||||
}
|
||||
|
||||
// SkipMultifactorInitializationAt implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SkipMultifactorInitializationAt(skippedAt time.Time) database.Change {
|
||||
if skippedAt.IsZero() {
|
||||
return u.SkipMultifactorInitialization()
|
||||
}
|
||||
return database.NewChange(u.multifactorInitializationSkippedAtColumn(), skippedAt)
|
||||
}
|
||||
|
||||
// SkipMultifactorInitialization implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SkipMultifactorInitialization() database.Change {
|
||||
return database.NewChange(u.multifactorInitializationSkippedAtColumn(), database.NowInstruction)
|
||||
}
|
||||
|
||||
// SetNickname implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetNickname(nickname string) database.Change {
|
||||
return database.NewChange(u.NicknameColumn(), nickname)
|
||||
}
|
||||
|
||||
// SetPreferredLanguage implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetPreferredLanguage(preferredLanguage language.Tag) database.Change {
|
||||
if preferredLanguage == language.Und {
|
||||
return database.NewChangeToNull(u.preferredLanguageColumn())
|
||||
}
|
||||
return database.NewChange(u.preferredLanguageColumn(), preferredLanguage.String())
|
||||
}
|
||||
|
||||
// SetVerification implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetVerification(verification domain.VerificationType) database.Change {
|
||||
switch typ := verification.(type) {
|
||||
case *domain.VerificationTypeInit:
|
||||
return database.NewCTEChange(func(builder *database.StatementBuilder) {
|
||||
var (
|
||||
createdAt any = database.NowInstruction
|
||||
expiry any = database.NullInstruction
|
||||
id any = database.DefaultInstruction
|
||||
)
|
||||
if !typ.CreatedAt.IsZero() {
|
||||
createdAt = typ.CreatedAt
|
||||
}
|
||||
if typ.Expiry != nil {
|
||||
expiry = *typ.Expiry
|
||||
}
|
||||
if typ.ID != nil {
|
||||
id = *typ.ID
|
||||
}
|
||||
builder.WriteString("INSERT INTO zitadel.verifications(instance_id, user_id, id, code, created_at, expiry) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(id, typ.Code, createdAt, expiry)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingHumanUser.unqualifiedTableName())
|
||||
builder.WriteString(" RETURNING id")
|
||||
}, nil)
|
||||
case *domain.VerificationTypeSucceeded:
|
||||
return database.NewCTEChange(func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM zitadel.verifications USING existing_user")
|
||||
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(u.verification.instanceIDColumn(), existingHumanUser.InstanceIDColumn()),
|
||||
database.NewColumnCondition(u.verification.userIDColumn(), existingHumanUser.IDColumn()),
|
||||
database.NewTextCondition(u.verification.idColumn(), database.TextOperationEqual, *typ.ID),
|
||||
))
|
||||
}, nil)
|
||||
|
||||
case *domain.VerificationTypeUpdate:
|
||||
changes := make(database.Changes, 0, 3)
|
||||
if typ.Code != nil {
|
||||
changes = append(changes, database.NewChange(u.verification.codeColumn(), typ.Code))
|
||||
}
|
||||
if typ.Expiry != nil {
|
||||
changes = append(changes, database.NewChange(u.verification.expiryColumn(), *typ.Expiry))
|
||||
}
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("UPDATE zitadel.verifications SET ")
|
||||
err := changes.Write(builder)
|
||||
logging.New(logging.StreamRuntime).Debug("write changes in cte failed", "error", err)
|
||||
builder.WriteString(" FROM existing_user")
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(u.verification.instanceIDColumn(), existingHumanUser.InstanceIDColumn()),
|
||||
database.NewColumnCondition(u.verification.userIDColumn(), existingHumanUser.IDColumn()),
|
||||
database.NewTextCondition(u.verification.idColumn(), database.TextOperationEqual, *typ.ID),
|
||||
))
|
||||
},
|
||||
nil,
|
||||
)
|
||||
case *domain.VerificationTypeSkipped:
|
||||
panic("skip verification is not supported for verifications")
|
||||
case *domain.VerificationTypeFailed:
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("UPDATE ")
|
||||
builder.WriteString(u.verification.qualifiedTableName())
|
||||
builder.WriteString(" SET ")
|
||||
err := database.NewIncrementColumnChange(u.verification.failedAttemptsColumn(), database.Coalesce(u.verification.failedAttemptsColumn(), 0)).Write(builder)
|
||||
logging.New(logging.StreamRuntime).Debug("write cte failed", "error", err)
|
||||
builder.WriteString(" FROM existing_user")
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(u.verification.instanceIDColumn(), existingHumanUser.InstanceIDColumn()),
|
||||
database.NewColumnCondition(u.verification.userIDColumn(), existingHumanUser.IDColumn()),
|
||||
database.NewTextCondition(u.verification.idColumn(), database.TextOperationEqual, *typ.ID),
|
||||
))
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
panic(fmt.Sprintf("undefined verification type %T", verification))
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// DisplayNameCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) DisplayNameCondition(op database.TextOperation, displayName string) database.Condition {
|
||||
return database.NewTextCondition(u.DisplayNameColumn(), op, displayName)
|
||||
}
|
||||
|
||||
// FirstNameCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) FirstNameCondition(op database.TextOperation, firstName string) database.Condition {
|
||||
return database.NewTextCondition(u.FirstNameColumn(), op, firstName)
|
||||
}
|
||||
|
||||
// LastNameCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) LastNameCondition(op database.TextOperation, lastName string) database.Condition {
|
||||
return database.NewTextCondition(u.LastNameColumn(), op, lastName)
|
||||
}
|
||||
|
||||
// NicknameCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) NicknameCondition(op database.TextOperation, nickname string) database.Condition {
|
||||
return database.NewTextCondition(u.NicknameColumn(), op, nickname)
|
||||
}
|
||||
|
||||
// PreferredLanguageCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) PreferredLanguageCondition(lang language.Tag) database.Condition {
|
||||
return database.NewTextCondition(u.preferredLanguageColumn(), database.TextOperationEqual, lang.String())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// DisplayNameColumn implements [domain.HumanUserRepository].
|
||||
func (u userHuman) DisplayNameColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "display_name")
|
||||
}
|
||||
|
||||
func (u userHuman) genderColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "gender")
|
||||
}
|
||||
|
||||
func (u userHuman) avatarKeyColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "avatar_key")
|
||||
}
|
||||
|
||||
// FirstNameColumn implements [domain.HumanUserRepository].
|
||||
func (u userHuman) FirstNameColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "first_name")
|
||||
}
|
||||
|
||||
// LastNameColumn implements [domain.HumanUserRepository].
|
||||
func (u userHuman) LastNameColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "last_name")
|
||||
}
|
||||
|
||||
// NicknameColumn implements [domain.HumanUserRepository].
|
||||
func (u userHuman) NicknameColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "nickname")
|
||||
}
|
||||
|
||||
func (u userHuman) multifactorInitializationSkippedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "multifactor_initialization_skipped_at")
|
||||
}
|
||||
|
||||
func (u userHuman) preferredLanguageColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "preferred_language")
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// scanners
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// sub repositories
|
||||
// -------------------------------------------------------------
|
||||
@@ -0,0 +1,132 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userHuman) SetEmail(email string) database.Change {
|
||||
return database.NewChange(u.EmailColumn(), email)
|
||||
}
|
||||
|
||||
func (u userHuman) SetUnverifiedEmail(email string) database.Change {
|
||||
return database.NewChange(u.unverifiedEmailColumn(), email)
|
||||
}
|
||||
|
||||
// SetEmailVerification implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetEmailVerification(verification domain.VerificationType) database.Change {
|
||||
switch typ := verification.(type) {
|
||||
case *domain.VerificationTypeInit:
|
||||
return u.verification.init(typ, existingHumanUser.unqualifiedTableName(), existingHumanUser.emailVerificationIDColumn())
|
||||
case *domain.VerificationTypeUpdate:
|
||||
return u.verification.update(typ, existingHumanUser.unqualifiedTableName(),
|
||||
existingHumanUser.InstanceIDColumn(), existingHumanUser.emailVerificationIDColumn())
|
||||
case *domain.VerificationTypeSucceeded:
|
||||
return database.NewChanges(
|
||||
u.verification.verified(typ, existingUser.unqualifiedTableName(), existingUser.InstanceIDColumn(),
|
||||
existingHumanUser.emailVerificationIDColumn(), u.emailVerifiedAtColumn(), u.failedEmailOTPAttemptsColumn(),
|
||||
),
|
||||
database.NewChangeToColumn(u.EmailColumn(), u.unverifiedEmailColumn()),
|
||||
)
|
||||
case *domain.VerificationTypeFailed:
|
||||
return u.verification.failed(existingHumanUser.unqualifiedTableName(), existingHumanUser.InstanceIDColumn(), existingHumanUser.emailVerificationIDColumn())
|
||||
case *domain.VerificationTypeSkipped:
|
||||
return u.verification.skipped(typ, u.emailVerifiedAtColumn(), u.emailVerificationIDColumn(), u.failedEmailOTPAttemptsColumn())
|
||||
}
|
||||
panic(fmt.Sprintf("type not allowed for email verification change %T", verification))
|
||||
}
|
||||
|
||||
// EnableEmailOTPAt implements [domain.HumanUserRepository].
|
||||
func (u userHuman) EnableEmailOTPAt(enabledAt time.Time) database.Change {
|
||||
return database.NewChange(u.emailOTPEnabledAtColumn(), enabledAt)
|
||||
}
|
||||
|
||||
// EnableEmailOTP implements [domain.HumanUserRepository].
|
||||
func (u userHuman) EnableEmailOTP() database.Change {
|
||||
return database.NewChange(u.emailOTPEnabledAtColumn(), database.NowInstruction)
|
||||
}
|
||||
|
||||
// DisableEmailOTP implements [domain.HumanUserRepository].
|
||||
func (u userHuman) DisableEmailOTP() database.Change {
|
||||
return database.NewChangeToNull(u.emailOTPEnabledAtColumn())
|
||||
}
|
||||
|
||||
func (u userHuman) SetLastSuccessfulEmailOTPCheck(checkedAt time.Time) database.Change {
|
||||
if checkedAt.IsZero() {
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulEmailOTPCheckColumn(), database.NowInstruction),
|
||||
u.ResetEmailOTPFailedAttempts(),
|
||||
)
|
||||
}
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulEmailOTPCheckColumn(), checkedAt),
|
||||
u.ResetEmailOTPFailedAttempts(),
|
||||
)
|
||||
}
|
||||
|
||||
func (u userHuman) IncrementEmailOTPFailedAttempts() database.Change {
|
||||
return database.NewIncrementColumnChange(u.failedEmailOTPAttemptsColumn(), database.Coalesce(u.failedEmailOTPAttemptsColumn(), 0))
|
||||
}
|
||||
|
||||
func (u userHuman) ResetEmailOTPFailedAttempts() database.Change {
|
||||
return database.NewChange(u.failedEmailOTPAttemptsColumn(), 0)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// EmailCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) EmailCondition(op database.TextOperation, email string) database.Condition {
|
||||
return database.Or(u.VerifiedEmailCondition(op, email), u.UnverifiedEmailCondition(op, email))
|
||||
}
|
||||
|
||||
// UnverifiedEmailCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) UnverifiedEmailCondition(op database.TextOperation, email string) database.Condition {
|
||||
return database.NewTextCondition(u.unverifiedEmailColumn(), op, email)
|
||||
}
|
||||
|
||||
// VerifiedEmailCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) VerifiedEmailCondition(op database.TextOperation, email string) database.Condition {
|
||||
return database.NewTextCondition(u.EmailColumn(), op, email)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// EmailColumn implements [domain.HumanUserRepository].
|
||||
func (u userHuman) EmailColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "email")
|
||||
}
|
||||
|
||||
func (u userHuman) unverifiedEmailColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "unverified_email")
|
||||
}
|
||||
|
||||
func (u userHuman) emailOTPEnabledAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "email_otp_enabled_at")
|
||||
}
|
||||
|
||||
func (u userHuman) emailVerifiedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "email_verified_at")
|
||||
}
|
||||
|
||||
func (u userHuman) emailVerificationIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "email_verification_id")
|
||||
}
|
||||
|
||||
func (u userHuman) failedEmailOTPAttemptsColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "email_otp_failed_attempts")
|
||||
}
|
||||
|
||||
func (u userHuman) lastSuccessfulEmailOTPCheckColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "email_otp_last_successful_check")
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userHuman) SetPassword(password string) database.Change {
|
||||
return database.NewChange(u.passwordHashColumn(), password)
|
||||
}
|
||||
|
||||
// SetResetPasswordVerification implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetResetPasswordVerification(verification domain.VerificationType) database.Change {
|
||||
switch typ := verification.(type) {
|
||||
case *domain.VerificationTypeInit:
|
||||
return u.verification.init(typ, existingUser.unqualifiedTableName(), existingHumanUser.passwordVerificationIDColumn())
|
||||
case *domain.VerificationTypeUpdate:
|
||||
return u.verification.update(typ, existingHumanUser.unqualifiedTableName(),
|
||||
existingHumanUser.InstanceIDColumn(), existingHumanUser.passwordVerificationIDColumn(),
|
||||
)
|
||||
case *domain.VerificationTypeSucceeded:
|
||||
return u.verification.verified(typ, existingHumanUser.unqualifiedTableName(), existingHumanUser.InstanceIDColumn(),
|
||||
existingHumanUser.passwordVerificationIDColumn(), u.passwordVerifiedAtColumn(), u.failedPasswordAttemptsColumn(),
|
||||
)
|
||||
case *domain.VerificationTypeSkipped:
|
||||
return u.verification.skipped(typ, u.passwordVerifiedAtColumn(), u.passwordVerificationIDColumn(), u.failedPasswordAttemptsColumn())
|
||||
case *domain.VerificationTypeFailed:
|
||||
return u.verification.failed(existingHumanUser.unqualifiedTableName(), existingHumanUser.InstanceIDColumn(), existingHumanUser.passwordVerificationIDColumn())
|
||||
}
|
||||
panic(fmt.Sprintf("undefined verification type %T", verification))
|
||||
}
|
||||
|
||||
// SetPasswordChangeRequired implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetPasswordChangeRequired(required bool) database.Change {
|
||||
return database.NewChange(u.passwordChangeRequiredColumn(), required)
|
||||
}
|
||||
|
||||
func (u userHuman) SetLastSuccessfulPasswordCheck(checkedAt time.Time) database.Change {
|
||||
if checkedAt.IsZero() {
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulPasswordCheckColumn(), database.NowInstruction),
|
||||
u.ResetPasswordFailedAttempts(),
|
||||
)
|
||||
}
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulPasswordCheckColumn(), checkedAt),
|
||||
u.ResetPasswordFailedAttempts(),
|
||||
)
|
||||
}
|
||||
|
||||
func (u userHuman) IncrementPasswordFailedAttempts() database.Change {
|
||||
return database.NewIncrementColumnChange(u.failedPasswordAttemptsColumn(), database.Coalesce(u.failedPasswordAttemptsColumn(), 0))
|
||||
}
|
||||
|
||||
func (u userHuman) ResetPasswordFailedAttempts() database.Change {
|
||||
return database.NewChange(u.failedPasswordAttemptsColumn(), 0)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userHuman) passwordHashColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "password_hash")
|
||||
}
|
||||
|
||||
func (u userHuman) passwordChangeRequiredColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "password_change_required")
|
||||
}
|
||||
|
||||
func (u userHuman) passwordVerifiedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "password_changed_at")
|
||||
}
|
||||
|
||||
func (u userHuman) passwordVerificationIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "password_verification_id")
|
||||
}
|
||||
|
||||
func (u userHuman) lastSuccessfulPasswordCheckColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "password_last_successful_check")
|
||||
}
|
||||
|
||||
func (u userHuman) failedPasswordAttemptsColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "password_failed_attempts")
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetPhone implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetPhone(phone string) database.Change {
|
||||
return database.NewChange(u.phoneColumn(), phone)
|
||||
}
|
||||
|
||||
// SetUnverifiedPhone implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetUnverifiedPhone(phone string) database.Change {
|
||||
return database.NewChange(u.unverifiedPhoneColumn(), phone)
|
||||
}
|
||||
|
||||
// SetPhoneVerification implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetPhoneVerification(verification domain.VerificationType) database.Change {
|
||||
switch typ := verification.(type) {
|
||||
case *domain.VerificationTypeInit:
|
||||
return u.verification.init(typ, existingHumanUser.unqualifiedTableName(), existingHumanUser.phoneVerificationIDColumn())
|
||||
case *domain.VerificationTypeUpdate:
|
||||
return u.verification.update(typ, existingHumanUser.unqualifiedTableName(),
|
||||
existingHumanUser.InstanceIDColumn(), existingHumanUser.phoneVerificationIDColumn(),
|
||||
)
|
||||
case *domain.VerificationTypeSucceeded:
|
||||
return database.NewChanges(
|
||||
u.verification.verified(typ, existingHumanUser.unqualifiedTableName(), existingHumanUser.InstanceIDColumn(),
|
||||
existingHumanUser.phoneVerificationIDColumn(), u.phoneVerifiedAtColumn(), u.failedSMSOTPAttemptsColumn(),
|
||||
),
|
||||
database.NewChangeToColumn(u.phoneColumn(), u.unverifiedPhoneColumn()),
|
||||
)
|
||||
case *domain.VerificationTypeFailed:
|
||||
return u.verification.failed(existingHumanUser.unqualifiedTableName(), existingHumanUser.InstanceIDColumn(), existingHumanUser.phoneVerificationIDColumn())
|
||||
case *domain.VerificationTypeSkipped:
|
||||
return u.verification.skipped(typ, u.phoneVerifiedAtColumn(), u.phoneVerificationIDColumn(), u.failedSMSOTPAttemptsColumn())
|
||||
}
|
||||
panic(fmt.Sprintf("type not allowed for phone verification change %T", verification))
|
||||
}
|
||||
|
||||
// RemovePhone implements [domain.HumanUserRepository].
|
||||
func (u userHuman) RemovePhone() database.Change {
|
||||
return database.NewChanges(
|
||||
database.NewChangeToNull(u.phoneColumn()),
|
||||
database.NewChangeToNull(u.phoneVerifiedAtColumn()),
|
||||
database.NewChangeToNull(u.phoneVerificationIDColumn()),
|
||||
database.NewChangeToNull(u.smsOTPEnabledAtColumn()),
|
||||
database.NewChangeToNull(u.lastSuccessfulSMSOTPCheckColumn()),
|
||||
database.NewChangeToNull(u.failedSMSOTPAttemptsColumn()),
|
||||
)
|
||||
}
|
||||
|
||||
// EnableSMSOTPAt implements [domain.HumanUserRepository].
|
||||
func (u userHuman) EnableSMSOTPAt(enabledAt time.Time) database.Change {
|
||||
return database.NewChange(u.smsOTPEnabledAtColumn(), enabledAt)
|
||||
}
|
||||
|
||||
// EnableSMSOTP implements [domain.HumanUserRepository].
|
||||
func (u userHuman) EnableSMSOTP() database.Change {
|
||||
return database.NewChange(u.smsOTPEnabledAtColumn(), database.NowInstruction)
|
||||
}
|
||||
|
||||
// DisableSMSOTP implements [domain.HumanUserRepository].
|
||||
func (u userHuman) DisableSMSOTP() database.Change {
|
||||
return database.NewChangeToNull(u.smsOTPEnabledAtColumn())
|
||||
}
|
||||
|
||||
func (u userHuman) SetLastSuccessfulSMSOTPCheck(checkedAt time.Time) database.Change {
|
||||
if checkedAt.IsZero() {
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulSMSOTPCheckColumn(), database.NowInstruction),
|
||||
u.ResetSMSOTPFailedAttempts(),
|
||||
)
|
||||
}
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulSMSOTPCheckColumn(), checkedAt),
|
||||
u.ResetSMSOTPFailedAttempts(),
|
||||
)
|
||||
}
|
||||
|
||||
// IncrementSMSOTPFailedAttempts implements [domain.HumanUserRepository].
|
||||
func (u userHuman) IncrementSMSOTPFailedAttempts() database.Change {
|
||||
return database.NewIncrementColumnChange(u.failedSMSOTPAttemptsColumn(), database.Coalesce(u.failedSMSOTPAttemptsColumn(), 0))
|
||||
}
|
||||
|
||||
// ResetSMSOTPFailedAttempts implements [domain.HumanUserRepository].
|
||||
func (u userHuman) ResetSMSOTPFailedAttempts() database.Change {
|
||||
return database.NewChange(u.failedSMSOTPAttemptsColumn(), 0)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PhoneCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) PhoneCondition(op database.TextOperation, phone string) database.Condition {
|
||||
return database.Or(u.UnverifiedPhoneCondition(op, phone), u.VerifiedPhoneCondition(op, phone))
|
||||
}
|
||||
|
||||
// UnverifiedPhoneCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) UnverifiedPhoneCondition(op database.TextOperation, phone string) database.Condition {
|
||||
return database.NewTextCondition(u.unverifiedPhoneColumn(), op, phone)
|
||||
}
|
||||
|
||||
// VerifiedPhoneCondition implements [domain.HumanUserRepository].
|
||||
func (u userHuman) VerifiedPhoneCondition(op database.TextOperation, phone string) database.Condition {
|
||||
return database.NewTextCondition(u.phoneColumn(), op, phone)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userHuman) phoneColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "phone")
|
||||
}
|
||||
|
||||
func (u userHuman) unverifiedPhoneColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "unverified_phone")
|
||||
}
|
||||
|
||||
func (u userHuman) phoneVerifiedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "phone_verified_at")
|
||||
}
|
||||
|
||||
func (u userHuman) phoneVerificationIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "phone_verification_id")
|
||||
}
|
||||
|
||||
func (u userHuman) smsOTPEnabledAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "sms_otp_enabled_at")
|
||||
}
|
||||
|
||||
func (u userHuman) lastSuccessfulSMSOTPCheckColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "sms_otp_last_successful_check")
|
||||
}
|
||||
|
||||
func (u userHuman) failedSMSOTPAttemptsColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "sms_otp_failed_attempts")
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"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/crypto"
|
||||
)
|
||||
|
||||
func Test_humanUser_create(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
t.Cleanup(rollback)
|
||||
// tx := pool
|
||||
|
||||
userRepo := repository.UserRepository()
|
||||
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
createdAt := time.Now().Round(time.Millisecond).UTC()
|
||||
|
||||
existingUserID := createMachineUser(t, tx, instanceID, orgID)
|
||||
password := gofakeit.Password(true, true, true, true, false, 16)
|
||||
|
||||
type args struct {
|
||||
user *domain.User
|
||||
}
|
||||
type want struct {
|
||||
err error
|
||||
user *domain.User
|
||||
}
|
||||
type test struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}
|
||||
tests := []test{
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := gofakeit.Email()
|
||||
|
||||
return test{
|
||||
name: "minimal representation",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: domain.HumanEmail{
|
||||
UnverifiedAddress: email,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: domain.HumanEmail{
|
||||
UnverifiedAddress: email,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := gofakeit.Email()
|
||||
|
||||
return test{
|
||||
name: "including optional profile fields",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Nickname: "Nick",
|
||||
DisplayName: firstName + " " + lastName,
|
||||
PreferredLanguage: language.Georgian,
|
||||
Gender: domain.HumanGenderFemale,
|
||||
AvatarKey: "http://localhost:8080/my/profile/picture",
|
||||
MultifactorInitializationSkippedAt: createdAt,
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
},
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Nickname: "Nick",
|
||||
DisplayName: firstName + " " + lastName,
|
||||
PreferredLanguage: language.Georgian,
|
||||
Gender: domain.HumanGenderFemale,
|
||||
AvatarKey: "http://localhost:8080/my/profile/picture",
|
||||
MultifactorInitializationSkippedAt: createdAt,
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
UnverifiedAddress: email,
|
||||
VerifiedAt: createdAt,
|
||||
},
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
ChangedAt: createdAt,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := gofakeit.Email()
|
||||
code := &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "aes256",
|
||||
KeyID: "key-id",
|
||||
Crypted: []byte("crypted"),
|
||||
}
|
||||
|
||||
return test{
|
||||
name: "with unverified email without expiry",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
},
|
||||
Email: domain.HumanEmail{
|
||||
UnverifiedAddress: email,
|
||||
PendingVerification: &domain.Verification{
|
||||
ID: "test",
|
||||
Code: code,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: domain.HumanEmail{
|
||||
UnverifiedAddress: email,
|
||||
PendingVerification: &domain.Verification{
|
||||
ID: "test",
|
||||
Code: code,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
},
|
||||
},
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
ChangedAt: createdAt,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := gofakeit.Email()
|
||||
phone := gofakeit.Phone()
|
||||
code := &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "aes256",
|
||||
KeyID: "key-id",
|
||||
Crypted: []byte("crypted"),
|
||||
}
|
||||
|
||||
return test{
|
||||
name: "with unverified phone without expiry",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
},
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
UnverifiedAddress: email,
|
||||
VerifiedAt: createdAt,
|
||||
},
|
||||
Phone: &domain.HumanPhone{
|
||||
UnverifiedNumber: phone,
|
||||
PendingVerification: &domain.Verification{
|
||||
ID: "id",
|
||||
Code: code,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
UnverifiedAddress: email,
|
||||
VerifiedAt: createdAt,
|
||||
},
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
ChangedAt: createdAt,
|
||||
},
|
||||
Phone: &domain.HumanPhone{
|
||||
UnverifiedNumber: phone,
|
||||
PendingVerification: &domain.Verification{
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
ID: "id",
|
||||
Code: code,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := gofakeit.Email()
|
||||
passkeys := []*domain.Passkey{
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
KeyID: []byte(gofakeit.UUID()),
|
||||
Type: domain.PasskeyTypePasswordless,
|
||||
Name: "passwordless",
|
||||
PublicKey: []byte("public key"),
|
||||
AttestationType: "don't know",
|
||||
AuthenticatorAttestationGUID: []byte("aaguid"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Challenge: []byte("challenge"),
|
||||
VerifiedAt: createdAt,
|
||||
RelyingPartyID: "rpid",
|
||||
},
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
KeyID: []byte(gofakeit.UUID()),
|
||||
Type: domain.PasskeyTypeU2F,
|
||||
Name: "u2f",
|
||||
PublicKey: []byte("public key"),
|
||||
AttestationType: "don't know",
|
||||
AuthenticatorAttestationGUID: []byte("aaguid"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Challenge: []byte("challenge"),
|
||||
VerifiedAt: createdAt,
|
||||
RelyingPartyID: "rpid",
|
||||
},
|
||||
}
|
||||
|
||||
return test{
|
||||
name: "with verified passkeys",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
},
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
},
|
||||
Passkeys: passkeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
UnverifiedAddress: email,
|
||||
VerifiedAt: createdAt,
|
||||
},
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
ChangedAt: createdAt,
|
||||
},
|
||||
Passkeys: passkeys,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := gofakeit.Email()
|
||||
code := &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "aes256",
|
||||
KeyID: "key-id",
|
||||
Crypted: []byte("crypted"),
|
||||
}
|
||||
verifications := []*domain.Verification{
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
Code: code,
|
||||
},
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
Code: code,
|
||||
},
|
||||
}
|
||||
|
||||
return test{
|
||||
name: "with verifications",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
},
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
},
|
||||
Verifications: verifications,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
UnverifiedAddress: email,
|
||||
VerifiedAt: createdAt,
|
||||
},
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
ChangedAt: createdAt,
|
||||
},
|
||||
Verifications: verifications,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
firstName := gofakeit.FirstName()
|
||||
lastName := gofakeit.LastName()
|
||||
email := gofakeit.Email()
|
||||
|
||||
return test{
|
||||
name: "with totp",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
},
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
},
|
||||
TOTP: &domain.HumanTOTP{
|
||||
VerifiedAt: createdAt,
|
||||
Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "aes-256",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Human: &domain.HumanUser{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: domain.HumanEmail{
|
||||
Address: email,
|
||||
UnverifiedAddress: email,
|
||||
VerifiedAt: createdAt,
|
||||
},
|
||||
Password: domain.HumanPassword{
|
||||
Hash: password,
|
||||
IsChangeRequired: true,
|
||||
ChangedAt: createdAt,
|
||||
},
|
||||
TOTP: &domain.HumanTOTP{
|
||||
VerifiedAt: createdAt,
|
||||
Secret: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "aes-256",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
return test{
|
||||
name: "already exists",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: existingUserID,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: new(database.UniqueError),
|
||||
},
|
||||
}
|
||||
}(),
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
savepoint, err := tx.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := savepoint.Rollback(context.Background())
|
||||
if err != nil {
|
||||
t.Log("rollback savepoint failed", err)
|
||||
}
|
||||
})
|
||||
|
||||
err = userRepo.Create(t.Context(), savepoint, tt.args.user)
|
||||
require.ErrorIs(t, err, tt.want.err)
|
||||
if tt.want.err != nil {
|
||||
return
|
||||
}
|
||||
got, err := userRepo.Get(t.Context(), savepoint, database.WithCondition(
|
||||
userRepo.PrimaryKeyCondition(tt.want.user.InstanceID, tt.want.user.ID),
|
||||
))
|
||||
require.NoError(t, err)
|
||||
assertUser(t, tt.want.user, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetTOTPSecret implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetTOTPSecret(secret *crypto.CryptoValue) database.Change {
|
||||
return database.NewChange(u.totpSecretColumn(), secret)
|
||||
}
|
||||
|
||||
// SetTOTPVerifiedAt implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetTOTPVerifiedAt(verifiedAt time.Time) database.Change {
|
||||
if verifiedAt.IsZero() {
|
||||
return database.NewChange(u.totpVerifiedAtColumn(), database.NowInstruction)
|
||||
}
|
||||
return database.NewChange(u.totpVerifiedAtColumn(), verifiedAt)
|
||||
}
|
||||
|
||||
// RemoveTOTP implements [domain.HumanUserRepository].
|
||||
func (u userHuman) RemoveTOTP() database.Change {
|
||||
return database.NewChanges(
|
||||
database.NewChangeToNull(u.totpSecretColumn()),
|
||||
database.NewChangeToNull(u.totpVerifiedAtColumn()),
|
||||
database.NewChangeToNull(u.lastSuccessfulTOTPCheckColumn()),
|
||||
)
|
||||
}
|
||||
|
||||
// SetLastSuccessfulTOTPCheck implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetLastSuccessfulTOTPCheck(checkedAt time.Time) database.Change {
|
||||
if checkedAt.IsZero() {
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulTOTPCheckColumn(), database.NowInstruction),
|
||||
u.ResetTOTPFailedAttempts(),
|
||||
)
|
||||
}
|
||||
return database.NewChanges(
|
||||
database.NewChange(u.lastSuccessfulTOTPCheckColumn(), checkedAt),
|
||||
u.ResetTOTPFailedAttempts(),
|
||||
)
|
||||
}
|
||||
|
||||
// IncrementTOTPFailedAttempts implements [domain.HumanUserRepository].
|
||||
func (u userHuman) IncrementTOTPFailedAttempts() database.Change {
|
||||
return database.NewIncrementColumnChange(u.totpFailedAttemptsColumn(), database.Coalesce(u.totpFailedAttemptsColumn(), 0))
|
||||
}
|
||||
|
||||
// ResetTOTPFailedAttempts implements [domain.HumanUserRepository].
|
||||
func (u userHuman) ResetTOTPFailedAttempts() database.Change {
|
||||
return database.NewChange(u.totpFailedAttemptsColumn(), 0)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userHuman) totpSecretColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "totp_secret")
|
||||
}
|
||||
|
||||
func (u userHuman) totpVerifiedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "totp_verified_at")
|
||||
}
|
||||
|
||||
func (u userHuman) lastSuccessfulTOTPCheckColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "totp_last_successful_check")
|
||||
}
|
||||
|
||||
func (u userHuman) totpFailedAttemptsColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "totp_failed_attempts")
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type userIdentityProviderLink struct{}
|
||||
|
||||
func (u userIdentityProviderLink) unqualifiedTableName() string {
|
||||
return "user_identity_provider_links"
|
||||
}
|
||||
|
||||
func (u userIdentityProviderLink) qualifiedTableName() string {
|
||||
return "zitadel.user_identity_provider_links"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// AddIdentityProviderLink implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) AddIdentityProviderLink(link *domain.IdentityProviderLink) database.Change {
|
||||
var createdAt, updatedAt any = database.NowInstruction, database.NowInstruction
|
||||
if !link.CreatedAt.IsZero() {
|
||||
createdAt = link.CreatedAt
|
||||
}
|
||||
if !link.UpdatedAt.IsZero() {
|
||||
updatedAt = link.UpdatedAt
|
||||
}
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO ")
|
||||
builder.WriteString(u.qualifiedTableName())
|
||||
builder.WriteString("(instance_id, user_id, provided_user_id, provided_username, identity_provider_id, created_at, updated_at) SELECT ")
|
||||
database.Columns{
|
||||
existingHumanUser.InstanceIDColumn(),
|
||||
existingHumanUser.IDColumn(),
|
||||
}.WriteQualified(builder)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteArgs(
|
||||
link.ProvidedUserID,
|
||||
link.ProvidedUsername,
|
||||
link.ProviderID,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingHumanUser.unqualifiedTableName())
|
||||
builder.WriteString(" RETURNING *")
|
||||
}, nil,
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateIdentityProviderLink implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) UpdateIdentityProviderLink(condition database.Condition, changes ...database.Change) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("UPDATE zitadel.user_identity_provider_links SET ")
|
||||
err := database.Changes(changes).Write(builder)
|
||||
logging.New(logging.StreamRuntime).Debug("write changes in cte failed", "error", err)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingHumanUser.unqualifiedTableName())
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(existingHumanUser.InstanceIDColumn(), u.instanceIDColumn()),
|
||||
database.NewColumnCondition(existingHumanUser.IDColumn(), u.userIDColumn()),
|
||||
condition,
|
||||
))
|
||||
}, nil,
|
||||
)
|
||||
}
|
||||
|
||||
// RemoveIdentityProviderLink implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) RemoveIdentityProviderLink(providerID string, providedUserID string) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM ")
|
||||
builder.WriteString(u.qualifiedTableName())
|
||||
builder.WriteString(" USING ")
|
||||
builder.WriteString(existingHumanUser.unqualifiedTableName())
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(
|
||||
existingHumanUser.InstanceIDColumn(),
|
||||
u.instanceIDColumn(),
|
||||
),
|
||||
database.NewColumnCondition(
|
||||
existingHumanUser.IDColumn(),
|
||||
u.userIDColumn(),
|
||||
),
|
||||
database.NewTextCondition(
|
||||
u.providerIDColumn(),
|
||||
database.TextOperationEqual,
|
||||
providerID,
|
||||
),
|
||||
database.NewTextCondition(
|
||||
u.providedUserIDColumn(),
|
||||
database.TextOperationEqual,
|
||||
providedUserID,
|
||||
),
|
||||
))
|
||||
}, nil,
|
||||
)
|
||||
}
|
||||
|
||||
// SetIdentityProviderLinkUsername implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) SetIdentityProviderLinkUsername(username string) database.Change {
|
||||
return database.NewChange(u.providedUsernameColumn(), username)
|
||||
}
|
||||
|
||||
// SetIdentityProviderLinkProvidedID implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) SetIdentityProviderLinkProvidedID(providedUserID string) database.Change {
|
||||
return database.NewChange(u.providedUserIDColumn(), providedUserID)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// IdentityProviderLinkConditions implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) IdentityProviderLinkConditions() domain.HumanIdentityProviderLinkConditions {
|
||||
return u
|
||||
}
|
||||
|
||||
// ProviderIDCondition implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) ProviderIDCondition(idpID string) database.Condition {
|
||||
return database.NewTextCondition(u.providerIDColumn(), database.TextOperationEqual, idpID)
|
||||
}
|
||||
|
||||
// ProvidedUserIDCondition implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) ProvidedUserIDCondition(providedUserID string) database.Condition {
|
||||
return database.NewTextCondition(u.providedUserIDColumn(), database.TextOperationEqual, providedUserID)
|
||||
}
|
||||
|
||||
// ProvidedUsernameCondition implements [domain.HumanUserRepository].
|
||||
func (u userIdentityProviderLink) ProvidedUsernameCondition(op database.TextOperation, username string) database.Condition {
|
||||
return database.NewTextCondition(u.providedUsernameColumn(), op, username)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userIdentityProviderLink) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
func (u userIdentityProviderLink) userIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "user_id")
|
||||
}
|
||||
|
||||
func (u userIdentityProviderLink) providerIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "identity_provider_id")
|
||||
}
|
||||
|
||||
func (u userIdentityProviderLink) providedUserIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "provided_user_id")
|
||||
}
|
||||
|
||||
func (u userIdentityProviderLink) providedUsernameColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "provided_username")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetInviteVerification implements [domain.HumanUserRepository].
|
||||
func (u userHuman) SetInviteVerification(verification domain.VerificationType) database.Change {
|
||||
switch typ := verification.(type) {
|
||||
case *domain.VerificationTypeInit:
|
||||
return u.verification.init(typ, existingUser.unqualifiedTableName(), existingHumanUser.inviteVerificationIDColumn())
|
||||
case *domain.VerificationTypeUpdate:
|
||||
return u.verification.update(typ, existingHumanUser.unqualifiedTableName(),
|
||||
existingHumanUser.InstanceIDColumn(), existingHumanUser.inviteVerificationIDColumn(),
|
||||
)
|
||||
case *domain.VerificationTypeSucceeded:
|
||||
return u.verification.verified(typ, existingHumanUser.unqualifiedTableName(), existingHumanUser.InstanceIDColumn(),
|
||||
existingHumanUser.inviteVerificationIDColumn(), u.inviteAcceptedAtColumn(), u.failedInviteAttemptsColumn(),
|
||||
)
|
||||
case *domain.VerificationTypeSkipped:
|
||||
return u.verification.skipped(typ, u.inviteAcceptedAtColumn(), u.inviteVerificationIDColumn(), u.failedInviteAttemptsColumn())
|
||||
case *domain.VerificationTypeFailed:
|
||||
return u.verification.failed(existingHumanUser.unqualifiedTableName(), existingHumanUser.InstanceIDColumn(), existingHumanUser.inviteVerificationIDColumn())
|
||||
}
|
||||
panic(fmt.Sprintf("undefined verification type %T", verification))
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userHuman) inviteVerificationIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "invite_verification_id")
|
||||
}
|
||||
|
||||
func (u userHuman) inviteAcceptedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "invite_accepted_at")
|
||||
}
|
||||
|
||||
func (u userHuman) failedInviteAttemptsColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "invite_failed_attempts")
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type userMachine struct {
|
||||
user
|
||||
machineKey
|
||||
userPersonalAccessToken
|
||||
}
|
||||
|
||||
func (u userMachine) create(ctx context.Context, builder *database.StatementBuilder, client database.QueryExecutor, user *domain.User) error {
|
||||
changes := make(database.Changes, 0, len(user.Machine.Keys)+len(user.Machine.PATs))
|
||||
for _, key := range user.Machine.Keys {
|
||||
changes = append(changes, u.AddKey(key))
|
||||
}
|
||||
for _, pat := range user.Machine.PATs {
|
||||
changes = append(changes, u.AddPersonalAccessToken(pat))
|
||||
}
|
||||
for _, metadata := range user.Metadata {
|
||||
changes = append(changes, u.SetMetadata(metadata))
|
||||
}
|
||||
for i, change := range changes {
|
||||
sessionCTE(change, i, 0, builder)
|
||||
}
|
||||
|
||||
builder.WriteString("INSERT INTO zitadel.users (" +
|
||||
"instance_id, organization_id, id, username, state, type" +
|
||||
", name, description, secret, access_token_type, created_at, updated_at) VALUES (",
|
||||
)
|
||||
|
||||
var createdAt any = database.NowInstruction
|
||||
if !user.CreatedAt.IsZero() {
|
||||
createdAt = user.CreatedAt
|
||||
}
|
||||
builder.WriteArgs(
|
||||
user.InstanceID, user.OrganizationID, user.ID, user.Username, user.State, "machine",
|
||||
user.Machine.Name, user.Machine.Description, user.Machine.Secret, uint8(user.Machine.AccessTokenType),
|
||||
createdAt, createdAt,
|
||||
)
|
||||
|
||||
builder.WriteString(") RETURNING created_at, updated_at")
|
||||
|
||||
return client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt)
|
||||
}
|
||||
|
||||
// Update implements [domain.MachineUserRepository].
|
||||
func (u userMachine) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
|
||||
if !condition.IsRestrictingColumn(u.TypeColumn()) {
|
||||
condition = database.And(condition, u.TypeCondition(domain.UserTypeMachine))
|
||||
}
|
||||
return u.user.Update(ctx, client, condition, changes...)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetAccessTokenType implements [domain.MachineUserRepository].
|
||||
func (u userMachine) SetAccessTokenType(tokenType domain.AccessTokenType) database.Change {
|
||||
return database.NewChange(u.accessTokenTypeColumn(), tokenType)
|
||||
}
|
||||
|
||||
// SetDescription implements [domain.MachineUserRepository].
|
||||
func (u userMachine) SetDescription(description string) database.Change {
|
||||
return database.NewChange(u.descriptionColumn(), description)
|
||||
}
|
||||
|
||||
// SetName implements [domain.MachineUserRepository].
|
||||
func (u userMachine) SetName(name string) database.Change {
|
||||
return database.NewChange(u.nameColumn(), name)
|
||||
}
|
||||
|
||||
// SetSecret implements [domain.MachineUserRepository].
|
||||
func (u userMachine) SetSecret(secret *string) database.Change {
|
||||
return database.NewChangePtr(u.secretColumn(), secret)
|
||||
}
|
||||
|
||||
var _ domain.MachineUserRepository = (*userMachine)(nil)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userMachine) nameColumn() database.Column {
|
||||
return database.NewColumn(u.user.unqualifiedTableName(), "name")
|
||||
}
|
||||
|
||||
func (u userMachine) descriptionColumn() database.Column {
|
||||
return database.NewColumn(u.user.unqualifiedTableName(), "description")
|
||||
}
|
||||
|
||||
func (u userMachine) accessTokenTypeColumn() database.Column {
|
||||
return database.NewColumn(u.user.unqualifiedTableName(), "access_token_type")
|
||||
}
|
||||
|
||||
func (u userMachine) secretColumn() database.Column {
|
||||
return database.NewColumn(u.user.unqualifiedTableName(), "secret")
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type machineKey struct{}
|
||||
|
||||
// AddKey implements [domain.MachineUserRepository].
|
||||
func (u machineKey) AddKey(key *domain.MachineKey) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.machine_keys (" +
|
||||
"instance_id, user_id, id, created_at, expires_at, type, public_key" +
|
||||
") SELECT instance_id, id, ",
|
||||
)
|
||||
var createdAt any = database.NowInstruction
|
||||
if !key.CreatedAt.IsZero() {
|
||||
createdAt = key.CreatedAt
|
||||
}
|
||||
var expiresAt any = database.NullInstruction
|
||||
if !key.ExpiresAt.IsZero() {
|
||||
expiresAt = key.ExpiresAt
|
||||
}
|
||||
builder.WriteArgs(key.ID, createdAt, expiresAt, key.Type, key.PublicKey)
|
||||
builder.WriteString(" FROM existing_user")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// RemoveKey implements [domain.MachineUserRepository].
|
||||
func (u machineKey) RemoveKey(id string) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM zitadel.machine_keys WHERE " +
|
||||
"(instance_id, user_id, id) = (SELECT instance_id, id, ")
|
||||
builder.WriteArg(id)
|
||||
builder.WriteString(" FROM existing_user)")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (u machineKey) qualifiedTableName() string {
|
||||
return "zitadel.machine_keys"
|
||||
}
|
||||
|
||||
func (u machineKey) unqualifiedTableName() string {
|
||||
return "machine_keys"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u machineKey) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (u machineKey) userIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "user_id")
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package repository_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/repository"
|
||||
)
|
||||
|
||||
func Test_machineUser_create(t *testing.T) {
|
||||
tx, rollback := transactionForRollback(t)
|
||||
t.Cleanup(rollback)
|
||||
|
||||
userRepo := repository.UserRepository()
|
||||
|
||||
instanceID := createInstance(t, tx)
|
||||
orgID := createOrganization(t, tx, instanceID)
|
||||
createdAt := time.Now().Round(time.Second)
|
||||
|
||||
existingUserID := createMachineUser(t, tx, instanceID, orgID)
|
||||
|
||||
type args struct {
|
||||
user *domain.User
|
||||
}
|
||||
type want struct {
|
||||
err error
|
||||
user *domain.User
|
||||
}
|
||||
type test struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}
|
||||
tests := []test{
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
return test{
|
||||
name: "minimal representation",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
return test{
|
||||
name: "with secret",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
Secret: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
Secret: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
return test{
|
||||
name: "with access token type",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
AccessTokenType: domain.AccessTokenTypeBearer,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
AccessTokenType: domain.AccessTokenTypeBearer,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
keys := []*domain.MachineKey{
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
PublicKey: []byte("public key"),
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(24 * time.Hour),
|
||||
Type: domain.MachineKeyTypeJSON,
|
||||
},
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
PublicKey: []byte("public key"),
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(24 * time.Hour),
|
||||
Type: domain.MachineKeyTypeJSON,
|
||||
},
|
||||
}
|
||||
return test{
|
||||
name: "with keys",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
Keys: keys,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
Keys: keys,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
pats := []*domain.PersonalAccessToken{
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(24 * time.Hour),
|
||||
},
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(24 * time.Hour),
|
||||
Scopes: []string{"some", "scopes"},
|
||||
},
|
||||
}
|
||||
return test{
|
||||
name: "with keys",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
PATs: pats,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
PATs: pats,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
id := gofakeit.UUID()
|
||||
pats := []*domain.PersonalAccessToken{
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(24 * time.Hour),
|
||||
},
|
||||
}
|
||||
keys := []*domain.MachineKey{
|
||||
{
|
||||
ID: gofakeit.UUID(),
|
||||
PublicKey: []byte("public key"),
|
||||
CreatedAt: createdAt,
|
||||
ExpiresAt: createdAt.Add(24 * time.Hour),
|
||||
Type: domain.MachineKeyTypeJSON,
|
||||
},
|
||||
}
|
||||
return test{
|
||||
name: "all fields set",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
Key: "meta1",
|
||||
Value: []byte("value1"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
},
|
||||
{
|
||||
Key: "meta2",
|
||||
Value: []byte("123"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
},
|
||||
},
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
PATs: pats,
|
||||
Keys: keys,
|
||||
Secret: "secret",
|
||||
AccessTokenType: domain.AccessTokenTypeJWT,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: id,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
Metadata: []*domain.Metadata{
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
Key: "meta1",
|
||||
Value: []byte("value1"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
},
|
||||
{
|
||||
InstanceID: instanceID,
|
||||
Key: "meta2",
|
||||
Value: []byte("123"),
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: createdAt,
|
||||
},
|
||||
},
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
Description: "description",
|
||||
PATs: pats,
|
||||
Keys: keys,
|
||||
Secret: "secret",
|
||||
AccessTokenType: domain.AccessTokenTypeJWT,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
func() test {
|
||||
username := gofakeit.Username()
|
||||
return test{
|
||||
name: "already exists",
|
||||
args: args{
|
||||
user: &domain.User{
|
||||
InstanceID: instanceID,
|
||||
OrganizationID: orgID,
|
||||
ID: existingUserID,
|
||||
Username: username,
|
||||
State: domain.UserStateActive,
|
||||
CreatedAt: createdAt,
|
||||
Machine: &domain.MachineUser{
|
||||
Name: "machine",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: new(database.UniqueError),
|
||||
},
|
||||
}
|
||||
}(),
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
savepoint, err := tx.Begin(t.Context())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := savepoint.Rollback(context.Background())
|
||||
if err != nil {
|
||||
t.Log("rollback savepoint failed", err)
|
||||
}
|
||||
})
|
||||
|
||||
err = userRepo.Create(t.Context(), savepoint, tt.args.user)
|
||||
require.ErrorIs(t, err, tt.want.err)
|
||||
if tt.want.err != nil {
|
||||
return
|
||||
}
|
||||
got, err := userRepo.Get(t.Context(), savepoint, database.WithCondition(
|
||||
userRepo.PrimaryKeyCondition(tt.want.user.InstanceID, tt.want.user.ID),
|
||||
))
|
||||
require.NoError(t, err)
|
||||
assertUser(t, tt.want.user, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type userMetadata struct{}
|
||||
|
||||
func (userMetadata) qualifiedTableName() string {
|
||||
return "zitadel.user_metadata"
|
||||
}
|
||||
|
||||
func (userMetadata) unqualifiedTableName() string {
|
||||
return "user_metadata"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// SetMetadata implements [domain.UserRepository].
|
||||
func (u userMetadata) SetMetadata(metadata ...*domain.Metadata) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.user_metadata(instance_id, user_id, key, value, created_at, updated_at)")
|
||||
builder.WriteString(" SELECT existing_user.instance_id, existing_user.id, md.key::TEXT, md.value::BYTEA, md.created_at::TIMESTAMPTZ, md.updated_at::TIMESTAMPTZ")
|
||||
builder.WriteString(" FROM existing_user CROSS JOIN (VALUES ")
|
||||
for i, md := range metadata {
|
||||
if i > 0 {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
builder.WriteRune('(')
|
||||
// The complex placeholder handling is needed to append the type casts
|
||||
// if we use WriteArgs directly, pgx resolves all args as TEXT instead of the correct type
|
||||
placeholders := make([]string, 4)
|
||||
placeholders[0] = builder.AppendArg(md.Key)
|
||||
placeholders[1] = builder.AppendArg(md.Value) + "::BYTEA"
|
||||
var createdAt, updatedAt any = database.NowInstruction, database.NullInstruction
|
||||
if !md.CreatedAt.IsZero() {
|
||||
createdAt = md.CreatedAt
|
||||
}
|
||||
if !md.UpdatedAt.IsZero() {
|
||||
updatedAt = md.UpdatedAt
|
||||
}
|
||||
placeholders[2] = builder.AppendArg(createdAt) + "::TIMESTAMPTZ"
|
||||
placeholders[3] = builder.AppendArg(updatedAt) + "::TIMESTAMPTZ"
|
||||
builder.WriteString(strings.Join(placeholders, ", "))
|
||||
builder.WriteRune(')')
|
||||
}
|
||||
builder.WriteString(") AS md(key, value, created_at, updated_at) ON CONFLICT (")
|
||||
database.Columns{
|
||||
u.instanceIDColumn(),
|
||||
u.userIDColumn(),
|
||||
u.keyColumn(),
|
||||
}.WriteUnqualified(builder)
|
||||
builder.WriteString(") DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at WHERE ")
|
||||
u.valueColumn().WriteQualified(builder)
|
||||
builder.WriteString(" IS DISTINCT FROM EXCLUDED.value")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// RemoveMetadata implements [domain.UserRepository].
|
||||
func (u userMetadata) RemoveMetadata(condition database.Condition) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM zitadel.user_metadata USING ")
|
||||
builder.WriteString(existingUser.unqualifiedTableName())
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(existingUser.InstanceIDColumn(), u.instanceIDColumn()),
|
||||
database.NewColumnCondition(existingUser.IDColumn(), u.userIDColumn()),
|
||||
condition,
|
||||
))
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userMetadata) MetadataConditions() domain.UserMetadataConditions {
|
||||
return u
|
||||
}
|
||||
|
||||
// MetadataKeyCondition implements [domain.UserMetadataConditions].
|
||||
func (u userMetadata) MetadataKeyCondition(op database.TextOperation, key string) database.Condition {
|
||||
return database.NewTextCondition(u.keyColumn(), op, key)
|
||||
}
|
||||
|
||||
// MetadataValueCondition implements [domain.UserMetadataConditions].
|
||||
func (u userMetadata) MetadataValueCondition(op database.BytesOperation, value []byte) database.Condition {
|
||||
return database.NewBytesCondition[[]byte](database.SHA256Column(u.valueColumn()), op, database.SHA256Value(value))
|
||||
}
|
||||
|
||||
var _ domain.UserMetadataConditions = (*userMetadata)(nil)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userMetadata) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (u userMetadata) userIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "user_id")
|
||||
}
|
||||
|
||||
func (u userMetadata) keyColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "key")
|
||||
}
|
||||
|
||||
func (u userMetadata) valueColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "value")
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type userPasskey struct{}
|
||||
|
||||
func (u userPasskey) unqualifiedTableName() string {
|
||||
return "user_passkeys"
|
||||
}
|
||||
|
||||
func (u userPasskey) qualifiedTableName() string {
|
||||
return "zitadel.user_passkeys"
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// changes
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// AddPasskey implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) AddPasskey(passkey *domain.Passkey) database.Change {
|
||||
var (
|
||||
createdAt any = database.NowInstruction
|
||||
updatedAt any = database.NowInstruction
|
||||
verifiedAt any = database.NowInstruction
|
||||
)
|
||||
if !passkey.CreatedAt.IsZero() {
|
||||
createdAt = passkey.CreatedAt
|
||||
}
|
||||
if !passkey.UpdatedAt.IsZero() {
|
||||
updatedAt = passkey.UpdatedAt
|
||||
}
|
||||
if !passkey.VerifiedAt.IsZero() {
|
||||
verifiedAt = passkey.VerifiedAt
|
||||
}
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO ")
|
||||
builder.WriteString(u.qualifiedTableName())
|
||||
builder.WriteString(" (")
|
||||
database.Columns{
|
||||
u.instanceIDColumn(),
|
||||
u.userIDColumn(),
|
||||
u.tokenIDColumn(),
|
||||
u.keyIDColumn(),
|
||||
u.createdAtColumn(),
|
||||
u.updatedAtColumn(),
|
||||
u.verifiedAtColumn(),
|
||||
u.typeColumn(),
|
||||
u.nameColumn(),
|
||||
u.signCountColumn(),
|
||||
u.challengeColumn(),
|
||||
u.publicKeyColumn(),
|
||||
u.attestationTypeColumn(),
|
||||
u.authenticatorAttestationGUIDColumn(),
|
||||
u.relyingPartyIDColumn(),
|
||||
}.WriteUnqualified(builder)
|
||||
builder.WriteString(") SELECT ")
|
||||
database.Columns{
|
||||
existingHumanUser.InstanceIDColumn(),
|
||||
existingHumanUser.IDColumn(),
|
||||
}.WriteQualified(builder)
|
||||
builder.WriteString(", ")
|
||||
builder.WriteArgs(
|
||||
passkey.ID,
|
||||
passkey.KeyID,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
verifiedAt,
|
||||
passkey.Type,
|
||||
passkey.Name,
|
||||
passkey.SignCount,
|
||||
passkey.Challenge,
|
||||
passkey.PublicKey,
|
||||
passkey.AttestationType,
|
||||
passkey.AuthenticatorAttestationGUID,
|
||||
passkey.RelyingPartyID,
|
||||
)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingHumanUser.unqualifiedTableName())
|
||||
}, nil,
|
||||
)
|
||||
}
|
||||
|
||||
// RemovePasskey implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) RemovePasskey(condition database.Condition) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM ")
|
||||
builder.WriteString(u.qualifiedTableName())
|
||||
builder.WriteString(" USING ")
|
||||
builder.WriteString(existingHumanUser.unqualifiedTableName())
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(existingHumanUser.InstanceIDColumn(), u.instanceIDColumn()),
|
||||
database.NewColumnCondition(existingHumanUser.IDColumn(), u.userIDColumn()),
|
||||
condition,
|
||||
))
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// UpdatePasskey implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) UpdatePasskey(condition database.Condition, changes ...database.Change) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("UPDATE ")
|
||||
builder.WriteString(u.qualifiedTableName())
|
||||
builder.WriteString(" SET ")
|
||||
err := database.Changes(changes).Write(builder)
|
||||
logging.New(logging.StreamRuntime).Debug("write changes in cte failed", "error", err)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingHumanUser.unqualifiedTableName())
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(existingHumanUser.InstanceIDColumn(), u.instanceIDColumn()),
|
||||
database.NewColumnCondition(existingHumanUser.IDColumn(), u.userIDColumn()),
|
||||
condition,
|
||||
))
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// SetPasskeyAttestationType implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeyAttestationType(attestationType string) database.Change {
|
||||
return database.NewChange(u.attestationTypeColumn(), attestationType)
|
||||
}
|
||||
|
||||
// SetPasskeyAuthenticatorAttestationGUID implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeyAuthenticatorAttestationGUID(aaguid []byte) database.Change {
|
||||
return database.NewChange(u.authenticatorAttestationGUIDColumn(), aaguid)
|
||||
}
|
||||
|
||||
// SetPasskeyKeyID implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeyKeyID(keyID []byte) database.Change {
|
||||
return database.NewChange(u.keyIDColumn(), keyID)
|
||||
}
|
||||
|
||||
// SetPasskeyName implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeyName(name string) database.Change {
|
||||
return database.NewChange(u.nameColumn(), name)
|
||||
}
|
||||
|
||||
// SetPasskeyPublicKey implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeyPublicKey(publicKey []byte) database.Change {
|
||||
return database.NewChange(u.publicKeyColumn(), publicKey)
|
||||
}
|
||||
|
||||
// SetPasskeySignCount implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeySignCount(signCount uint32) database.Change {
|
||||
return database.NewChange(u.signCountColumn(), signCount)
|
||||
}
|
||||
|
||||
// SetPasskeyUpdatedAt implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeyUpdatedAt(updatedAt time.Time) database.Change {
|
||||
return database.NewChange(u.updatedAtColumn(), updatedAt)
|
||||
}
|
||||
|
||||
// SetPasskeyVerifiedAt implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) SetPasskeyVerifiedAt(verifiedAt time.Time) database.Change {
|
||||
verifiedAtChange := database.NewChange(u.verifiedAtColumn(), database.NowInstruction)
|
||||
if !verifiedAt.IsZero() {
|
||||
verifiedAtChange = database.NewChange(u.verifiedAtColumn(), verifiedAt)
|
||||
}
|
||||
return verifiedAtChange
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// conditions
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// PasskeyConditions implements [domain.HumanUserRepository].
|
||||
func (u userPasskey) PasskeyConditions() domain.HumanPasskeyConditions {
|
||||
return u
|
||||
}
|
||||
|
||||
// ChallengeCondition implements [domain.HumanPasskeyConditions].
|
||||
func (u userPasskey) ChallengeCondition(challenge []byte) database.Condition {
|
||||
return database.NewBytesCondition[[]byte](database.SHA256Column(u.challengeColumn()), database.BytesOperationEqual, database.SHA256Value(challenge))
|
||||
}
|
||||
|
||||
// IDCondition implements [domain.HumanPasskeyConditions].
|
||||
func (u userPasskey) IDCondition(passkeyID string) database.Condition {
|
||||
return database.NewTextCondition(u.tokenIDColumn(), database.TextOperationEqual, passkeyID)
|
||||
}
|
||||
|
||||
// KeyIDCondition implements [domain.HumanPasskeyConditions].
|
||||
func (u userPasskey) KeyIDCondition(keyID string) database.Condition {
|
||||
return database.NewTextCondition(u.keyIDColumn(), database.TextOperationEqual, keyID)
|
||||
}
|
||||
|
||||
// TypeCondition implements [domain.HumanPasskeyConditions].
|
||||
func (u userPasskey) TypeCondition(passkeyType domain.PasskeyType) database.Condition {
|
||||
return database.NewTextCondition(u.typeColumn(), database.TextOperationEqual, passkeyType.String())
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (u userPasskey) tokenIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "token_id")
|
||||
}
|
||||
|
||||
func (u userPasskey) keyIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "key_id")
|
||||
}
|
||||
|
||||
func (u userPasskey) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (u userPasskey) userIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "user_id")
|
||||
}
|
||||
|
||||
func (u userPasskey) relyingPartyIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "relying_party_id")
|
||||
}
|
||||
|
||||
func (u userPasskey) authenticatorAttestationGUIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "authenticator_attestation_guid")
|
||||
}
|
||||
|
||||
func (u userPasskey) attestationTypeColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "attestation_type")
|
||||
}
|
||||
|
||||
func (u userPasskey) publicKeyColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "public_key")
|
||||
}
|
||||
|
||||
func (u userPasskey) challengeColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "challenge")
|
||||
}
|
||||
|
||||
func (u userPasskey) signCountColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "sign_count")
|
||||
}
|
||||
|
||||
func (u userPasskey) nameColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "name")
|
||||
}
|
||||
|
||||
func (u userPasskey) typeColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "type")
|
||||
}
|
||||
|
||||
func (u userPasskey) verifiedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "verified_at")
|
||||
}
|
||||
|
||||
func (u userPasskey) updatedAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "updated_at")
|
||||
}
|
||||
|
||||
func (u userPasskey) createdAtColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "created_at")
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type userPersonalAccessToken struct{}
|
||||
|
||||
// AddPersonalAccessToken implements [domain.MachineUserRepository].
|
||||
func (userPersonalAccessToken) AddPersonalAccessToken(pat *domain.PersonalAccessToken) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("INSERT INTO zitadel.user_personal_access_tokens (" +
|
||||
"instance_id, user_id, id, created_at, expires_at, scopes" +
|
||||
") SELECT instance_id, id, ",
|
||||
)
|
||||
var createdAt any = database.NowInstruction
|
||||
if !pat.CreatedAt.IsZero() {
|
||||
createdAt = pat.CreatedAt
|
||||
}
|
||||
var expiresAt any = database.NullInstruction
|
||||
if !pat.ExpiresAt.IsZero() {
|
||||
expiresAt = pat.ExpiresAt
|
||||
}
|
||||
builder.WriteArgs(pat.ID, createdAt, expiresAt, pat.Scopes)
|
||||
builder.WriteString(" FROM existing_user")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// RemovePersonalAccessToken implements [domain.MachineUserRepository].
|
||||
func (userPersonalAccessToken) RemovePersonalAccessToken(id string) database.Change {
|
||||
return database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM zitadel.user_personal_access_tokens WHERE " +
|
||||
"(instance_id, user_id, id) = (SELECT instance_id, id, ")
|
||||
builder.WriteArg(id)
|
||||
builder.WriteString(" FROM existing_user)")
|
||||
},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (userPersonalAccessToken) qualifiedTableName() string {
|
||||
return "zitadel.user_personal_access_tokens"
|
||||
}
|
||||
|
||||
func (userPersonalAccessToken) unqualifiedTableName() string {
|
||||
return "user_personal_access_tokens"
|
||||
}
|
||||
|
||||
func (u userPersonalAccessToken) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (u userPersonalAccessToken) userIDColumn() database.Column {
|
||||
return database.NewColumn(u.unqualifiedTableName(), "user_id")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,171 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type verification struct{}
|
||||
|
||||
func (v verification) unqualifiedTableName() string {
|
||||
return "verifications"
|
||||
}
|
||||
|
||||
func (v verification) qualifiedTableName() string {
|
||||
return "zitadel." + v.unqualifiedTableName()
|
||||
}
|
||||
|
||||
// init creates a new verification
|
||||
func (v verification) init(init *domain.VerificationTypeInit, existingTableName string, verificationID database.Column) database.CTEChange {
|
||||
return database.NewCTEChange(func(builder *database.StatementBuilder) {
|
||||
var (
|
||||
createdAt any = database.NowInstruction
|
||||
expiry any = database.NullInstruction
|
||||
id any = database.GenRandomUUIDInstruction
|
||||
)
|
||||
if !init.CreatedAt.IsZero() {
|
||||
createdAt = init.CreatedAt
|
||||
}
|
||||
if init.Expiry != nil {
|
||||
expiry = *init.Expiry
|
||||
}
|
||||
if init.ID != nil {
|
||||
id = *init.ID
|
||||
}
|
||||
builder.WriteString("INSERT INTO zitadel.verifications(instance_id, user_id, id, code, created_at, updated_at, expiry) SELECT instance_id, id, ")
|
||||
builder.WriteArgs(id, init.Code, createdAt, createdAt, expiry)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingTableName)
|
||||
builder.WriteString(" RETURNING id")
|
||||
}, func(name string) database.Change {
|
||||
return database.NewChangeToStatement(verificationID, func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("SELECT id FROM ")
|
||||
builder.WriteString(name)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// updates the given fields of the verification with the given ID. Only non-nil fields get updated.
|
||||
func (v verification) update(update *domain.VerificationTypeUpdate, existingTableName string, instanceID, verificationID database.Column) database.CTEChange {
|
||||
changes := make(database.Changes, 0, 3)
|
||||
if update.Code != nil {
|
||||
changes = append(changes, database.NewChange(v.codeColumn(), update.Code))
|
||||
}
|
||||
if update.Expiry != nil {
|
||||
changes = append(changes, database.NewChange(v.expiryColumn(), *update.Expiry))
|
||||
}
|
||||
if update.UpdatedAt.IsZero() {
|
||||
changes = append(changes, database.NewChange(v.updatedAtColumn(), database.NowInstruction))
|
||||
} else {
|
||||
changes = append(changes, database.NewChange(v.updatedAtColumn(), update.UpdatedAt))
|
||||
}
|
||||
return database.NewCTEChange(func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("UPDATE ")
|
||||
builder.WriteString(v.qualifiedTableName())
|
||||
builder.WriteString(" SET ")
|
||||
err := changes.Write(builder)
|
||||
logging.New(logging.StreamRuntime).Debug("write changes in cte failed", "error", err)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingTableName)
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(v.instanceIDColumn(), instanceID),
|
||||
database.NewColumnCondition(v.idColumn(), verificationID),
|
||||
))
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// verified constructs the following changes:
|
||||
// - sets the verifiedAt column to the current time or the provided time
|
||||
// - clears the pending verification ID
|
||||
// - deletes the verification
|
||||
// - resets the failed attempts counter
|
||||
func (v verification) verified(verified *domain.VerificationTypeSucceeded, existingTableName string, instanceID, pendingVerificationID, verifiedAt, failedAttempts database.Column) database.Changes {
|
||||
verifiedAtChange := database.NewChange(verifiedAt, database.NowInstruction)
|
||||
if !verified.VerifiedAt.IsZero() {
|
||||
verifiedAtChange = database.NewChange(verifiedAt, verified.VerifiedAt)
|
||||
}
|
||||
|
||||
return database.NewChanges(
|
||||
verifiedAtChange,
|
||||
database.NewChangeToNull(pendingVerificationID),
|
||||
database.NewChange(failedAttempts, 0),
|
||||
database.NewCTEChange(
|
||||
func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("DELETE FROM ")
|
||||
builder.WriteString(v.qualifiedTableName())
|
||||
builder.WriteString(" USING ")
|
||||
builder.WriteString(existingTableName)
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(v.instanceIDColumn(), instanceID),
|
||||
database.NewColumnCondition(v.idColumn(), pendingVerificationID),
|
||||
))
|
||||
}, nil,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// skipped constructs the following changes:
|
||||
// - sets the verifiedAt column to the current time or the provided time
|
||||
// - clears the pending verification ID
|
||||
// - resets the failed attempts counter
|
||||
func (v verification) skipped(skipped *domain.VerificationTypeSkipped, verifiedAt, pendingVerificationID, failedAttempts database.Column) database.Changes {
|
||||
verifiedAtChange := database.NewChange(verifiedAt, database.NowInstruction)
|
||||
if !skipped.SkippedAt.IsZero() {
|
||||
verifiedAtChange = database.NewChange(verifiedAt, skipped.SkippedAt)
|
||||
}
|
||||
return database.NewChanges(
|
||||
verifiedAtChange,
|
||||
database.NewChange(failedAttempts, 0),
|
||||
database.NewChangeToNull(pendingVerificationID),
|
||||
)
|
||||
}
|
||||
|
||||
// failed increments the failed attempts counter.
|
||||
func (v verification) failed(existingTableName string, instanceID, verificationID database.Column) database.CTEChange {
|
||||
return database.NewCTEChange(func(builder *database.StatementBuilder) {
|
||||
builder.WriteString("UPDATE ")
|
||||
builder.WriteString(v.qualifiedTableName())
|
||||
builder.WriteString(" SET ")
|
||||
err := database.NewIncrementColumnChange(v.failedAttemptsColumn(), database.Coalesce(v.failedAttemptsColumn(), 0)).Write(builder)
|
||||
logging.New(logging.StreamRuntime).Debug("write cte failed", "error", err)
|
||||
builder.WriteString(" FROM ")
|
||||
builder.WriteString(existingTableName)
|
||||
writeCondition(builder, database.And(
|
||||
database.NewColumnCondition(v.instanceIDColumn(), instanceID),
|
||||
database.NewColumnCondition(v.idColumn(), verificationID),
|
||||
))
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// columns
|
||||
// -------------------------------------------------------------
|
||||
|
||||
func (v verification) instanceIDColumn() database.Column {
|
||||
return database.NewColumn(v.unqualifiedTableName(), "instance_id")
|
||||
}
|
||||
|
||||
func (v verification) idColumn() database.Column {
|
||||
return database.NewColumn(v.unqualifiedTableName(), "id")
|
||||
}
|
||||
|
||||
func (v verification) codeColumn() database.Column {
|
||||
return database.NewColumn(v.unqualifiedTableName(), "code")
|
||||
}
|
||||
|
||||
func (v verification) expiryColumn() database.Column {
|
||||
return database.NewColumn(v.unqualifiedTableName(), "expiry")
|
||||
}
|
||||
|
||||
func (v verification) failedAttemptsColumn() database.Column {
|
||||
return database.NewColumn(v.unqualifiedTableName(), "failed_attempts")
|
||||
}
|
||||
|
||||
func (v verification) userIDColumn() database.Column {
|
||||
return database.NewColumn(v.unqualifiedTableName(), "user_id")
|
||||
}
|
||||
|
||||
func (v verification) updatedAtColumn() database.Column {
|
||||
return database.NewColumn(v.unqualifiedTableName(), "updated_at")
|
||||
}
|
||||
Reference in New Issue
Block a user