mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
feat: org command sides (#96)
* start org * refactor(eventstore): filter in sql for querier * feat(eventstore): Aggregate precondition preconditions are checked right before insert. Insert is still transaction save * feat(eventstore): check preconditions in repository * test(eventstore): test precondition in models * test(eventstore): precondition-tests * start org * refactor(eventstore): filter in sql for querier * feat(eventstore): Aggregate precondition preconditions are checked right before insert. Insert is still transaction save * feat(admin): start implement org * feat(eventstore): check preconditions in repository * fix(eventstore): data as NULL if empty refactor(eventstore): naming in sequence methods * feat(admin): org command side * feat(management): start org-repo * feat(org): member * fix: replace ObjectRoot.ID with ObjectRoot.AggregateID * aggregateID * add remove,change member * refactor(org): namings * refactor(eventstore): querier as type * fix(precondition): rename validation from precondition to validation * test(eventstore): isErr func instead of wantErr bool * fix(tests): Data * fix(eventstore): correct check for existing events in push, simplify insert statement * fix(eventstore): aggregate id public * test(org): eventsourcing * test(org): eventstore * test(org): deactivate, reactivate, orgbyid * test(org): getMemberByIDs * tests * running tests * add user repo to admin * thorw not found if no org found * eventstore tests done * lauft * validate if user is already member of org * modules * delete unused file * add member validation test * return error if unable to validat member * generate org id once, set resourceowner of org * Update internal/admin/repository/eventsourcing/eventstore/org.go * Update internal/admin/repository/eventsourcing/eventstore/org.go * Update internal/org/repository/eventsourcing/member_model.go * Update internal/org/repository/eventsourcing/org.go * Update internal/org/repository/eventsourcing/org.go * Update internal/org/repository/eventsourcing/org_member.go * Update internal/org/repository/eventsourcing/org_member.go * Update internal/org/repository/eventsourcing/org_model.go * Update internal/org/repository/eventsourcing/org.go * Update internal/org/repository/eventsourcing/org_model.go * Update internal/org/repository/eventsourcing/org_model.go * typo * correct user events * usercreate for setuporg instead of userregister * set data * mod * mod * tests * cleanup code * code styling * return member on add and change * change username in startup * girignore * orgID as parameter in re-/deactive org * startup config * migration for admin_api-user * probes fro admin * move unique org Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com>
This commit is contained in:
25
internal/admin/auth/token_verifier.go
Normal file
25
internal/admin/auth/token_verifier.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
)
|
||||
|
||||
type TokenVerifier struct {
|
||||
}
|
||||
|
||||
func Start() (v *TokenVerifier) {
|
||||
return new(TokenVerifier)
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) {
|
||||
return "", "", "", nil
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) ResolveGrants(ctx context.Context, userID, orgID string) ([]*auth.Grant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
|
||||
return "", nil
|
||||
}
|
11
internal/admin/model/setup_org.go
Normal file
11
internal/admin/model/setup_org.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
)
|
||||
|
||||
type SetupOrg struct {
|
||||
*org_model.Org
|
||||
*usr_model.User
|
||||
}
|
60
internal/admin/repository/eventsourcing/eventstore/org.go
Normal file
60
internal/admin/repository/eventsourcing/eventstore/org.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
admin_model "github.com/caos/zitadel/internal/admin/model"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/sdk"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type OrgRepo struct {
|
||||
Eventstore eventstore.Eventstore
|
||||
OrgEventstore *org_es.OrgEventstore
|
||||
UserEventstore *usr_es.UserEventstore
|
||||
}
|
||||
|
||||
func (repo *OrgRepo) SetUpOrg(ctx context.Context, setUp *admin_model.SetupOrg) (*admin_model.SetupOrg, error) {
|
||||
org, aggregates, err := repo.OrgEventstore.PrepareCreateOrg(ctx, setUp.Org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, userAggregate, err := repo.UserEventstore.PrepareCreateUser(ctx, setUp.User, org.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aggregates = append(aggregates, userAggregate)
|
||||
setupModel := &Setup{Org: org, User: user}
|
||||
|
||||
member := org_model.NewOrgMemberWithRoles(org.AggregateID, user.AggregateID, "ORG_ADMIN") //TODO: role as const
|
||||
_, memberAggregate, err := repo.OrgEventstore.PrepareAddOrgMember(ctx, member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, memberAggregate)
|
||||
|
||||
err = sdk.PushAggregates(ctx, repo.Eventstore.PushAggregates, setupModel.AppendEvents, aggregates...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return SetupToModel(setupModel), nil
|
||||
}
|
||||
|
||||
func (repo *OrgRepo) OrgByID(ctx context.Context, id string) (*org_model.Org, error) {
|
||||
return repo.OrgEventstore.OrgByID(ctx, org_model.NewOrg(id))
|
||||
}
|
||||
|
||||
func (repo *OrgRepo) SearchOrgs(ctx context.Context) ([]*org_model.Org, error) {
|
||||
return nil, errors.ThrowUnimplemented(nil, "EVENT-hFIHK", "search not implemented")
|
||||
}
|
||||
|
||||
func (repo *OrgRepo) IsOrgUnique(ctx context.Context, name, domain string) (isUnique bool, err error) {
|
||||
return repo.OrgEventstore.IsOrgUnique(ctx, name, domain)
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
admin_model "github.com/caos/zitadel/internal/admin/model"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
type Setup struct {
|
||||
*org_es.Org
|
||||
*usr_es.User
|
||||
}
|
||||
|
||||
func (s *Setup) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
var err error
|
||||
switch event.AggregateType {
|
||||
case org_model.OrgAggregate:
|
||||
err = s.Org.AppendEvent(event)
|
||||
case usr_es.UserAggregate:
|
||||
err = s.User.AppendEvent(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetupToModel(setup *Setup) *admin_model.SetupOrg {
|
||||
return &admin_model.SetupOrg{
|
||||
Org: org_es.OrgToModel(setup.Org),
|
||||
User: usr_es.UserToModel(setup.User),
|
||||
}
|
||||
}
|
23
internal/admin/repository/eventsourcing/eventstore/user.go
Normal file
23
internal/admin/repository/eventsourcing/eventstore/user.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type UserRepo struct {
|
||||
UserEvents *usr_event.UserEventstore
|
||||
}
|
||||
|
||||
func (repo *UserRepo) UserByID(ctx context.Context, id string) (project *usr_model.User, err error) {
|
||||
return repo.UserEvents.UserByID(ctx, id)
|
||||
}
|
||||
|
||||
func (repo *UserRepo) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) {
|
||||
return repo.UserEvents.CreateUser(ctx, user)
|
||||
}
|
||||
|
||||
func (repo *UserRepo) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) {
|
||||
return repo.UserEvents.RegisterUser(ctx, user, resourceOwner)
|
||||
}
|
68
internal/admin/repository/eventsourcing/repository.go
Normal file
68
internal/admin/repository/eventsourcing/repository.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/admin/repository/eventsourcing/eventstore"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
es_int "github.com/caos/zitadel/internal/eventstore"
|
||||
es_org "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
es_usr "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Eventstore es_int.Config
|
||||
//View view.ViewConfig
|
||||
//Spooler spooler.SpoolerConfig
|
||||
}
|
||||
|
||||
type EsRepository struct {
|
||||
//spooler *es_spooler.Spooler
|
||||
eventstore.OrgRepo
|
||||
}
|
||||
|
||||
func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) {
|
||||
es, err := es_int.Start(conf.Eventstore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//view, sql, err := mgmt_view.StartView(conf.View)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
//conf.Spooler.View = view
|
||||
//conf.Spooler.EsClient = es.Client
|
||||
//conf.Spooler.SQL = sql
|
||||
//spool := spooler.StartSpooler(conf.Spooler)
|
||||
|
||||
org := es_org.StartOrg(es_org.OrgConfig{Eventstore: es})
|
||||
|
||||
user, err := es_usr.StartUser(es_usr.UserConfig{
|
||||
Eventstore: es,
|
||||
Cache: conf.Eventstore.Cache,
|
||||
}, systemDefaults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EsRepository{
|
||||
OrgRepo: eventstore.OrgRepo{
|
||||
Eventstore: es,
|
||||
OrgEventstore: org,
|
||||
UserEventstore: user,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *EsRepository) Health(ctx context.Context) error {
|
||||
err := repo.Eventstore.Health(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = repo.UserEventstore.Health(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return repo.OrgEventstore.Health(ctx)
|
||||
}
|
15
internal/admin/repository/org.go
Normal file
15
internal/admin/repository/org.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
admin_model "github.com/caos/zitadel/internal/admin/model"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
|
||||
type OrgRepository interface {
|
||||
SetUpOrg(context.Context, *admin_model.SetupOrg) (*admin_model.SetupOrg, error)
|
||||
IsOrgUnique(ctx context.Context, name, domain string) (bool, error)
|
||||
OrgByID(ctx context.Context, id string) (*org_model.Org, error)
|
||||
SearchOrgs(ctx context.Context) ([]*org_model.Org, error)
|
||||
}
|
8
internal/admin/repository/repository.go
Normal file
8
internal/admin/repository/repository.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package repository
|
||||
|
||||
import "context"
|
||||
|
||||
type Repository interface {
|
||||
Health(ctx context.Context) error
|
||||
OrgRepository
|
||||
}
|
@@ -74,5 +74,5 @@ func readerFuncForFile(configFile string) (ReaderFunc, error) {
|
||||
case ".toml":
|
||||
return TOMLReader, nil
|
||||
}
|
||||
return nil, errors.ThrowUnimplementedf(nil, "file extension (%s) not supported", ext)
|
||||
return nil, errors.ThrowUnimplementedf(nil, "CONFI-ZLk4u", "file extension (%s) not supported", ext)
|
||||
}
|
||||
|
25
internal/eventstore/internal/repository/sql/data.go
Normal file
25
internal/eventstore/internal/repository/sql/data.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package sql
|
||||
|
||||
import "database/sql/driver"
|
||||
|
||||
// Data represents a byte array that may be null.
|
||||
// Data implements the sql.Scanner interface
|
||||
type Data []byte
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (data *Data) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*data = nil
|
||||
return nil
|
||||
}
|
||||
*data = Data(value.([]byte))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (data Data) Value() (driver.Value, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []byte(data), nil
|
||||
}
|
@@ -24,16 +24,13 @@ var (
|
||||
expectedFilterEventsAggregateIDTypeLimit = regexp.MustCompile(selectEscaped + ` WHERE aggregate_id = \$1 AND aggregate_type = ANY\(\$2\) ORDER BY event_sequence LIMIT \$3`).String()
|
||||
expectedGetAllEvents = regexp.MustCompile(selectEscaped + ` ORDER BY event_sequence`).String()
|
||||
|
||||
expectedInsertStatement = regexp.MustCompile(`insert into eventstore\.events ` +
|
||||
expectedInsertStatement = regexp.MustCompile(`INSERT INTO eventstore\.events ` +
|
||||
`\(event_type, aggregate_type, aggregate_id, aggregate_version, creation_date, event_data, editor_user, editor_service, resource_owner, previous_sequence\) ` +
|
||||
`select \$1, \$2, \$3, \$4, coalesce\(\$5, now\(\)\), \$6, \$7, \$8, \$9, ` +
|
||||
`case \(select exists\(select event_sequence from eventstore\.events where aggregate_type = \$10 AND aggregate_id = \$11\)\) ` +
|
||||
`WHEN true then \(select event_sequence from eventstore\.events where aggregate_type = \$12 AND aggregate_id = \$13 order by event_sequence desc limit 1\) ` +
|
||||
`ELSE NULL ` +
|
||||
`end ` +
|
||||
`where \(` +
|
||||
`\(select count\(id\) from eventstore\.events where event_sequence >= COALESCE\(\$14, 0\) AND aggregate_type = \$15 AND aggregate_id = \$16\) = 1 OR ` +
|
||||
`\(\(select count\(id\) from eventstore\.events where aggregate_type = \$17 and aggregate_id = \$18\) = 0 AND COALESCE\(\$19, 0\) = 0\)\) RETURNING id, event_sequence, creation_date`).String()
|
||||
`SELECT \$1, \$2, \$3, \$4, COALESCE\(\$5, now\(\)\), \$6, \$7, \$8, \$9, \$10 ` +
|
||||
`WHERE EXISTS \(SELECT 1 WHERE ` +
|
||||
`EXISTS \(SELECT 1 FROM eventstore\.events WHERE event_sequence = COALESCE\(\$11, 0\) AND aggregate_type = \$12 AND aggregate_id = \$13\) OR ` +
|
||||
`NOT EXISTS \(SELECT 1 FROM eventstore\.events WHERE aggregate_type = \$14 AND aggregate_id = \$15\) AND COALESCE\(\$16, 0\) = 0\) ` +
|
||||
`RETURNING id, event_sequence, creation_date`).String()
|
||||
)
|
||||
|
||||
type dbMock struct {
|
||||
@@ -105,9 +102,7 @@ func (db *dbMock) expectRollback(err error) *dbMock {
|
||||
func (db *dbMock) expectInsertEvent(e *models.Event, returnedID string, returnedSequence uint64) *dbMock {
|
||||
db.mock.ExpectQuery(expectedInsertStatement).
|
||||
WithArgs(
|
||||
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), e.Data, e.EditorUser, e.EditorService, e.ResourceOwner,
|
||||
e.AggregateType, e.AggregateID,
|
||||
e.AggregateType, e.AggregateID,
|
||||
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), Data(e.Data), e.EditorUser, e.EditorService, e.ResourceOwner, Sequence(e.PreviousSequence),
|
||||
Sequence(e.PreviousSequence), e.AggregateType, e.AggregateID,
|
||||
e.AggregateType, e.AggregateID, Sequence(e.PreviousSequence),
|
||||
).
|
||||
@@ -122,9 +117,7 @@ func (db *dbMock) expectInsertEvent(e *models.Event, returnedID string, returned
|
||||
func (db *dbMock) expectInsertEventError(e *models.Event) *dbMock {
|
||||
db.mock.ExpectQuery(expectedInsertStatement).
|
||||
WithArgs(
|
||||
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), e.Data, e.EditorUser, e.EditorService, e.ResourceOwner,
|
||||
e.AggregateType, e.AggregateID,
|
||||
e.AggregateType, e.AggregateID,
|
||||
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), Data(e.Data), e.EditorUser, e.EditorService, e.ResourceOwner, Sequence(e.PreviousSequence),
|
||||
Sequence(e.PreviousSequence), e.AggregateType, e.AggregateID,
|
||||
e.AggregateType, e.AggregateID, Sequence(e.PreviousSequence),
|
||||
).
|
||||
|
@@ -54,6 +54,7 @@ func filter(querier Querier, searchQuery *es_models.SearchQuery) (events []*es_m
|
||||
for rows.Next() {
|
||||
event := new(models.Event)
|
||||
var previousSequence Sequence
|
||||
data := make(Data, 0)
|
||||
|
||||
err = rows.Scan(
|
||||
&event.ID,
|
||||
@@ -61,7 +62,7 @@ func filter(querier Querier, searchQuery *es_models.SearchQuery) (events []*es_m
|
||||
&event.Type,
|
||||
&event.Sequence,
|
||||
&previousSequence,
|
||||
&event.Data,
|
||||
&data,
|
||||
&event.EditorService,
|
||||
&event.EditorUser,
|
||||
&event.ResourceOwner,
|
||||
@@ -76,6 +77,10 @@ func filter(querier Querier, searchQuery *es_models.SearchQuery) (events []*es_m
|
||||
}
|
||||
|
||||
event.PreviousSequence = uint64(previousSequence)
|
||||
|
||||
event.Data = make([]byte, len(data))
|
||||
copy(event.Data, data)
|
||||
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
|
@@ -11,19 +11,14 @@ import (
|
||||
"github.com/cockroachdb/cockroach-go/crdb"
|
||||
)
|
||||
|
||||
const insertStmt = "insert into eventstore.events " +
|
||||
const insertStmt = "INSERT INTO eventstore.events " +
|
||||
"(event_type, aggregate_type, aggregate_id, aggregate_version, creation_date, event_data, editor_user, editor_service, resource_owner, previous_sequence) " +
|
||||
"select $1, $2, $3, $4, coalesce($5, now()), $6, $7, $8, $9, " +
|
||||
// case is to set the highest sequence or NULL in previous_sequence
|
||||
"case (select exists(select event_sequence from eventstore.events where aggregate_type = $10 AND aggregate_id = $11)) " +
|
||||
"WHEN true then (select event_sequence from eventstore.events where aggregate_type = $12 AND aggregate_id = $13 order by event_sequence desc limit 1) " +
|
||||
"ELSE NULL " +
|
||||
"end " +
|
||||
"where (" +
|
||||
// exactly one event of requested aggregate must have a >= sequence (last inserted event)
|
||||
"(select count(id) from eventstore.events where event_sequence >= COALESCE($14, 0) AND aggregate_type = $15 AND aggregate_id = $16) = 1 OR " +
|
||||
// previous sequence = 0, no events must exist for the requested aggregate
|
||||
"((select count(id) from eventstore.events where aggregate_type = $17 and aggregate_id = $18) = 0 AND COALESCE($19, 0) = 0)) " +
|
||||
"SELECT $1, $2, $3, $4, COALESCE($5, now()), $6, $7, $8, $9, $10 " +
|
||||
"WHERE EXISTS (SELECT 1 WHERE " +
|
||||
// exactly one event of requested aggregate must have the given previous sequence as sequence (last inserted event)
|
||||
"EXISTS (SELECT 1 FROM eventstore.events WHERE event_sequence = COALESCE($11, 0) AND aggregate_type = $12 AND aggregate_id = $13) OR " +
|
||||
// if previous sequence = 0, no events must exist for the requested aggregate
|
||||
"NOT EXISTS (SELECT 1 FROM eventstore.events WHERE aggregate_type = $14 AND aggregate_id = $15) AND COALESCE($16, 0) = 0) " +
|
||||
"RETURNING id, event_sequence, creation_date"
|
||||
|
||||
func (db *SQL) PushAggregates(ctx context.Context, aggregates ...*models.Aggregate) (err error) {
|
||||
@@ -74,14 +69,7 @@ func precondtion(tx *sql.Tx, aggregate *models.Aggregate) error {
|
||||
|
||||
func insertEvents(stmt *sql.Stmt, previousSequence Sequence, events []*models.Event) error {
|
||||
for _, event := range events {
|
||||
if event.Data == nil || len(event.Data) == 0 {
|
||||
//json decoder failes with EOF if json text is empty
|
||||
event.Data = []byte("{}")
|
||||
}
|
||||
|
||||
rows, err := stmt.Query(event.Type, event.AggregateType, event.AggregateID, event.AggregateVersion, event.CreationDate, event.Data, event.EditorUser, event.EditorService, event.ResourceOwner,
|
||||
event.AggregateType, event.AggregateID,
|
||||
event.AggregateType, event.AggregateID,
|
||||
rows, err := stmt.Query(event.Type, event.AggregateType, event.AggregateID, event.AggregateVersion, event.CreationDate, Data(event.Data), event.EditorUser, event.EditorService, event.ResourceOwner, previousSequence,
|
||||
previousSequence, event.AggregateType, event.AggregateID,
|
||||
event.AggregateType, event.AggregateID, previousSequence)
|
||||
|
||||
@@ -99,6 +87,7 @@ func insertEvents(stmt *sql.Stmt, previousSequence Sequence, events []*models.Ev
|
||||
}
|
||||
|
||||
if !rowInserted {
|
||||
logging.LogWithFields("SQL-5aATu", "aggregate", event.AggregateType, "id", event.AggregateID).Info("wrong sequence")
|
||||
return caos_errs.ThrowAlreadyExists(nil, "SQL-GKcAa", "wrong sequence")
|
||||
}
|
||||
|
||||
|
@@ -89,7 +89,6 @@ func TestSQL_PushAggregates(t *testing.T) {
|
||||
ResourceOwner: "ro",
|
||||
PreviousSequence: 34,
|
||||
Type: "eventTyp",
|
||||
Data: []byte("{}"),
|
||||
AggregateVersion: "v0.0.1",
|
||||
},
|
||||
"asdfölk-234", 45).
|
||||
@@ -101,7 +100,6 @@ func TestSQL_PushAggregates(t *testing.T) {
|
||||
ResourceOwner: "ro2",
|
||||
PreviousSequence: 45,
|
||||
Type: "eventTyp",
|
||||
Data: []byte("{}"),
|
||||
AggregateVersion: "v0.0.1",
|
||||
}, "asdfölk-233", 46).
|
||||
expectReleaseSavepoint(nil).
|
||||
@@ -151,7 +149,6 @@ func TestSQL_PushAggregates(t *testing.T) {
|
||||
EditorUser: "usr",
|
||||
ResourceOwner: "ro",
|
||||
PreviousSequence: 34,
|
||||
Data: []byte("{}"),
|
||||
Type: "eventTyp",
|
||||
AggregateVersion: "v0.0.1",
|
||||
}, "asdfölk-233", 47).
|
||||
@@ -162,7 +159,6 @@ func TestSQL_PushAggregates(t *testing.T) {
|
||||
EditorUser: "usr",
|
||||
ResourceOwner: "ro",
|
||||
PreviousSequence: 40,
|
||||
Data: []byte("{}"),
|
||||
Type: "eventTyp",
|
||||
AggregateVersion: "v0.0.1",
|
||||
}, "asdfölk-233", 48).
|
||||
@@ -410,8 +406,11 @@ func Test_precondtion(t *testing.T) {
|
||||
t.FailNow()
|
||||
}
|
||||
err = precondtion(tx, tt.args.aggregate)
|
||||
if (tt.isErr != nil && err == nil) || (tt.isErr != nil && !tt.isErr(err)) {
|
||||
t.Error("precondtion() wrong error", err)
|
||||
if (err != nil) && (tt.isErr == nil) {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.isErr != nil && !tt.isErr(err) {
|
||||
t.Errorf("precondtion() wrong error %T, %v", err, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -5,16 +5,16 @@ import (
|
||||
)
|
||||
|
||||
// Sequence represents a number that may be null.
|
||||
// Sequence implements the sql.Scanner interface so
|
||||
// Sequence implements the sql.Scanner interface
|
||||
type Sequence uint64
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (n *Sequence) Scan(value interface{}) error {
|
||||
func (seq *Sequence) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*n = 0
|
||||
*seq = 0
|
||||
return nil
|
||||
}
|
||||
*n = Sequence(value.(int64))
|
||||
*seq = Sequence(value.(int64))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -15,7 +15,7 @@ func (at AggregateType) String() string {
|
||||
type Aggregates []*Aggregate
|
||||
|
||||
type Aggregate struct {
|
||||
id string
|
||||
ID string
|
||||
typ AggregateType
|
||||
PreviousSequence uint64
|
||||
version Version
|
||||
@@ -45,7 +45,7 @@ func (a *Aggregate) AppendEvent(typ EventType, payload interface{}) (*Aggregate,
|
||||
CreationDate: time.Now(),
|
||||
Data: data,
|
||||
Type: typ,
|
||||
AggregateID: a.id,
|
||||
AggregateID: a.ID,
|
||||
AggregateType: a.typ,
|
||||
AggregateVersion: a.version,
|
||||
EditorService: a.editorService,
|
||||
@@ -66,7 +66,7 @@ func (a *Aggregate) Validate() error {
|
||||
if a == nil {
|
||||
return errors.ThrowPreconditionFailed(nil, "MODEL-yi5AC", "aggregate is nil")
|
||||
}
|
||||
if a.id == "" {
|
||||
if a.ID == "" {
|
||||
return errors.ThrowPreconditionFailed(nil, "MODEL-FSjKV", "id not set")
|
||||
}
|
||||
if string(a.typ) == "" {
|
||||
|
@@ -22,7 +22,7 @@ func (c *AggregateCreator) NewAggregate(ctx context.Context, id string, typ Aggr
|
||||
resourceOwner := ctxData.OrgID
|
||||
|
||||
aggregate := &Aggregate{
|
||||
id: id,
|
||||
ID: id,
|
||||
typ: typ,
|
||||
PreviousSequence: previousSequence,
|
||||
version: version,
|
||||
|
@@ -83,7 +83,7 @@ func TestAggregateCreator_NewAggregate(t *testing.T) {
|
||||
creator: &AggregateCreator{serviceName: "admin"},
|
||||
wantErr: false,
|
||||
want: &Aggregate{
|
||||
id: "hodor",
|
||||
ID: "hodor",
|
||||
Events: make([]*Event, 0, 2),
|
||||
typ: "user",
|
||||
version: "v1.0.0",
|
||||
|
@@ -114,7 +114,7 @@ func TestAggregate_Validate(t *testing.T) {
|
||||
name: "no type error",
|
||||
wantErr: true,
|
||||
fields: fields{aggregate: &Aggregate{
|
||||
id: "aggID",
|
||||
ID: "aggID",
|
||||
version: "v1.0.0",
|
||||
editorService: "svc",
|
||||
editorUser: "hodor",
|
||||
@@ -135,7 +135,7 @@ func TestAggregate_Validate(t *testing.T) {
|
||||
name: "invalid version error",
|
||||
wantErr: true,
|
||||
fields: fields{aggregate: &Aggregate{
|
||||
id: "aggID",
|
||||
ID: "aggID",
|
||||
typ: "user",
|
||||
editorService: "svc",
|
||||
editorUser: "hodor",
|
||||
@@ -156,7 +156,7 @@ func TestAggregate_Validate(t *testing.T) {
|
||||
name: "no query in precondition error",
|
||||
wantErr: true,
|
||||
fields: fields{aggregate: &Aggregate{
|
||||
id: "aggID",
|
||||
ID: "aggID",
|
||||
typ: "user",
|
||||
version: "v1.0.0",
|
||||
editorService: "svc",
|
||||
@@ -182,7 +182,7 @@ func TestAggregate_Validate(t *testing.T) {
|
||||
name: "no func in precondition error",
|
||||
wantErr: true,
|
||||
fields: fields{aggregate: &Aggregate{
|
||||
id: "aggID",
|
||||
ID: "aggID",
|
||||
typ: "user",
|
||||
version: "v1.0.0",
|
||||
editorService: "svc",
|
||||
@@ -208,7 +208,7 @@ func TestAggregate_Validate(t *testing.T) {
|
||||
name: "validation without precondition ok",
|
||||
wantErr: false,
|
||||
fields: fields{aggregate: &Aggregate{
|
||||
id: "aggID",
|
||||
ID: "aggID",
|
||||
typ: "user",
|
||||
version: "v1.0.0",
|
||||
editorService: "svc",
|
||||
@@ -231,7 +231,7 @@ func TestAggregate_Validate(t *testing.T) {
|
||||
name: "validation with precondition ok",
|
||||
wantErr: false,
|
||||
fields: fields{aggregate: &Aggregate{
|
||||
id: "aggID",
|
||||
ID: "aggID",
|
||||
typ: "user",
|
||||
version: "v1.0.0",
|
||||
editorService: "svc",
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package models
|
||||
|
||||
import "github.com/caos/zitadel/internal/errors"
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
type SearchQuery struct {
|
||||
Limit uint64
|
||||
@@ -33,6 +35,10 @@ func (q *SearchQuery) AggregateIDFilter(id string) *SearchQuery {
|
||||
return q.setFilter(NewFilter(Field_AggregateID, id, Operation_Equals))
|
||||
}
|
||||
|
||||
func (q *SearchQuery) AggregateIDsFilter(ids ...string) *SearchQuery {
|
||||
return q.setFilter(NewFilter(Field_AggregateID, ids, Operation_In))
|
||||
}
|
||||
|
||||
func (q *SearchQuery) AggregateTypeFilter(types ...AggregateType) *SearchQuery {
|
||||
return q.setFilter(NewFilter(Field_AggregateType, types, Operation_In))
|
||||
}
|
||||
|
@@ -0,0 +1,52 @@
|
||||
package eventstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type OrgRepository struct {
|
||||
*org_es.OrgEventstore
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) OrgByID(ctx context.Context, id string) (*org_model.Org, error) {
|
||||
org := org_model.NewOrg(id)
|
||||
return repo.OrgEventstore.OrgByID(ctx, org)
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) OrgByDomainGlobal(ctx context.Context, domain string) (*org_model.Org, error) {
|
||||
return nil, errors.ThrowUnimplemented(nil, "EVENT-GQoS8", "not implemented")
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) UpdateOrg(ctx context.Context, org *org_model.Org) (*org_model.Org, error) {
|
||||
return nil, errors.ThrowUnimplemented(nil, "EVENT-RkurR", "not implemented")
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) DeactivateOrg(ctx context.Context, id string) (*org_model.Org, error) {
|
||||
return repo.OrgEventstore.DeactivateOrg(ctx, id)
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) ReactivateOrg(ctx context.Context, id string) (*org_model.Org, error) {
|
||||
return repo.OrgEventstore.ReactivateOrg(ctx, id)
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) OrgMemberByID(ctx context.Context, orgID, userID string) (member *org_model.OrgMember, err error) {
|
||||
member = org_model.NewOrgMember(orgID, userID)
|
||||
return repo.OrgEventstore.OrgMemberByIDs(ctx, member)
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) AddOrgMember(ctx context.Context, member *org_model.OrgMember) (*org_model.OrgMember, error) {
|
||||
return repo.OrgEventstore.AddOrgMember(ctx, member)
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) ChangeOrgMember(ctx context.Context, member *org_model.OrgMember) (*org_model.OrgMember, error) {
|
||||
return repo.OrgEventstore.ChangeOrgMember(ctx, member)
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) RemoveOrgMember(ctx context.Context, orgID, userID string) error {
|
||||
member := org_model.NewOrgMember(orgID, userID)
|
||||
return repo.OrgEventstore.RemoveOrgMember(ctx, member)
|
||||
}
|
@@ -2,6 +2,7 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
es_int "github.com/caos/zitadel/internal/eventstore"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/caos/zitadel/internal/management/repository/eventsourcing/handler"
|
||||
"github.com/caos/zitadel/internal/management/repository/eventsourcing/spooler"
|
||||
mgmt_view "github.com/caos/zitadel/internal/management/repository/eventsourcing/view"
|
||||
es_org "github.com/caos/zitadel/internal/org/repository/eventsourcing"
|
||||
es_proj "github.com/caos/zitadel/internal/project/repository/eventsourcing"
|
||||
es_usr "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
es_grant "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing"
|
||||
@@ -24,6 +26,7 @@ type Config struct {
|
||||
|
||||
type EsRepository struct {
|
||||
spooler *es_spol.Spooler
|
||||
eventstore.OrgRepository
|
||||
eventstore.ProjectRepo
|
||||
eventstore.UserRepo
|
||||
eventstore.UserGrantRepo
|
||||
@@ -65,14 +68,17 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
org := es_org.StartOrg(es_org.OrgConfig{Eventstore: es})
|
||||
|
||||
eventstoreRepos := handler.EventstoreRepos{ProjectEvents: project}
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, eventstoreRepos)
|
||||
|
||||
return &EsRepository{
|
||||
spool,
|
||||
eventstore.ProjectRepo{conf.SearchLimit, project, view},
|
||||
eventstore.UserRepo{conf.SearchLimit, user, view},
|
||||
eventstore.UserGrantRepo{conf.SearchLimit, usergrant, view},
|
||||
spooler: spool,
|
||||
OrgRepository: eventstore.OrgRepository{org},
|
||||
ProjectRepo: eventstore.ProjectRepo{conf.SearchLimit, project, view},
|
||||
UserRepo: eventstore.UserRepo{conf.SearchLimit, user, view},
|
||||
UserGrantRepo: eventstore.UserGrantRepo{conf.SearchLimit, usergrant, view},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
21
internal/management/repository/org.go
Normal file
21
internal/management/repository/org.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
|
||||
type OrgRepository interface {
|
||||
OrgByID(ctx context.Context, id string) (*org_model.Org, error)
|
||||
OrgByDomainGlobal(ctx context.Context, domain string) (*org_model.Org, error)
|
||||
UpdateOrg(ctx context.Context, org *org_model.Org) (*org_model.Org, error)
|
||||
DeactivateOrg(ctx context.Context, id string) (*org_model.Org, error)
|
||||
ReactivateOrg(ctx context.Context, id string) (*org_model.Org, error)
|
||||
}
|
||||
|
||||
type OrgMemberRepository interface {
|
||||
AddOrgMember(ctx context.Context, member *org_model.OrgMember) (*org_model.OrgMember, error)
|
||||
ChangeOrgMember(ctx context.Context, member *org_model.OrgMember) (*org_model.OrgMember, error)
|
||||
RemoveOrgMember(ctx context.Context, orgID, userID string) error
|
||||
}
|
@@ -3,6 +3,8 @@ package repository
|
||||
type Repository interface {
|
||||
Health() error
|
||||
ProjectRepository
|
||||
OrgRepository
|
||||
OrgMemberRepository
|
||||
UserRepository
|
||||
UserGrantRepository
|
||||
}
|
||||
|
21
internal/org/model/member.go
Normal file
21
internal/org/model/member.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
|
||||
type OrgMember struct {
|
||||
es_models.ObjectRoot
|
||||
UserID string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func NewOrgMember(orgID, userID string) *OrgMember {
|
||||
return &OrgMember{ObjectRoot: es_models.ObjectRoot{AggregateID: orgID}, UserID: userID}
|
||||
}
|
||||
|
||||
func NewOrgMemberWithRoles(orgID, userID string, roles ...string) *OrgMember {
|
||||
return &OrgMember{ObjectRoot: es_models.ObjectRoot{AggregateID: orgID}, UserID: userID, Roles: roles}
|
||||
}
|
||||
|
||||
func (member *OrgMember) IsValid() bool {
|
||||
return member.AggregateID != "" && member.UserID != ""
|
||||
}
|
43
internal/org/model/org.go
Normal file
43
internal/org/model/org.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
)
|
||||
|
||||
type Org struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
State OrgState
|
||||
Name string
|
||||
Domain string
|
||||
|
||||
Members []*OrgMember
|
||||
}
|
||||
|
||||
type OrgState int32
|
||||
|
||||
const (
|
||||
ORGSTATE_ACTIVE OrgState = iota
|
||||
ORGSTATE_INACTIVE
|
||||
)
|
||||
|
||||
func NewOrg(id string) *Org {
|
||||
return &Org{ObjectRoot: es_models.ObjectRoot{AggregateID: id}, State: ORGSTATE_ACTIVE}
|
||||
}
|
||||
|
||||
func (o *Org) IsActive() bool {
|
||||
return o.State == ORGSTATE_ACTIVE
|
||||
}
|
||||
|
||||
func (o *Org) IsValid() bool {
|
||||
return o.Name != "" && o.Domain != ""
|
||||
}
|
||||
|
||||
func (o *Org) ContainsMember(userID string) bool {
|
||||
for _, member := range o.Members {
|
||||
if member.UserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
24
internal/org/model/types.go
Normal file
24
internal/org/model/types.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import "github.com/caos/zitadel/internal/eventstore/models"
|
||||
|
||||
const (
|
||||
OrgAggregate models.AggregateType = "org"
|
||||
OrgDomainAggregate models.AggregateType = "org.domain"
|
||||
OrgNameAggregate models.AggregateType = "org.name"
|
||||
|
||||
OrgAdded models.EventType = "org.added"
|
||||
OrgChanged models.EventType = "org.changed"
|
||||
OrgDeactivated models.EventType = "org.deactivated"
|
||||
OrgReactivated models.EventType = "org.reactivated"
|
||||
|
||||
OrgNameReserved models.EventType = "org.name.reserved"
|
||||
OrgNameReleased models.EventType = "org.name.released"
|
||||
|
||||
OrgDomainReserved models.EventType = "org.domain.reserved"
|
||||
OrgDomainReleased models.EventType = "org.domain.released"
|
||||
|
||||
OrgMemberAdded models.EventType = "org.member.added"
|
||||
OrgMemberChanged models.EventType = "org.member.changed"
|
||||
OrgMemberRemoved models.EventType = "org.member.removed"
|
||||
)
|
215
internal/org/repository/eventsourcing/eventstore.go
Normal file
215
internal/org/repository/eventsourcing/eventstore.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
|
||||
type OrgEventstore struct {
|
||||
eventstore.Eventstore
|
||||
}
|
||||
|
||||
type OrgConfig struct {
|
||||
eventstore.Eventstore
|
||||
}
|
||||
|
||||
func StartOrg(conf OrgConfig) *OrgEventstore {
|
||||
return &OrgEventstore{Eventstore: conf.Eventstore}
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) PrepareCreateOrg(ctx context.Context, orgModel *org_model.Org) (*Org, []*es_models.Aggregate, error) {
|
||||
if orgModel == nil || !orgModel.IsValid() {
|
||||
return nil, nil, errors.ThrowInvalidArgument(nil, "EVENT-OeLSk", "org not valid")
|
||||
}
|
||||
id, err := idGenerator.NextID()
|
||||
if err != nil {
|
||||
return nil, nil, errors.ThrowInternal(err, "EVENT-OwciI", "id gen failed")
|
||||
}
|
||||
orgModel.AggregateID = strconv.FormatUint(id, 10)
|
||||
org := OrgFromModel(orgModel)
|
||||
|
||||
aggregates, err := orgCreatedAggregates(ctx, es.AggregateCreator(), org)
|
||||
|
||||
return org, aggregates, err
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) CreateOrg(ctx context.Context, orgModel *org_model.Org) (*org_model.Org, error) {
|
||||
org, aggregates, err := es.PrepareCreateOrg(ctx, orgModel)
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, org.AppendEvents, aggregates...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return OrgToModel(org), nil
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) OrgByID(ctx context.Context, org *org_model.Org) (*org_model.Org, error) {
|
||||
if org == nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-gQTYP", "org not set")
|
||||
}
|
||||
query, err := OrgByIDQuery(org.AggregateID, org.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
esOrg := OrgFromModel(org)
|
||||
err = es_sdk.Filter(ctx, es.FilterEvents, esOrg.AppendEvents, query)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if esOrg.Sequence == 0 {
|
||||
return nil, errors.ThrowNotFound(nil, "EVENT-kVLb2", "org not found")
|
||||
}
|
||||
|
||||
return OrgToModel(esOrg), nil
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) IsOrgUnique(ctx context.Context, name, domain string) (isUnique bool, err error) {
|
||||
var found bool
|
||||
err = es_sdk.Filter(ctx, es.FilterEvents, isUniqueValidation(&found), OrgNameUniqueQuery(name))
|
||||
if (err != nil && !errors.IsNotFound(err)) || found {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = es_sdk.Filter(ctx, es.FilterEvents, isUniqueValidation(&found), OrgDomainUniqueQuery(domain))
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return !found, nil
|
||||
}
|
||||
|
||||
func isUniqueValidation(unique *bool) func(events ...*es_models.Event) error {
|
||||
return func(events ...*es_models.Event) error {
|
||||
if len(events) == 0 {
|
||||
return nil
|
||||
}
|
||||
*unique = *unique || events[0].Type == org_model.OrgDomainReserved || events[0].Type == org_model.OrgNameReserved
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) DeactivateOrg(ctx context.Context, orgID string) (*org_model.Org, error) {
|
||||
existingOrg, err := es.OrgByID(ctx, org_model.NewOrg(orgID))
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-oL9nT", "org not found")
|
||||
}
|
||||
org := OrgFromModel(existingOrg)
|
||||
|
||||
aggregate := orgDeactivateAggregate(es.AggregateCreator(), org)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, org.AppendEvents, aggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return OrgToModel(org), nil
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) ReactivateOrg(ctx context.Context, orgID string) (*org_model.Org, error) {
|
||||
existingOrg, err := es.OrgByID(ctx, org_model.NewOrg(orgID))
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-oL9nT", "org not set")
|
||||
}
|
||||
org := OrgFromModel(existingOrg)
|
||||
|
||||
aggregate := orgReactivateAggregate(es.AggregateCreator(), org)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, org.AppendEvents, aggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
}
|
||||
return OrgToModel(org), nil
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) OrgMemberByIDs(ctx context.Context, member *org_model.OrgMember) (*org_model.OrgMember, error) {
|
||||
if member == nil || member.UserID == "" || member.AggregateID == "" {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-ld93d", "member not set")
|
||||
}
|
||||
|
||||
org, err := es.OrgByID(ctx, &org_model.Org{ObjectRoot: member.ObjectRoot, Members: []*org_model.OrgMember{member}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, currentMember := range org.Members {
|
||||
if currentMember.UserID == member.UserID {
|
||||
return currentMember, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.ThrowNotFound(nil, "EVENT-SXji6", "member not found")
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) PrepareAddOrgMember(ctx context.Context, member *org_model.OrgMember) (*OrgMember, *es_models.Aggregate, error) {
|
||||
if member == nil || !member.IsValid() {
|
||||
return nil, nil, errors.ThrowPreconditionFailed(nil, "EVENT-9dk45", "UserID and Roles are required")
|
||||
}
|
||||
|
||||
repoMember := OrgMemberFromModel(member)
|
||||
addAggregate, err := orgMemberAddedAggregate(ctx, es.Eventstore.AggregateCreator(), repoMember)
|
||||
|
||||
return repoMember, addAggregate, err
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) AddOrgMember(ctx context.Context, member *org_model.OrgMember) (*org_model.OrgMember, error) {
|
||||
repoMember, addAggregate, err := es.PrepareAddOrgMember(ctx, member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoMember.AppendEvents, addAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return OrgMemberToModel(repoMember), nil
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) ChangeOrgMember(ctx context.Context, member *org_model.OrgMember) (*org_model.OrgMember, error) {
|
||||
if member == nil || !member.IsValid() {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-9dk45", "UserID and Roles are required")
|
||||
}
|
||||
|
||||
existingMember, err := es.OrgMemberByIDs(ctx, member)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
member.ObjectRoot = existingMember.ObjectRoot
|
||||
repoMember := OrgMemberFromModel(member)
|
||||
repoExistingMember := OrgMemberFromModel(existingMember)
|
||||
|
||||
orgAggregate := orgMemberChangedAggregate(es.Eventstore.AggregateCreator(), repoExistingMember, repoMember)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoMember.AppendEvents, orgAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return OrgMemberToModel(repoMember), nil
|
||||
}
|
||||
|
||||
func (es *OrgEventstore) RemoveOrgMember(ctx context.Context, member *org_model.OrgMember) error {
|
||||
if member == nil || member.UserID == "" {
|
||||
return errors.ThrowInvalidArgument(nil, "EVENT-d43fs", "UserID is required")
|
||||
}
|
||||
|
||||
existingMember, err := es.OrgMemberByIDs(ctx, member)
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
member.ObjectRoot = existingMember.ObjectRoot
|
||||
repoMember := OrgMemberFromModel(member)
|
||||
|
||||
orgAggregate := orgMemberRemovedAggregate(es.Eventstore.AggregateCreator(), repoMember)
|
||||
return es_sdk.Push(ctx, es.PushAggregates, repoMember.AppendEvents, orgAggregate)
|
||||
}
|
1026
internal/org/repository/eventsourcing/eventstore_test.go
Normal file
1026
internal/org/repository/eventsourcing/eventstore_test.go
Normal file
File diff suppressed because it is too large
Load Diff
96
internal/org/repository/eventsourcing/member_model.go
Normal file
96
internal/org/repository/eventsourcing/member_model.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
|
||||
type OrgMember struct {
|
||||
es_models.ObjectRoot `json:"-"`
|
||||
|
||||
UserID string `json:"userId,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
func (m *OrgMember) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
err := m.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *OrgMember) AppendEvent(event *es_models.Event) error {
|
||||
m.ObjectRoot.AppendEvent(event)
|
||||
|
||||
return m.setData(event)
|
||||
}
|
||||
|
||||
func (m *OrgMember) setData(event *es_models.Event) error {
|
||||
err := json.Unmarshal(event.Data, m)
|
||||
if err != nil {
|
||||
return errors.ThrowInternal(err, "EVENT-Hz7Mb", "unable to unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *OrgMember) Changes(updatedMember *OrgMember) map[string]interface{} {
|
||||
changes := make(map[string]interface{}, 2)
|
||||
|
||||
if !reflect.DeepEqual(m.Roles, updatedMember.Roles) {
|
||||
changes["roles"] = updatedMember.Roles
|
||||
changes["userId"] = m.UserID
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
func OrgMemberFromEvent(member *OrgMember, event *es_models.Event) (*OrgMember, error) {
|
||||
if member == nil {
|
||||
member = new(OrgMember)
|
||||
}
|
||||
member.ObjectRoot.AppendEvent(event)
|
||||
err := json.Unmarshal(event.Data, member)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "EVENT-D4qxo", "invalid event data")
|
||||
}
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func OrgMembersFromModel(members []*model.OrgMember) []*OrgMember {
|
||||
convertedMembers := make([]*OrgMember, len(members))
|
||||
for i, m := range members {
|
||||
convertedMembers[i] = OrgMemberFromModel(m)
|
||||
}
|
||||
return convertedMembers
|
||||
}
|
||||
|
||||
func OrgMemberFromModel(member *model.OrgMember) *OrgMember {
|
||||
return &OrgMember{
|
||||
ObjectRoot: member.ObjectRoot,
|
||||
UserID: member.UserID,
|
||||
Roles: member.Roles,
|
||||
}
|
||||
}
|
||||
|
||||
func OrgMembersToModel(members []*OrgMember) []*model.OrgMember {
|
||||
convertedMembers := make([]*model.OrgMember, len(members))
|
||||
for i, m := range members {
|
||||
convertedMembers[i] = OrgMemberToModel(m)
|
||||
}
|
||||
return convertedMembers
|
||||
}
|
||||
|
||||
func OrgMemberToModel(member *OrgMember) *model.OrgMember {
|
||||
return &model.OrgMember{
|
||||
ObjectRoot: member.ObjectRoot,
|
||||
UserID: member.UserID,
|
||||
Roles: member.Roles,
|
||||
}
|
||||
}
|
195
internal/org/repository/eventsourcing/org.go
Normal file
195
internal/org/repository/eventsourcing/org.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
"github.com/sony/sonyflake"
|
||||
)
|
||||
|
||||
var idGenerator = sonyflake.NewSonyflake(sonyflake.Settings{})
|
||||
|
||||
func OrgByIDQuery(id string, latestSequence uint64) (*es_models.SearchQuery, error) {
|
||||
if id == "" {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dke74", "id should be filled")
|
||||
}
|
||||
return OrgQuery(latestSequence).
|
||||
AggregateIDFilter(id), nil
|
||||
}
|
||||
|
||||
func OrgDomainUniqueQuery(domain string) *es_models.SearchQuery {
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_model.OrgDomainAggregate).
|
||||
AggregateIDFilter(domain).
|
||||
OrderDesc().
|
||||
SetLimit(1)
|
||||
}
|
||||
|
||||
func OrgNameUniqueQuery(name string) *es_models.SearchQuery {
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_model.OrgNameAggregate).
|
||||
AggregateIDFilter(name).
|
||||
OrderDesc().
|
||||
SetLimit(1)
|
||||
}
|
||||
|
||||
func OrgQuery(latestSequence uint64) *es_models.SearchQuery {
|
||||
return es_models.NewSearchQuery().
|
||||
AggregateTypeFilter(org_model.OrgAggregate).
|
||||
LatestSequenceFilter(latestSequence)
|
||||
}
|
||||
|
||||
func OrgAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, id string, sequence uint64) (*es_models.Aggregate, error) {
|
||||
return aggCreator.NewAggregate(ctx, id, org_model.OrgAggregate, orgVersion, sequence)
|
||||
}
|
||||
|
||||
func orgCreatedAggregates(ctx context.Context, aggCreator *es_models.AggregateCreator, org *Org) (_ []*es_models.Aggregate, err error) {
|
||||
if org == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-kdie7", "org should not be nil")
|
||||
}
|
||||
|
||||
domainAgrregate, err := uniqueDomainAggregate(ctx, aggCreator, org.Domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nameAggregate, err := uniqueNameAggregate(ctx, aggCreator, org.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agg, err := aggCreator.NewAggregate(ctx, org.AggregateID, org_model.OrgAggregate, orgVersion, org.Sequence, es_models.OverwriteResourceOwner(org.AggregateID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agg, err = agg.AppendEvent(org_model.OrgAdded, org)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []*es_models.Aggregate{
|
||||
agg,
|
||||
domainAgrregate,
|
||||
nameAggregate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func OrgUpdateAggregates(ctx context.Context, aggCreator *es_models.AggregateCreator, existing *Org, updated *Org) ([]*es_models.Aggregate, error) {
|
||||
if existing == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dk83d", "existing org must not be nil")
|
||||
}
|
||||
if updated == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dhr74", "updated org must not be nil")
|
||||
}
|
||||
changes := existing.Changes(updated)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-E0hc5", "no changes")
|
||||
}
|
||||
|
||||
aggregates := make([]*es_models.Aggregate, 0, 3)
|
||||
|
||||
if name, ok := changes["name"]; ok {
|
||||
nameAggregate, err := uniqueNameAggregate(ctx, aggCreator, name.(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, nameAggregate)
|
||||
}
|
||||
|
||||
if name, ok := changes["domain"]; ok {
|
||||
domainAggregate, err := uniqueDomainAggregate(ctx, aggCreator, name.(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, domainAggregate)
|
||||
}
|
||||
|
||||
orgAggregate, err := OrgAggregate(ctx, aggCreator, existing.AggregateID, existing.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgAggregate, err = orgAggregate.AppendEvent(org_model.OrgChanged, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregates = append(aggregates, orgAggregate)
|
||||
|
||||
return aggregates, nil
|
||||
}
|
||||
|
||||
func orgDeactivateAggregate(aggCreator *es_models.AggregateCreator, org *Org) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if org == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-R03z8", "existing org must not be nil")
|
||||
}
|
||||
if org.State == int32(org_model.ORGSTATE_INACTIVE) {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-mcPH0", "org already inactive")
|
||||
}
|
||||
agg, err := OrgAggregate(ctx, aggCreator, org.AggregateID, org.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return agg.AppendEvent(org_model.OrgDeactivated, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func orgReactivateAggregate(aggCreator *es_models.AggregateCreator, org *Org) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if org == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-cTHLd", "existing org must not be nil")
|
||||
}
|
||||
if org.State == int32(org_model.ORGSTATE_ACTIVE) {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-pUSMs", "org already active")
|
||||
}
|
||||
agg, err := OrgAggregate(ctx, aggCreator, org.AggregateID, org.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return agg.AppendEvent(org_model.OrgReactivated, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueDomainAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, domain string) (*es_models.Aggregate, error) {
|
||||
aggregate, err := aggCreator.NewAggregate(ctx, domain, org_model.OrgDomainAggregate, orgVersion, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregate, err = aggregate.AppendEvent(org_model.OrgDomainReserved, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aggregate.SetPrecondition(OrgDomainUniqueQuery(domain), isReservedValidation(aggregate, org_model.OrgDomainReserved)), nil
|
||||
}
|
||||
|
||||
func uniqueNameAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, name string) (*es_models.Aggregate, error) {
|
||||
aggregate, err := aggCreator.NewAggregate(ctx, name, org_model.OrgNameAggregate, orgVersion, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregate, err = aggregate.AppendEvent(org_model.OrgNameReserved, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aggregate.SetPrecondition(OrgNameUniqueQuery(name), isReservedValidation(aggregate, org_model.OrgNameReserved)), nil
|
||||
}
|
||||
|
||||
func isReservedValidation(aggregate *es_models.Aggregate, resevedEventType es_models.EventType) func(...*es_models.Event) error {
|
||||
return func(events ...*es_models.Event) error {
|
||||
if len(events) == 0 {
|
||||
aggregate.PreviousSequence = 0
|
||||
return nil
|
||||
}
|
||||
if events[0].Type == resevedEventType {
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-eJQqe", "org already reseved")
|
||||
}
|
||||
aggregate.PreviousSequence = events[0].Sequence
|
||||
return nil
|
||||
}
|
||||
}
|
93
internal/org/repository/eventsourcing/org_member.go
Normal file
93
internal/org/repository/eventsourcing/org_member.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
func orgMemberAddedAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, member *OrgMember) (*es_models.Aggregate, error) {
|
||||
if member == nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-c63Ap", "member must not be nil")
|
||||
}
|
||||
|
||||
aggregate, err := aggCreator.NewAggregate(ctx, member.AggregateID, org_model.OrgAggregate, orgVersion, member.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validationQuery := es_models.NewSearchQuery().
|
||||
AggregateTypeFilter("org", "user").
|
||||
AggregateIDsFilter(member.AggregateID, member.UserID)
|
||||
|
||||
validation := addMemberValidation(aggregate, member)
|
||||
|
||||
return aggregate.SetPrecondition(validationQuery, validation).AppendEvent(org_model.OrgMemberAdded, member)
|
||||
}
|
||||
|
||||
func orgMemberChangedAggregate(aggCreator *es_models.AggregateCreator, existingMember *OrgMember, member *OrgMember) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if member == nil || existingMember == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d34fs", "member must not be nil")
|
||||
}
|
||||
|
||||
changes := existingMember.Changes(member)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "EVENT-VLMGn", "nothing changed")
|
||||
}
|
||||
|
||||
agg, err := OrgAggregate(ctx, aggCreator, existingMember.AggregateID, existingMember.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(org_model.OrgMemberChanged, changes)
|
||||
}
|
||||
}
|
||||
|
||||
func orgMemberRemovedAggregate(aggCreator *es_models.AggregateCreator, member *OrgMember) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if member == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dieu7", "member must not be nil")
|
||||
}
|
||||
|
||||
agg, err := OrgAggregate(ctx, aggCreator, member.AggregateID, member.Sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(org_model.OrgMemberRemoved, member)
|
||||
}
|
||||
}
|
||||
|
||||
func addMemberValidation(aggregate *es_models.Aggregate, member *OrgMember) func(...*es_models.Event) error {
|
||||
return func(events ...*es_models.Event) error {
|
||||
existsOrg := false
|
||||
existsUser := false
|
||||
isMember := false
|
||||
for _, event := range events {
|
||||
switch event.AggregateType {
|
||||
case usr_model.UserAggregate:
|
||||
existsUser = true
|
||||
case org_model.OrgAggregate:
|
||||
aggregate.PreviousSequence = event.Sequence
|
||||
existsOrg = true
|
||||
switch event.Type {
|
||||
case org_model.OrgMemberAdded, org_model.OrgMemberRemoved:
|
||||
manipulatedMember, err := OrgMemberFromEvent(new(OrgMember), event)
|
||||
if err != nil {
|
||||
return errors.ThrowInternal(err, "EVENT-Eg8St", "unable to validate object")
|
||||
}
|
||||
if manipulatedMember.UserID == member.UserID {
|
||||
isMember = event.Type == org_model.OrgMemberAdded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if existsOrg && existsUser && !isMember {
|
||||
return nil
|
||||
}
|
||||
return errors.ThrowPreconditionFailed(nil, "EVENT-3OfIm", "conditions not met")
|
||||
}
|
||||
}
|
397
internal/org/repository/eventsourcing/org_member_test.go
Normal file
397
internal/org/repository/eventsourcing/org_member_test.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
)
|
||||
|
||||
func TestOrgMemberAddedAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
isErr func(error) bool
|
||||
eventCount int
|
||||
}
|
||||
type args struct {
|
||||
aggCreator *es_models.AggregateCreator
|
||||
member *OrgMember
|
||||
ctx context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no member",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
member: nil,
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "member added sucessfully",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
member: &OrgMember{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "asdf", Sequence: 234},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: nil,
|
||||
eventCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregate, err := orgMemberAddedAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.member)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && aggregate == nil {
|
||||
t.Error("aggregate must not be nil")
|
||||
}
|
||||
if tt.res.isErr == nil && len(aggregate.Events) != tt.res.eventCount {
|
||||
t.Error("wrong amount of events")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgMemberChangedAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
isErr func(error) bool
|
||||
eventCount int
|
||||
}
|
||||
type args struct {
|
||||
aggCreator *es_models.AggregateCreator
|
||||
existingMember *OrgMember
|
||||
member *OrgMember
|
||||
ctx context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no member",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
member: nil,
|
||||
existingMember: &OrgMember{},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no existing member",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
existingMember: nil,
|
||||
member: &OrgMember{},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
member: &OrgMember{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "asdf", Sequence: 234},
|
||||
},
|
||||
existingMember: &OrgMember{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "asdf", Sequence: 234},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with changes success",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
member: &OrgMember{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "asdf", Sequence: 234},
|
||||
Roles: []string{"asdf"},
|
||||
},
|
||||
existingMember: &OrgMember{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "asdf", Sequence: 234},
|
||||
Roles: []string{"asdf", "woeri"},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: nil,
|
||||
eventCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregateCreator := orgMemberChangedAggregate(tt.args.aggCreator, tt.args.existingMember, tt.args.member)
|
||||
aggregate, err := aggregateCreator(tt.args.ctx)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && aggregate == nil {
|
||||
t.Error("aggregate must not be nil")
|
||||
}
|
||||
if tt.res.isErr == nil && len(aggregate.Events) != tt.res.eventCount {
|
||||
t.Error("wrong amount of events")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgMemberRemovedAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
isErr func(error) bool
|
||||
eventCount int
|
||||
}
|
||||
type args struct {
|
||||
aggCreator *es_models.AggregateCreator
|
||||
member *OrgMember
|
||||
ctx context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no member",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
member: nil,
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "member added sucessfully",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
member: &OrgMember{
|
||||
ObjectRoot: es_models.ObjectRoot{AggregateID: "asdf", Sequence: 234},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: nil,
|
||||
eventCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregateCreator := orgMemberRemovedAggregate(tt.args.aggCreator, tt.args.member)
|
||||
aggregate, err := aggregateCreator(tt.args.ctx)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && aggregate == nil {
|
||||
t.Error("aggregate must not be nil")
|
||||
}
|
||||
if tt.res.isErr == nil && len(aggregate.Events) != tt.res.eventCount {
|
||||
t.Error("wrong amount of events")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addMemberValidation(t *testing.T) {
|
||||
type res struct {
|
||||
isErr func(error) bool
|
||||
preivousSequence uint64
|
||||
}
|
||||
type args struct {
|
||||
aggregate *es_models.Aggregate
|
||||
events []*es_models.Event
|
||||
member *OrgMember
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no events",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only org events",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
events: []*es_models.Event{
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 13,
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 142,
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 1234,
|
||||
Type: org_model.OrgMemberAdded,
|
||||
Data: []byte(`{"userId":"hodor"}`),
|
||||
},
|
||||
},
|
||||
member: &OrgMember{UserID: "hodor"},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only user events",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
events: []*es_models.Event{
|
||||
{
|
||||
AggregateType: usr_model.UserAggregate,
|
||||
Sequence: 13,
|
||||
},
|
||||
{
|
||||
AggregateType: usr_model.UserAggregate,
|
||||
Sequence: 142,
|
||||
},
|
||||
},
|
||||
member: &OrgMember{UserID: "hodor"},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user, org events success",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
events: []*es_models.Event{
|
||||
{
|
||||
AggregateType: usr_model.UserAggregate,
|
||||
Sequence: 13,
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 142,
|
||||
},
|
||||
},
|
||||
member: &OrgMember{UserID: "hodor"},
|
||||
},
|
||||
res: res{
|
||||
isErr: nil,
|
||||
preivousSequence: 142,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user, org and member events success",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
events: []*es_models.Event{
|
||||
{
|
||||
AggregateType: usr_model.UserAggregate,
|
||||
Sequence: 13,
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 142,
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 1234,
|
||||
Type: org_model.OrgMemberAdded,
|
||||
Data: []byte(`{"userId":"hodor"}`),
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 1236,
|
||||
Type: org_model.OrgMemberRemoved,
|
||||
Data: []byte(`{"userId":"hodor"}`),
|
||||
},
|
||||
},
|
||||
member: &OrgMember{UserID: "hodor"},
|
||||
},
|
||||
res: res{
|
||||
isErr: nil,
|
||||
preivousSequence: 1236,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user, org and member added events fail",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
events: []*es_models.Event{
|
||||
{
|
||||
AggregateType: usr_model.UserAggregate,
|
||||
Sequence: 13,
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 142,
|
||||
},
|
||||
{
|
||||
AggregateType: org_model.OrgAggregate,
|
||||
Sequence: 1234,
|
||||
Type: org_model.OrgMemberAdded,
|
||||
Data: []byte(`{"userId":"hodor"}`),
|
||||
},
|
||||
},
|
||||
member: &OrgMember{UserID: "hodor"},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
validaiton := addMemberValidation(tt.args.aggregate, tt.args.member)
|
||||
err := validaiton(tt.args.events...)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && tt.args.aggregate.PreviousSequence != tt.res.preivousSequence {
|
||||
t.Errorf("wrong previous sequence got: %d want %d", tt.args.aggregate.PreviousSequence, tt.res.preivousSequence)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
160
internal/org/repository/eventsourcing/org_model.go
Normal file
160
internal/org/repository/eventsourcing/org_model.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
|
||||
const (
|
||||
orgVersion = "v1"
|
||||
)
|
||||
|
||||
type Org struct {
|
||||
es_models.ObjectRoot `json:"-"`
|
||||
|
||||
Name string `json:"name,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
State int32 `json:"-"`
|
||||
|
||||
Members []*OrgMember `json:"-"`
|
||||
}
|
||||
|
||||
func OrgFromModel(org *org_model.Org) *Org {
|
||||
members := OrgMembersFromModel(org.Members)
|
||||
|
||||
return &Org{
|
||||
ObjectRoot: org.ObjectRoot,
|
||||
Domain: org.Domain,
|
||||
Name: org.Name,
|
||||
State: int32(org.State),
|
||||
Members: members,
|
||||
}
|
||||
}
|
||||
|
||||
func OrgToModel(org *Org) *org_model.Org {
|
||||
return &org_model.Org{
|
||||
ObjectRoot: org.ObjectRoot,
|
||||
Domain: org.Domain,
|
||||
Name: org.Name,
|
||||
State: org_model.OrgState(org.State),
|
||||
Members: OrgMembersToModel(org.Members),
|
||||
}
|
||||
}
|
||||
|
||||
func OrgFromEvents(org *Org, events ...*es_models.Event) (*Org, error) {
|
||||
if org == nil {
|
||||
org = new(Org)
|
||||
}
|
||||
|
||||
return org, org.AppendEvents(events...)
|
||||
}
|
||||
|
||||
func (o *Org) AppendEvents(events ...*es_models.Event) error {
|
||||
for _, event := range events {
|
||||
err := o.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Org) AppendEvent(event *es_models.Event) error {
|
||||
switch event.Type {
|
||||
case org_model.OrgAdded:
|
||||
*o = Org{}
|
||||
err := o.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case org_model.OrgChanged:
|
||||
err := o.setData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case org_model.OrgDeactivated:
|
||||
o.State = int32(org_model.ORGSTATE_INACTIVE)
|
||||
case org_model.OrgReactivated:
|
||||
o.State = int32(org_model.ORGSTATE_ACTIVE)
|
||||
case org_model.OrgMemberAdded:
|
||||
member, err := OrgMemberFromEvent(nil, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
member.CreationDate = event.CreationDate
|
||||
|
||||
o.setMember(member)
|
||||
case org_model.OrgMemberChanged:
|
||||
member, err := OrgMemberFromEvent(nil, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existingMember := o.getMember(member.UserID)
|
||||
member.CreationDate = existingMember.CreationDate
|
||||
|
||||
o.setMember(member)
|
||||
case org_model.OrgMemberRemoved:
|
||||
member, err := OrgMemberFromEvent(nil, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.removeMember(member.UserID)
|
||||
}
|
||||
|
||||
o.ObjectRoot.AppendEvent(event)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Org) setData(event *es_models.Event) error {
|
||||
err := json.Unmarshal(event.Data, o)
|
||||
if err != nil {
|
||||
return errors.ThrowInternal(err, "EVENT-BpbQZ", "unable to unmarshal event")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Org) getMember(userID string) *OrgMember {
|
||||
for _, member := range o.Members {
|
||||
if member.UserID == userID {
|
||||
return member
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Org) setMember(member *OrgMember) {
|
||||
for i, existingMember := range o.Members {
|
||||
if existingMember.UserID == member.UserID {
|
||||
o.Members[i] = member
|
||||
return
|
||||
}
|
||||
}
|
||||
o.Members = append(o.Members, member)
|
||||
}
|
||||
|
||||
func (o *Org) removeMember(userID string) {
|
||||
for i := len(o.Members) - 1; i >= 0; i-- {
|
||||
if o.Members[i].UserID == userID {
|
||||
copy(o.Members[i:], o.Members[i+1:])
|
||||
o.Members[len(o.Members)-1] = nil
|
||||
o.Members = o.Members[:len(o.Members)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Org) Changes(changed *Org) map[string]interface{} {
|
||||
changes := make(map[string]interface{}, 2)
|
||||
|
||||
if changed.Name != "" && changed.Name != o.Name {
|
||||
changes["name"] = changed.Name
|
||||
}
|
||||
if changed.Domain != "" && changed.Domain != o.Domain {
|
||||
changes["domain"] = changed.Domain
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
170
internal/org/repository/eventsourcing/org_model_test.go
Normal file
170
internal/org/repository/eventsourcing/org_model_test.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
|
||||
func TestOrgFromEvents(t *testing.T) {
|
||||
type args struct {
|
||||
event []*es_models.Event
|
||||
org *Org
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Org
|
||||
}{
|
||||
{
|
||||
name: "org from events, ok",
|
||||
args: args{
|
||||
event: []*es_models.Event{
|
||||
{AggregateID: "ID", Sequence: 1, Type: model.OrgAdded},
|
||||
},
|
||||
org: &Org{Name: "OrgName"},
|
||||
},
|
||||
result: &Org{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, State: int32(model.ORGSTATE_ACTIVE), Name: "OrgName"},
|
||||
},
|
||||
{
|
||||
name: "org from events, nil org",
|
||||
args: args{
|
||||
event: []*es_models.Event{
|
||||
{AggregateID: "ID", Sequence: 1, Type: model.OrgAdded},
|
||||
},
|
||||
org: nil,
|
||||
},
|
||||
result: &Org{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, State: int32(model.ORGSTATE_ACTIVE)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.org != nil {
|
||||
data, _ := json.Marshal(tt.args.org)
|
||||
tt.args.event[0].Data = data
|
||||
}
|
||||
result, _ := OrgFromEvents(tt.args.org, tt.args.event...)
|
||||
if result.Name != tt.result.Name {
|
||||
t.Errorf("got wrong result name: expected: %v, actual: %v ", tt.result.Name, result.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendEvent(t *testing.T) {
|
||||
type args struct {
|
||||
event *es_models.Event
|
||||
org *Org
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Org
|
||||
}{
|
||||
{
|
||||
name: "append added event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "ID", Sequence: 1, Type: model.OrgAdded},
|
||||
org: &Org{Name: "OrgName"},
|
||||
},
|
||||
result: &Org{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, State: int32(model.ORGSTATE_ACTIVE), Name: "OrgName"},
|
||||
},
|
||||
{
|
||||
name: "append change event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "ID", Sequence: 1, Type: model.OrgChanged, Data: []byte(`{"domain": "OrgDomain"}`)},
|
||||
org: &Org{Name: "OrgName", Domain: "asdf"},
|
||||
},
|
||||
result: &Org{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, State: int32(model.ORGSTATE_ACTIVE), Name: "OrgName", Domain: "OrgDomain"},
|
||||
},
|
||||
{
|
||||
name: "append deactivate event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "ID", Sequence: 1, Type: model.OrgDeactivated},
|
||||
},
|
||||
result: &Org{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, State: int32(model.ORGSTATE_INACTIVE)},
|
||||
},
|
||||
{
|
||||
name: "append reactivate event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "ID", Sequence: 1, Type: model.OrgReactivated},
|
||||
},
|
||||
result: &Org{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, State: int32(model.ORGSTATE_ACTIVE)},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.org != nil {
|
||||
data, _ := json.Marshal(tt.args.org)
|
||||
tt.args.event.Data = data
|
||||
}
|
||||
result := &Org{}
|
||||
result.AppendEvent(tt.args.event)
|
||||
if result.State != tt.result.State {
|
||||
t.Errorf("got wrong result state: expected: %v, actual: %v ", tt.result.State, result.State)
|
||||
}
|
||||
if result.Name != tt.result.Name {
|
||||
t.Errorf("got wrong result name: expected: %v, actual: %v ", tt.result.Name, result.Name)
|
||||
}
|
||||
if result.ObjectRoot.AggregateID != tt.result.ObjectRoot.AggregateID {
|
||||
t.Errorf("got wrong result id: expected: %v, actual: %v ", tt.result.ObjectRoot.AggregateID, result.ObjectRoot.AggregateID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChanges(t *testing.T) {
|
||||
type args struct {
|
||||
existing *Org
|
||||
new *Org
|
||||
}
|
||||
type res struct {
|
||||
changesLen int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "org name changes",
|
||||
args: args{
|
||||
existing: &Org{Name: "Name"},
|
||||
new: &Org{Name: "NameChanged"},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org domain changes",
|
||||
args: args{
|
||||
existing: &Org{Name: "Name", Domain: "old domain"},
|
||||
new: &Org{Name: "Name", Domain: "new domain"},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes",
|
||||
args: args{
|
||||
existing: &Org{Name: "Name"},
|
||||
new: &Org{Name: "Name"},
|
||||
},
|
||||
res: res{
|
||||
changesLen: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
changes := tt.args.existing.Changes(tt.args.new)
|
||||
if len(changes) != tt.res.changesLen {
|
||||
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
585
internal/org/repository/eventsourcing/org_test.go
Normal file
585
internal/org/repository/eventsourcing/org_test.go
Normal file
@@ -0,0 +1,585 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
org_model "github.com/caos/zitadel/internal/org/model"
|
||||
)
|
||||
|
||||
func Test_isReservedValidation(t *testing.T) {
|
||||
type res struct {
|
||||
isErr func(error) bool
|
||||
agggregateSequence uint64
|
||||
}
|
||||
type args struct {
|
||||
aggregate *es_models.Aggregate
|
||||
eventType es_models.EventType
|
||||
Events []*es_models.Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no events success",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
eventType: "object.reserved",
|
||||
Events: []*es_models.Event{},
|
||||
},
|
||||
res: res{
|
||||
isErr: nil,
|
||||
agggregateSequence: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not reseved success",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
eventType: "object.reserved",
|
||||
Events: []*es_models.Event{
|
||||
{
|
||||
AggregateID: "asdf",
|
||||
AggregateType: "org",
|
||||
Sequence: 45,
|
||||
Type: "object.released",
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: nil,
|
||||
agggregateSequence: 45,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reseved error",
|
||||
args: args{
|
||||
aggregate: &es_models.Aggregate{},
|
||||
eventType: "object.reserved",
|
||||
Events: []*es_models.Event{
|
||||
{
|
||||
AggregateID: "asdf",
|
||||
AggregateType: "org",
|
||||
Sequence: 45,
|
||||
Type: "object.reserved",
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
agggregateSequence: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
validate := isReservedValidation(tt.args.aggregate, tt.args.eventType)
|
||||
|
||||
err := validate(tt.args.Events...)
|
||||
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got: %v", err)
|
||||
}
|
||||
if err == nil && tt.args.aggregate.PreviousSequence != tt.res.agggregateSequence {
|
||||
t.Errorf("expected sequence %d got %d", tt.res.agggregateSequence, tt.args.aggregate.PreviousSequence)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func aggregateWithPrecondition() *es_models.Aggregate {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_uniqueNameAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
expected *es_models.Aggregate
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
aggCreator *es_models.AggregateCreator
|
||||
orgName string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no org name error",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
orgName: "",
|
||||
},
|
||||
res: res{
|
||||
expected: nil,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aggregate created",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
orgName: "asdf",
|
||||
},
|
||||
res: res{
|
||||
expected: aggregateWithPrecondition(),
|
||||
isErr: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := uniqueNameAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.orgName)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && (got.Precondition == nil || got.Precondition.Query == nil || got.Precondition.Validation == nil) {
|
||||
t.Errorf("precondition is not set correctly")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_uniqueDomainAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
expected *es_models.Aggregate
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
aggCreator *es_models.AggregateCreator
|
||||
orgDomain string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no org domain error",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
orgDomain: "",
|
||||
},
|
||||
res: res{
|
||||
expected: nil,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "aggregate created",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
orgDomain: "asdf",
|
||||
},
|
||||
res: res{
|
||||
expected: aggregateWithPrecondition(),
|
||||
isErr: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := uniqueDomainAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.orgDomain)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && (got.Precondition == nil || got.Precondition.Query == nil || got.Precondition.Validation == nil) {
|
||||
t.Errorf("precondition is not set correctly")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgReactivateAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
aggCreator *es_models.AggregateCreator
|
||||
org *Org
|
||||
ctx context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "correct",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
org: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "orgID",
|
||||
Sequence: 2,
|
||||
},
|
||||
State: int32(org_model.ORGSTATE_INACTIVE),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already active error",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
org: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "orgID",
|
||||
Sequence: 2,
|
||||
},
|
||||
State: int32(org_model.ORGSTATE_ACTIVE),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org nil error",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
org: nil,
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregateCreator := orgReactivateAggregate(tt.args.aggCreator, tt.args.org)
|
||||
aggregate, err := aggregateCreator(tt.args.ctx)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && aggregate == nil {
|
||||
t.Error("aggregate must not be nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgDeactivateAggregate(t *testing.T) {
|
||||
type res struct {
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
aggCreator *es_models.AggregateCreator
|
||||
org *Org
|
||||
ctx context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "correct",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
org: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "orgID",
|
||||
Sequence: 2,
|
||||
},
|
||||
State: int32(org_model.ORGSTATE_ACTIVE),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "already inactive error",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
org: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "orgID",
|
||||
Sequence: 2,
|
||||
},
|
||||
State: int32(org_model.ORGSTATE_INACTIVE),
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsErrorInvalidArgument,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org nil error",
|
||||
args: args{
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
org: nil,
|
||||
},
|
||||
res: res{
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aggregateCreator := orgDeactivateAggregate(tt.args.aggCreator, tt.args.org)
|
||||
aggregate, err := aggregateCreator(tt.args.ctx)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && aggregate == nil {
|
||||
t.Error("aggregate must not be nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgUpdateAggregates(t *testing.T) {
|
||||
type res struct {
|
||||
aggregateCount int
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
aggCreator *es_models.AggregateCreator
|
||||
existing *Org
|
||||
updated *Org
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no existing org error",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
existing: nil,
|
||||
updated: &Org{},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 0,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no updated org error",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
existing: &Org{},
|
||||
updated: nil,
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 0,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no changes",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
existing: &Org{},
|
||||
updated: &Org{},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 0,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "name changed",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
existing: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "sdaf",
|
||||
Sequence: 5,
|
||||
},
|
||||
Domain: "caos.ch",
|
||||
Name: "coas",
|
||||
},
|
||||
updated: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "sdaf",
|
||||
Sequence: 5,
|
||||
},
|
||||
Domain: "caos.ch",
|
||||
Name: "caos",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 2,
|
||||
isErr: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "domain changed",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
existing: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "sdaf",
|
||||
Sequence: 5,
|
||||
},
|
||||
Domain: "caos.swiss",
|
||||
Name: "caos",
|
||||
},
|
||||
updated: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "sdaf",
|
||||
Sequence: 5,
|
||||
},
|
||||
Domain: "caos.ch",
|
||||
Name: "caos",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 2,
|
||||
isErr: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := OrgUpdateAggregates(tt.args.ctx, tt.args.aggCreator, tt.args.existing, tt.args.updated)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got: %v", err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && len(got) != tt.res.aggregateCount {
|
||||
t.Errorf("OrgUpdateAggregates() aggregate count = %d, wanted count %d", len(got), tt.res.aggregateCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrgCreatedAggregates(t *testing.T) {
|
||||
type res struct {
|
||||
aggregateCount int
|
||||
isErr func(error) bool
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
aggCreator *es_models.AggregateCreator
|
||||
org *Org
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "no org error",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
org: nil,
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 0,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org successful",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
org: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "sdaf",
|
||||
Sequence: 5,
|
||||
},
|
||||
Domain: "caos.ch",
|
||||
Name: "caos",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 3,
|
||||
isErr: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no domain error",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
org: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "sdaf",
|
||||
Sequence: 5,
|
||||
},
|
||||
Name: "caos",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 2,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no name error",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("org", "user"),
|
||||
aggCreator: es_models.NewAggregateCreator("test"),
|
||||
org: &Org{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: "sdaf",
|
||||
Sequence: 5,
|
||||
},
|
||||
Domain: "caos.ch",
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
aggregateCount: 2,
|
||||
isErr: errors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := orgCreatedAggregates(tt.args.ctx, tt.args.aggCreator, tt.args.org)
|
||||
if tt.res.isErr == nil && err != nil {
|
||||
t.Errorf("no error expected got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr != nil && !tt.res.isErr(err) {
|
||||
t.Errorf("wrong error got %T: %v", err, err)
|
||||
}
|
||||
if tt.res.isErr == nil && len(got) != tt.res.aggregateCount {
|
||||
t.Errorf("OrgUpdateAggregates() aggregate count = %d, wanted count %d", len(got), tt.res.aggregateCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
10
internal/org/repository/model.go
Normal file
10
internal/org/repository/model.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package repository
|
||||
|
||||
import es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
|
||||
type Org struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Name string
|
||||
Domain string
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -92,26 +93,26 @@ func (u *User) HashPasswordIfExisting(passwordAlg crypto.HashAlgorithm, onetime
|
||||
}
|
||||
|
||||
func (u *User) GenerateInitCodeIfNeeded(initGenerator crypto.Generator) error {
|
||||
u.InitCode = new(InitUserCode)
|
||||
if !u.IsInitialState() {
|
||||
return nil
|
||||
}
|
||||
u.InitCode = new(InitUserCode)
|
||||
return u.InitCode.GenerateInitUserCode(initGenerator)
|
||||
}
|
||||
|
||||
func (u *User) GeneratePhoneCodeIfNeeded(phoneGenerator crypto.Generator) error {
|
||||
u.PhoneCode = new(PhoneCode)
|
||||
if u.Phone == nil || u.IsPhoneVerified {
|
||||
return nil
|
||||
}
|
||||
u.PhoneCode = new(PhoneCode)
|
||||
return u.PhoneCode.GeneratePhoneCode(phoneGenerator)
|
||||
}
|
||||
|
||||
func (u *User) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) error {
|
||||
u.EmailCode = new(EmailCode)
|
||||
if u.Email == nil || u.IsEmailVerified {
|
||||
return nil
|
||||
}
|
||||
u.EmailCode = new(EmailCode)
|
||||
return u.EmailCode.GenerateEmailCode(emailGenerator)
|
||||
}
|
||||
|
||||
|
@@ -2,18 +2,20 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/caos/zitadel/internal/cache/config"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_int "github.com/caos/zitadel/internal/eventstore"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
|
||||
global_model "github.com/caos/zitadel/internal/model"
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/sony/sonyflake"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type UserEventstore struct {
|
||||
@@ -49,6 +51,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto
|
||||
phoneVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PhoneVerificationCode, aesCrypto)
|
||||
passwordVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PasswordVerificationCode, aesCrypto)
|
||||
aesOtpCrypto, err := crypto.NewAESCrypto(systemDefaults.Multifactors.OTP.VerificationKey)
|
||||
passwordAlg := crypto.NewBCrypt(systemDefaults.SecretGenerators.PasswordSaltCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -67,6 +70,7 @@ func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventsto
|
||||
PhoneVerificationCode: phoneVerificationCode,
|
||||
PasswordVerificationCode: passwordVerificationCode,
|
||||
Multifactors: mfa,
|
||||
PasswordAlg: passwordAlg,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -85,37 +89,47 @@ func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.U
|
||||
return model.UserToModel(user), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) {
|
||||
func (es *UserEventstore) PrepareCreateUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*model.User, *es_models.Aggregate, error) {
|
||||
user.SetEmailAsUsername()
|
||||
if !user.IsValid() {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "Name is required")
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "Name is required")
|
||||
}
|
||||
//TODO: Check Uniqueness
|
||||
id, err := es.idGenerator.NextID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
user.AggregateID = strconv.FormatUint(id, 10)
|
||||
|
||||
err = user.HashPasswordIfExisting(es.PasswordAlg, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
err = user.GenerateInitCodeIfNeeded(es.InitializeUserCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
err = user.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoInitCode := model.InitCodeFromModel(user.InitCode)
|
||||
repoPhoneCode := model.PhoneCodeFromModel(user.PhoneCode)
|
||||
|
||||
createAggregate := UserCreateAggregate(es.AggregateCreator(), repoUser, repoInitCode, repoPhoneCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregate)
|
||||
createAggregate, err := UserCreateAggregate(ctx, es.AggregateCreator(), repoUser, repoInitCode, repoPhoneCode, resourceOwner)
|
||||
|
||||
return repoUser, createAggregate, err
|
||||
}
|
||||
|
||||
func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) {
|
||||
repoUser, aggregate, err := es.PrepareCreateUser(ctx, user, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, aggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -124,32 +138,41 @@ func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User)
|
||||
return model.UserToModel(repoUser), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) {
|
||||
func (es *UserEventstore) PrepareRegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*model.User, *es_models.Aggregate, error) {
|
||||
user.SetEmailAsUsername()
|
||||
if !user.IsValid() || user.Password == nil || user.SecretString == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "user is invalid")
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "user is invalid")
|
||||
}
|
||||
//TODO: Check Uniqueness
|
||||
id, err := es.idGenerator.NextID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
user.AggregateID = strconv.FormatUint(id, 10)
|
||||
|
||||
err = user.HashPasswordIfExisting(es.PasswordAlg, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
err = user.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
repoEmailCode := model.EmailCodeFromModel(user.EmailCode)
|
||||
|
||||
createAggregate := UserRegisterAggregate(es.AggregateCreator(), repoUser, resourceOwner, repoEmailCode)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregate)
|
||||
aggregate, err := UserRegisterAggregate(ctx, es.AggregateCreator(), repoUser, resourceOwner, repoEmailCode)
|
||||
return repoUser, aggregate, err
|
||||
}
|
||||
|
||||
func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) {
|
||||
repoUser, createAggregate, err := es.PrepareRegisterUser(ctx, user, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = es_sdk.PushAggregates(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -2,12 +2,13 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,11 +23,11 @@ type User struct {
|
||||
*Email
|
||||
*Phone
|
||||
*Address
|
||||
InitCode *InitUserCode
|
||||
EmailCode *EmailCode
|
||||
PhoneCode *PhoneCode
|
||||
PasswordCode *PasswordCode
|
||||
OTP *OTP
|
||||
InitCode *InitUserCode `json:"-"`
|
||||
EmailCode *EmailCode `json:"-"`
|
||||
PhoneCode *PhoneCode `json:"-"`
|
||||
PasswordCode *PasswordCode `json:"-"`
|
||||
OTP *OTP `json:"-"`
|
||||
}
|
||||
|
||||
type InitUserCode struct {
|
||||
|
@@ -2,6 +2,7 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
@@ -36,72 +37,72 @@ func UserAggregateOverwriteContext(ctx context.Context, aggCreator *es_models.Ag
|
||||
return aggCreator.NewAggregate(ctx, user.AggregateID, model.UserAggregate, model.UserVersion, user.Sequence, es_models.OverwriteResourceOwner(resourceOwnerID), es_models.OverwriteEditorUser(userID))
|
||||
}
|
||||
|
||||
func UserCreateAggregate(aggCreator *es_models.AggregateCreator, user *model.User, initCode *model.InitUserCode, phoneCode *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if user == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user should not be nil")
|
||||
}
|
||||
|
||||
agg, err := UserAggregate(ctx, aggCreator, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agg, err = agg.AppendEvent(model.UserAdded, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Email != nil && user.EmailAddress != "" && user.IsEmailVerified {
|
||||
agg, err = agg.AppendEvent(model.UserEmailVerified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Phone != nil && user.PhoneNumber != "" && user.IsPhoneVerified {
|
||||
agg, err = agg.AppendEvent(model.UserPhoneVerified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Password != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPasswordCodeAdded, user.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if initCode != nil {
|
||||
agg, err = agg.AppendEvent(model.InitializedUserCodeAdded, initCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if phoneCode != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPhoneCodeAdded, phoneCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return agg, err
|
||||
func UserCreateAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, initCode *model.InitUserCode, phoneCode *model.PhoneCode, resourceOwner string) (agg *es_models.Aggregate, err error) {
|
||||
if user == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user should not be nil")
|
||||
}
|
||||
|
||||
if resourceOwner != "" {
|
||||
agg, err = UserAggregateOverwriteContext(ctx, aggCreator, user, user.AggregateID, resourceOwner)
|
||||
} else {
|
||||
agg, err = UserAggregate(ctx, aggCreator, user)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agg, err = agg.AppendEvent(model.UserAdded, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Email != nil && user.EmailAddress != "" && user.IsEmailVerified {
|
||||
agg, err = agg.AppendEvent(model.UserEmailVerified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Phone != nil && user.PhoneNumber != "" && user.IsPhoneVerified {
|
||||
agg, err = agg.AppendEvent(model.UserPhoneVerified, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Password != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPasswordCodeAdded, user.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if initCode != nil {
|
||||
agg, err = agg.AppendEvent(model.InitializedUserCodeAdded, initCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if phoneCode != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPhoneCodeAdded, phoneCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return agg, err
|
||||
}
|
||||
|
||||
func UserRegisterAggregate(aggCreator *es_models.AggregateCreator, user *model.User, resourceOwner string, emailCode *model.EmailCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if user == nil || resourceOwner == "" || emailCode == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user, resourceowner, emailcode should not be nothing")
|
||||
}
|
||||
|
||||
agg, err := UserAggregateOverwriteContext(ctx, aggCreator, user, resourceOwner, user.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agg, err = agg.AppendEvent(model.UserRegistered, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserEmailCodeAdded, emailCode)
|
||||
func UserRegisterAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, resourceOwner string, emailCode *model.EmailCode) (*es_models.Aggregate, error) {
|
||||
if user == nil || resourceOwner == "" || emailCode == nil {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user, resourceowner, emailcode should not be nothing")
|
||||
}
|
||||
|
||||
agg, err := UserAggregateOverwriteContext(ctx, aggCreator, user, resourceOwner, user.AggregateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agg, err = agg.AppendEvent(model.UserRegistered, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserEmailCodeAdded, emailCode)
|
||||
}
|
||||
|
||||
func UserDeactivateAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
|
@@ -2,12 +2,13 @@ package eventsourcing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUserByIDQuery(t *testing.T) {
|
||||
@@ -212,7 +213,7 @@ func TestUserCreateAggregate(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := UserCreateAggregate(tt.args.aggCreator, tt.args.new, tt.args.initCode, tt.args.phoneCode)(tt.args.ctx)
|
||||
agg, err := UserCreateAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.new, tt.args.initCode, tt.args.phoneCode, "")
|
||||
|
||||
if !tt.res.wantErr && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
@@ -329,7 +330,7 @@ func TestUserRegisterAggregate(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := UserRegisterAggregate(tt.args.aggCreator, tt.args.new, tt.args.resourceOwner, tt.args.emailCode)(tt.args.ctx)
|
||||
agg, err := UserRegisterAggregate(tt.args.ctx, tt.args.aggCreator, tt.args.new, tt.args.resourceOwner, tt.args.emailCode)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
|
Reference in New Issue
Block a user