fix: v2 human command (#3435)

* add/register human command done

* validations

* crypto

* move clientid

* keys

* fix: clientID

* remove v2 package

* tests

* tests running

* revert old code

* instance domain from ctx

* chore: rename zitadel app ids

* comments

* fix: test
This commit is contained in:
Silvan 2022-04-12 16:20:17 +02:00 committed by GitHub
parent 4a0d61d75a
commit cea2567e22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 3524 additions and 2832 deletions

View File

@ -12,14 +12,14 @@ CREATE TABLE eventstore.events (
, editor_user TEXT NOT NULL
, editor_service TEXT NOT NULL
, resource_owner TEXT NOT NULL
, instance_id TEXT
, instance_id TEXT NOT NULL
, PRIMARY KEY (event_sequence DESC) USING HASH WITH BUCKET_COUNT = 10
, INDEX agg_type_agg_id (aggregate_type, aggregate_id)
, INDEX agg_type (aggregate_type)
, INDEX agg_type_seq (aggregate_type, event_sequence DESC)
STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, instance_id, previous_aggregate_type_sequence)
, INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC)
, CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC)
, CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence)
, PRIMARY KEY (event_sequence DESC, instance_id) USING HASH WITH BUCKET_COUNT = 10
, INDEX agg_type_agg_id (aggregate_type, aggregate_id, instance_id)
, INDEX agg_type (aggregate_type, instance_id)
, INDEX agg_type_seq (aggregate_type, event_sequence DESC, instance_id)
STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, previous_aggregate_type_sequence)
, INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC, instance_id)
, CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC, instance_id)
, CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence, instance_id)
)

View File

@ -3,7 +3,6 @@ package setup
import (
"context"
"database/sql"
_ "embed"
)
const (

View File

@ -2,21 +2,62 @@ package setup
import (
"context"
"database/sql"
"fmt"
"github.com/caos/zitadel/internal/command/v2"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
crypto_db "github.com/caos/zitadel/internal/crypto/database"
"github.com/caos/zitadel/internal/eventstore"
)
type DefaultInstance struct {
cmd *command.Command
InstanceSetup command.InstanceSetup
userEncryptionKey *crypto.KeyConfig
masterKey string
db *sql.DB
es *eventstore.Eventstore
domain string
defaults systemdefaults.SystemDefaults
zitadelRoles []authz.RoleMapping
}
func (mig *DefaultInstance) Execute(ctx context.Context) error {
_, err := mig.cmd.SetUpInstance(ctx, &mig.InstanceSetup)
keyStorage, err := crypto_db.NewKeyStorage(mig.db, mig.masterKey)
if err != nil {
return fmt.Errorf("cannot start key storage: %w", err)
}
if err = verifyKey(mig.userEncryptionKey, keyStorage); err != nil {
return err
}
userAlg, err := crypto.NewAESCrypto(mig.userEncryptionKey, keyStorage)
if err != nil {
return err
}
cmd := command.NewCommandV2(mig.es, mig.defaults, userAlg, mig.zitadelRoles)
ctx = authz.WithRequestedDomain(ctx, mig.domain)
_, err = cmd.SetUpInstance(ctx, &mig.InstanceSetup)
return err
}
func (mig *DefaultInstance) String() string {
return "03_default_instance"
}
func verifyKey(key *crypto.KeyConfig, storage crypto.KeyStorage) (err error) {
_, err = crypto.LoadKey(key.EncryptionKeyID, storage)
if err == nil {
return nil
}
k, err := crypto.NewKey(key.EncryptionKeyID)
if err != nil {
return err
}
return storage.CreateKeys(k)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/config/hook"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/database"
)
@ -21,6 +22,7 @@ type Config struct {
ExternalDomain string
ExternalSecure bool
Log *logging.Config
EncryptionKeys *encryptionKeyConfig
}
func MustNewConfig(v *viper.Viper) *Config {
@ -40,6 +42,10 @@ type Steps struct {
S3DefaultInstance *DefaultInstance
}
type encryptionKeyConfig struct {
User *crypto.KeyConfig
}
func MustNewSteps(v *viper.Viper) *Steps {
v.SetConfigType("yaml")
err := v.ReadConfig(bytes.NewBuffer(defaultSteps))

View File

@ -8,8 +8,8 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/caos/zitadel/cmd/admin/key"
http_util "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/command/v2"
"github.com/caos/zitadel/internal/database"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/migration"
@ -31,12 +31,15 @@ Requirements:
config := MustNewConfig(viper.GetViper())
steps := MustNewSteps(viper.New())
Setup(config, steps)
masterKey, err := key.MasterKey(cmd)
logging.OnError(err).Panic("No master key provided")
Setup(config, steps, masterKey)
},
}
}
func Setup(config *Config, steps *Steps) {
func Setup(config *Config, steps *Steps, masterKey string) {
dbClient, err := database.Connect(config.Database)
logging.OnError(err).Fatal("unable to connect to database")
@ -44,11 +47,17 @@ func Setup(config *Config, steps *Steps) {
logging.OnError(err).Fatal("unable to start eventstore")
migration.RegisterMappers(eventstoreClient)
cmd := command.New(eventstoreClient, "localhost", config.SystemDefaults)
steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient}
steps.s2AssetsTable = &AssetTable{dbClient: dbClient}
steps.S3DefaultInstance.cmd = cmd
steps.S3DefaultInstance.es = eventstoreClient
steps.S3DefaultInstance.db = dbClient
steps.S3DefaultInstance.defaults = config.SystemDefaults
steps.S3DefaultInstance.masterKey = masterKey
steps.S3DefaultInstance.domain = config.SystemDefaults.Domain
steps.S3DefaultInstance.zitadelRoles = config.InternalAuthZ.RolePermissionMappings
steps.S3DefaultInstance.userEncryptionKey = config.EncryptionKeys.User
steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure
steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure
steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)

View File

@ -8,11 +8,64 @@ S3DefaultInstance:
LastName: Admin
NickName:
DisplayName:
Email: admin@zitadel.ch
Email:
Address: admin@zitadel.ch
Verified: true
PreferredLanguage:
Gender:
Phone:
Number:
Verified:
Password: Password1!
SecretGenerators:
PasswordSaltCost: 14
ClientSecret:
Length: 64
IncludeLowerLetters: true
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
InitializeUserCode:
Length: 6
Expiry: '72h'
IncludeLowerLetters: false
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
EmailVerificationCode:
Length: 6
Expiry: '1h'
IncludeLowerLetters: false
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
PhoneVerificationCode:
Length: 6
Expiry: '1h'
IncludeLowerLetters: false
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
PasswordVerificationCode:
Length: 6
Expiry: '1h'
IncludeLowerLetters: false
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
PasswordlessInitCode:
Length: 12
Expiry: '1h'
IncludeLowerLetters: true
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
DomainVerification:
Length: 32
IncludeLowerLetters: true
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: false
Features:
TierName: Default Tier
TierDescription: ""

View File

@ -32,26 +32,20 @@ type encryptionKeys struct {
OIDCKey []byte
}
func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyStorage) (*encryptionKeys, error) {
keys, err := keyStorage.ReadKeys()
func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyStorage) (keys *encryptionKeys, err error) {
if err := verifyDefaultKeys(keyStorage); err != nil {
return nil, err
}
keys = new(encryptionKeys)
keys.DomainVerification, err = crypto.NewAESCrypto(keyConfig.DomainVerification, keyStorage)
if err != nil {
return nil, err
}
if len(keys) == 0 {
if err := createDefaultKeys(keyStorage); err != nil {
return nil, err
}
}
encryptionKeys := new(encryptionKeys)
encryptionKeys.DomainVerification, err = crypto.NewAESCrypto(keyConfig.DomainVerification, keyStorage)
keys.IDPConfig, err = crypto.NewAESCrypto(keyConfig.IDPConfig, keyStorage)
if err != nil {
return nil, err
}
encryptionKeys.IDPConfig, err = crypto.NewAESCrypto(keyConfig.IDPConfig, keyStorage)
if err != nil {
return nil, err
}
encryptionKeys.OIDC, err = crypto.NewAESCrypto(keyConfig.OIDC, keyStorage)
keys.OIDC, err = crypto.NewAESCrypto(keyConfig.OIDC, keyStorage)
if err != nil {
return nil, err
}
@ -59,20 +53,20 @@ func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyS
if err != nil {
return nil, err
}
encryptionKeys.OIDCKey = []byte(key)
encryptionKeys.OTP, err = crypto.NewAESCrypto(keyConfig.OTP, keyStorage)
keys.OIDCKey = []byte(key)
keys.OTP, err = crypto.NewAESCrypto(keyConfig.OTP, keyStorage)
if err != nil {
return nil, err
}
encryptionKeys.SMS, err = crypto.NewAESCrypto(keyConfig.SMS, keyStorage)
keys.SMS, err = crypto.NewAESCrypto(keyConfig.SMS, keyStorage)
if err != nil {
return nil, err
}
encryptionKeys.SMTP, err = crypto.NewAESCrypto(keyConfig.SMTP, keyStorage)
keys.SMTP, err = crypto.NewAESCrypto(keyConfig.SMTP, keyStorage)
if err != nil {
return nil, err
}
encryptionKeys.User, err = crypto.NewAESCrypto(keyConfig.User, keyStorage)
keys.User, err = crypto.NewAESCrypto(keyConfig.User, keyStorage)
if err != nil {
return nil, err
}
@ -80,23 +74,30 @@ func ensureEncryptionKeys(keyConfig *encryptionKeyConfig, keyStorage crypto.KeyS
if err != nil {
return nil, err
}
encryptionKeys.CSRFCookieKey = []byte(key)
keys.CSRFCookieKey = []byte(key)
key, err = crypto.LoadKey(keyConfig.UserAgentCookieKeyID, keyStorage)
if err != nil {
return nil, err
}
encryptionKeys.UserAgentCookieKey = []byte(key)
return encryptionKeys, nil
keys.UserAgentCookieKey = []byte(key)
return keys, nil
}
func createDefaultKeys(keyStorage crypto.KeyStorage) error {
keys := make([]*crypto.Key, len(defaultKeyIDs))
for i, keyID := range defaultKeyIDs {
func verifyDefaultKeys(keyStorage crypto.KeyStorage) (err error) {
keys := make([]*crypto.Key, 0, len(defaultKeyIDs))
for _, keyID := range defaultKeyIDs {
_, err := crypto.LoadKey(keyID, keyStorage)
if err == nil {
continue
}
key, err := crypto.NewKey(keyID)
if err != nil {
return err
}
keys[i] = key
keys = append(keys, key)
}
if len(keys) == 0 {
return nil
}
if err := keyStorage.CreateKeys(keys...); err != nil {
return caos_errs.ThrowInternal(err, "START-aGBq2", "cannot create default keys")

View File

@ -21,7 +21,6 @@ import (
"golang.org/x/net/http2/h2c"
"github.com/caos/zitadel/cmd/admin/key"
admin_es "github.com/caos/zitadel/internal/admin/repository/eventsourcing"
"github.com/caos/zitadel/internal/api"
"github.com/caos/zitadel/internal/api/assets"
@ -118,7 +117,7 @@ func startZitadel(config *Config, masterKey string) error {
Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure),
DisplayName: "ZITADEL",
}
commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ, storage, authZRepo, webAuthNConfig, keys.IDPConfig, keys.OTP, keys.SMTP, keys.SMS, keys.DomainVerification, keys.OIDC)
commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ, storage, authZRepo, webAuthNConfig, keys.IDPConfig, keys.OTP, keys.SMTP, keys.SMS, keys.User, keys.DomainVerification, keys.OIDC)
if err != nil {
return fmt.Errorf("cannot start commands: %w", err)
}

View File

@ -3,6 +3,7 @@ package start
import (
"github.com/caos/logging"
"github.com/caos/zitadel/cmd/admin/initialise"
"github.com/caos/zitadel/cmd/admin/key"
"github.com/caos/zitadel/cmd/admin/setup"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -20,16 +21,18 @@ Last ZITADEL starts.
Requirements:
- cockroachdb`,
Run: func(cmd *cobra.Command, args []string) {
masterKey, err := key.MasterKey(cmd)
logging.OnError(err).Panic("No master key provided")
initialise.InitAll(initialise.MustNewConfig(viper.GetViper()))
setupConfig := setup.MustNewConfig(viper.GetViper())
setupSteps := setup.MustNewSteps(viper.New())
setup.Setup(setupConfig, setupSteps)
setup.Setup(setupConfig, setupSteps, masterKey)
startConfig := MustNewConfig(viper.GetViper())
startMasterKey, _ := cmd.Flags().GetString(flagMasterKey)
err := startZitadel(startConfig, startMasterKey)
err = startZitadel(startConfig, masterKey)
logging.OnError(err).Fatal("unable to start zitadel")
},
}

View File

@ -12,6 +12,7 @@ type Instance interface {
InstanceID() string
ProjectID() string
ConsoleClientID() string
RequestedDomain() string
}
type InstanceVerifier interface {
@ -20,6 +21,7 @@ type InstanceVerifier interface {
type instance struct {
ID string
Domain string
}
func (i *instance) InstanceID() string {
@ -34,6 +36,10 @@ func (i *instance) ConsoleClientID() string {
return ""
}
func (i *instance) RequestedDomain() string {
return i.Domain
}
func GetInstance(ctx context.Context) Instance {
instance, ok := ctx.Value(instanceKey).(Instance)
if !ok {
@ -49,3 +55,13 @@ func WithInstance(ctx context.Context, instance Instance) context.Context {
func WithInstanceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, instanceKey, &instance{ID: id})
}
func WithRequestedDomain(ctx context.Context, domain string) context.Context {
i, ok := ctx.Value(instanceKey).(*instance)
if !ok {
i = new(instance)
}
i.Domain = domain
return context.WithValue(ctx, instanceKey, i)
}

View File

@ -78,3 +78,7 @@ func (m *mockInstance) ProjectID() string {
func (m *mockInstance) ConsoleClientID() string {
return "consoleID"
}
func (m *mockInstance) RequestedDomain() string {
return "zitadel.cloud"
}

View File

@ -28,7 +28,8 @@ type Server struct {
userCodeAlg crypto.EncryptionAlgorithm
}
func CreateServer(command *command.Commands,
func CreateServer(
command *command.Commands,
query *query.Queries,
sd systemdefaults.SystemDefaults,
assetAPIPrefix string,

View File

@ -4,7 +4,9 @@ import (
"context"
"time"
"github.com/caos/logging"
"github.com/caos/oidc/pkg/oidc"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/caos/zitadel/internal/api/authz"
@ -13,9 +15,9 @@ import (
idp_grpc "github.com/caos/zitadel/internal/api/grpc/idp"
"github.com/caos/zitadel/internal/api/grpc/metadata"
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/api/grpc/user"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
z_oidc "github.com/caos/zitadel/internal/api/oidc"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/query"
mgmt_pb "github.com/caos/zitadel/pkg/grpc/management"
@ -192,24 +194,40 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
}
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
initCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, s.userCodeAlg)
if err != nil {
return nil, err
}
phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.userCodeAlg)
if err != nil {
return nil, err
}
human, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToDomain(req), initCodeGenerator, phoneCodeGenerator)
lang, err := language.Parse(req.Profile.PreferredLanguage)
logging.OnError(err).Debug("unable to parse language")
details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, &command.AddHuman{
Username: req.UserName,
FirstName: req.Profile.FirstName,
LastName: req.Profile.LastName,
NickName: req.Profile.NickName,
DisplayName: req.Profile.DisplayName,
Email: command.Email{
Address: req.Email.Email,
Verified: req.Email.IsEmailVerified,
},
PreferredLang: lang,
Gender: user_grpc.GenderToDomain(req.Profile.Gender),
Phone: command.Phone{
Number: req.Phone.Phone,
Verified: req.Phone.IsPhoneVerified,
},
Password: req.InitialPassword,
PasswordChangeRequired: true,
Passwordless: false,
Register: false,
ExternalIDP: false,
})
if err != nil {
return nil, err
}
return &mgmt_pb.AddHumanUserResponse{
UserId: human.AggregateID,
UserId: details.ID,
Details: obj_grpc.AddToDetailsPb(
human.Sequence,
human.ChangeDate,
human.ResourceOwner,
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}
@ -763,7 +781,7 @@ func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.G
return nil, err
}
return &mgmt_pb.GetPersonalAccessTokenByIDsResponse{
Token: user.PersonalAccessTokenToPb(token),
Token: user_grpc.PersonalAccessTokenToPb(token),
}, nil
}

View File

@ -172,3 +172,7 @@ func (m *mockInstance) ProjectID() string {
func (m *mockInstance) ConsoleClientID() string {
return "consoleClientID"
}
func (m *mockInstance) RequestedDomain() string {
return "localhost"
}

View File

@ -256,3 +256,7 @@ func (m *mockInstance) ProjectID() string {
func (m *mockInstance) ConsoleClientID() string {
return "consoleClientID"
}
func (m *mockInstance) RequestedDomain() string {
return "zitadel.cloud"
}

View File

@ -48,6 +48,18 @@ type Commands struct {
privateKeyLifetime time.Duration
publicKeyLifetime time.Duration
tokenVerifier orgFeatureChecker
v2 *commandNew
}
type commandNew struct {
es *eventstore.Eventstore
userPasswordAlg crypto.HashAlgorithm
phoneAlg crypto.EncryptionAlgorithm
emailAlg crypto.EncryptionAlgorithm
initCodeAlg crypto.EncryptionAlgorithm
zitadelRoles []authz.RoleMapping
id id.Generator
}
type orgFeatureChecker interface {
@ -64,6 +76,7 @@ func StartCommands(es *eventstore.Eventstore,
otpEncryption,
smtpEncryption,
smsEncryption,
userEncryption,
domainVerificationEncryption,
oidcEncryption crypto.EncryptionAlgorithm,
) (repo *Commands, err error) {
@ -81,7 +94,9 @@ func StartCommands(es *eventstore.Eventstore,
smsCrypto: smsEncryption,
domainVerificationAlg: domainVerificationEncryption,
keyAlgorithm: oidcEncryption,
v2: NewCommandV2(es, defaults, userEncryption, authZConfig.RolePermissionMappings),
}
instance_repo.RegisterEventMappers(repo.eventstore)
org.RegisterEventMappers(repo.eventstore)
usr_repo.RegisterEventMappers(repo.eventstore)
@ -113,6 +128,31 @@ func StartCommands(es *eventstore.Eventstore,
return repo, nil
}
func NewCommandV2(
es *eventstore.Eventstore,
defaults sd.SystemDefaults,
userAlg crypto.EncryptionAlgorithm,
zitadelRoles []authz.RoleMapping,
) *commandNew {
instance_repo.RegisterEventMappers(es)
org.RegisterEventMappers(es)
usr_repo.RegisterEventMappers(es)
usr_grant_repo.RegisterEventMappers(es)
proj_repo.RegisterEventMappers(es)
keypair.RegisterEventMappers(es)
action.RegisterEventMappers(es)
return &commandNew{
es: es,
userPasswordAlg: crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost),
initCodeAlg: userAlg,
phoneAlg: userAlg,
emailAlg: userAlg,
zitadelRoles: zitadelRoles,
id: id.SonyFlakeGenerator,
}
}
func AppendAndReduce(object interface {
AppendEvents(...eventstore.Event)
Reduce() error

View File

@ -0,0 +1,67 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
)
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) {
config, err := secretGeneratorConfig(ctx, filter, typ)
if err != nil {
return nil, -1, err
}
switch a := alg.(type) {
case crypto.HashAlgorithm:
value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
case crypto.EncryptionAlgorithm:
value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
default:
return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
}
if err != nil {
return nil, -1, err
}
return value, config.Expiry, nil
}
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
config, err := secretGeneratorConfig(ctx, filter, typ)
if err != nil {
return nil, "", err
}
switch a := alg.(type) {
case crypto.HashAlgorithm:
return crypto.NewCode(crypto.NewHashGenerator(*config, a))
case crypto.EncryptionAlgorithm:
return crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
}
return nil, "", errors.ThrowInvalidArgument(nil, "V2-NGESt", "Errors.Internal")
}
func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType) (*crypto.GeneratorConfig, error) {
wm := NewInstanceSecretGeneratorConfigWriteModel(ctx, typ)
events, err := filter(ctx, wm.Query())
if err != nil {
return nil, err
}
wm.AppendEvents(events...)
if err := wm.Reduce(); err != nil {
return nil, err
}
return &crypto.GeneratorConfig{
Length: wm.Length,
Expiry: wm.Expiry,
IncludeLowerLetters: wm.IncludeLowerLetters,
IncludeUpperLetters: wm.IncludeUpperLetters,
IncludeDigits: wm.IncludeDigits,
IncludeSymbols: wm.IncludeSymbols,
}, nil
}

23
internal/command/email.go Normal file
View File

@ -0,0 +1,23 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
)
type Email struct {
Address string
Verified bool
}
func (e *Email) Valid() bool {
return e.Address != "" && domain.EmailRegex.MatchString(e.Address)
}
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
}

View File

@ -6,8 +6,10 @@ import (
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/ui/console"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/repository/instance"
@ -53,6 +55,16 @@ type InstanceSetup struct {
ActionsAllowed domain.ActionsAllowed
MaxActions int
}
SecretGenerators struct {
PasswordSaltCost uint
ClientSecret *crypto.GeneratorConfig
InitializeUserCode *crypto.GeneratorConfig
EmailVerificationCode *crypto.GeneratorConfig
PhoneVerificationCode *crypto.GeneratorConfig
PasswordVerificationCode *crypto.GeneratorConfig
PasswordlessInitCode *crypto.GeneratorConfig
DomainVerification *crypto.GeneratorConfig
}
PasswordComplexityPolicy struct {
MinLength uint64
HasLowercase bool
@ -111,14 +123,10 @@ type ZitadelConfig struct {
BaseURL string
projectID string
mgmtID string
mgmtClientID string
adminID string
adminClientID string
authID string
authClientID string
consoleID string
consoleClientID string
mgmtAppID string
adminAppID string
authAppID string
consoleAppID string
}
func (s *InstanceSetup) generateIDs() (err error) {
@ -127,50 +135,35 @@ func (s *InstanceSetup) generateIDs() (err error) {
return err
}
s.Zitadel.mgmtID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.mgmtClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
s.Zitadel.mgmtAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.adminID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.adminClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
s.Zitadel.adminAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.authID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.authClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
s.Zitadel.authAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.consoleID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.consoleClientID, err = domain.NewClientID(id.SonyFlakeGenerator, zitadelProjectName)
s.Zitadel.consoleAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
return nil
}
func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*domain.ObjectDetails, error) {
func (c *commandNew) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*domain.ObjectDetails, error) {
instanceID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
ctx = authz.SetCtxData(authz.WithInstanceID(ctx, instanceID), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID})
requestedDomain := authz.GetInstance(ctx).RequestedDomain()
ctx = authz.SetCtxData(authz.WithRequestedDomain(authz.WithInstanceID(ctx, instanceID), requestedDomain), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID})
orgID, err := id.SonyFlakeGenerator.Next()
if err != nil {
@ -219,6 +212,14 @@ func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup)
setup.Features.ActionsAllowed,
setup.Features.MaxActions,
),
addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeAppSecret, setup.SecretGenerators.ClientSecret),
addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeInitCode, setup.SecretGenerators.InitializeUserCode),
addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyEmailCode, setup.SecretGenerators.EmailVerificationCode),
addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyPhoneCode, setup.SecretGenerators.PhoneVerificationCode),
addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypePasswordResetCode, setup.SecretGenerators.PasswordVerificationCode),
addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypePasswordlessInitCode, setup.SecretGenerators.PasswordlessInitCode),
addSecretGeneratorConfig(instanceAgg, domain.SecretGeneratorTypeVerifyDomain, setup.SecretGenerators.DomainVerification),
AddPasswordComplexityPolicy(
instanceAgg,
setup.PasswordComplexityPolicy.MinLength,
@ -280,72 +281,82 @@ func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup)
validations = append(validations, SetInstanceCustomTexts(instanceAgg, msg))
}
validations = append(validations,
AddOrg(orgAgg, setup.Org.Name, command.iamDomain),
AddHumanCommand(userAgg, &setup.Org.Human, command.userPasswordAlg),
AddOrgMember(orgAgg, userID, domain.RoleOrgOwner),
AddInstanceMember(instanceAgg, userID, domain.RoleIAMOwner),
console := &addOIDCApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.consoleAppID,
Name: consoleAppName,
},
Version: domain.OIDCVersionV1,
RedirectUris: []string{setup.Zitadel.BaseURL + consoleRedirectPath},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: domain.OIDCApplicationTypeUserAgent,
AuthMethodType: domain.OIDCAuthMethodTypeNone,
PostLogoutRedirectUris: []string{setup.Zitadel.BaseURL + consolePostLogoutPath},
DevMode: setup.Zitadel.IsDevMode,
AccessTokenType: domain.OIDCTokenTypeBearer,
AccessTokenRoleAssertion: false,
IDTokenRoleAssertion: false,
IDTokenUserinfoAssertion: false,
ClockSkew: 0,
}
AddProject(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified),
validations = append(validations,
AddOrgCommand(ctx, orgAgg, setup.Org.Name),
addHumanCommand(userAgg, &setup.Org.Human, c.userPasswordAlg, c.phoneAlg, c.emailAlg, c.initCodeAlg),
c.AddOrgMember(orgAgg, userID, domain.RoleOrgOwner),
c.AddInstanceMember(instanceAgg, userID, domain.RoleIAMOwner),
AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified),
SetIAMProject(instanceAgg, projectAgg.ID),
AddAPIApp(
*projectAgg,
setup.Zitadel.mgmtID,
mgmtAppName,
setup.Zitadel.mgmtClientID,
AddAPIAppCommand(
&addAPIApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.mgmtAppID,
Name: mgmtAppName,
},
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
},
nil,
domain.APIAuthMethodTypePrivateKeyJWT,
),
AddAPIApp(
*projectAgg,
setup.Zitadel.adminID,
adminAppName,
setup.Zitadel.adminClientID,
AddAPIAppCommand(
&addAPIApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.adminAppID,
Name: adminAppName,
},
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
},
nil,
domain.APIAuthMethodTypePrivateKeyJWT,
),
AddAPIApp(
*projectAgg,
setup.Zitadel.authID,
authAppName,
setup.Zitadel.authClientID,
AddAPIAppCommand(
&addAPIApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.authAppID,
Name: authAppName,
},
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
},
nil,
domain.APIAuthMethodTypePrivateKeyJWT,
),
AddOIDCApp(
*projectAgg,
domain.OIDCVersionV1,
setup.Zitadel.consoleID,
consoleAppName,
setup.Zitadel.consoleClientID,
nil,
[]string{setup.Zitadel.BaseURL + consoleRedirectPath},
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeUserAgent,
domain.OIDCAuthMethodTypeNone,
[]string{setup.Zitadel.BaseURL + consolePostLogoutPath},
setup.Zitadel.IsDevMode,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
),
SetIAMConsoleID(instanceAgg, setup.Zitadel.consoleClientID),
AddOIDCAppCommand(console, nil),
SetIAMConsoleID(instanceAgg, &console.ClientID),
)
cmds, err := preparation.PrepareCommands(ctx, command.es.Filter, validations...)
cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, validations...)
if err != nil {
return nil, err
}
events, err := command.es.Push(ctx, cmds...)
events, err := c.es.Push(ctx, cmds...)
if err != nil {
return nil, err
}
@ -368,7 +379,7 @@ func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validati
}
//SetIAMConsoleID defines the command to set the clientID of the Console App onto the instance
func SetIAMConsoleID(a *instance.Aggregate, clientID string) preparation.Validation {
func SetIAMConsoleID(a *instance.Aggregate, clientID *string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
@ -377,3 +388,25 @@ func SetIAMConsoleID(a *instance.Aggregate, clientID string) preparation.Validat
}, nil
}
}
func (c *Commands) setGlobalOrg(ctx context.Context, iamAgg *eventstore.Aggregate, iamWriteModel *InstanceWriteModel, orgID string) (eventstore.Command, error) {
err := c.eventstore.FilterToQueryReducer(ctx, iamWriteModel)
if err != nil {
return nil, err
}
if iamWriteModel.GlobalOrgID != "" {
return nil, errors.ThrowPreconditionFailed(nil, "IAM-HGG24", "Errors.IAM.GlobalOrgAlreadySet")
}
return instance.NewGlobalOrgSetEventEvent(ctx, iamAgg, orgID), nil
}
func (c *Commands) setIAMProject(ctx context.Context, iamAgg *eventstore.Aggregate, iamWriteModel *InstanceWriteModel, projectID string) (eventstore.Command, error) {
err := c.eventstore.FilterToQueryReducer(ctx, iamWriteModel)
if err != nil {
return nil, err
}
if iamWriteModel.ProjectID != "" {
return nil, errors.ThrowPreconditionFailed(nil, "IAM-EGbw2", "Errors.IAM.IAMProjectAlreadySet")
}
return instance.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil
}

View File

@ -5,12 +5,11 @@ import (
"strings"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func (c *Commands) AddInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) {
@ -52,7 +51,7 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri
func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain string, generated bool) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument")
return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain)
@ -60,7 +59,7 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin
return nil, err
}
if domainWriteModel.State == domain.InstanceDomainStateActive {
return nil, caos_errs.ThrowAlreadyExists(nil, "INST-i2nl", "Errors.Instance.Domain.AlreadyExists")
return nil, errors.ThrowAlreadyExists(nil, "INST-i2nl", "Errors.Instance.Domain.AlreadyExists")
}
return []eventstore.Command{instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated)}, nil
}, nil
@ -70,7 +69,7 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin
func (c *Commands) removeInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-39nls", "Errors.Invalid.Argument")
return nil, errors.ThrowInvalidArgument(nil, "INST-39nls", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain)
@ -78,10 +77,10 @@ func (c *Commands) removeInstanceDomain(a *instance.Aggregate, instanceDomain st
return nil, err
}
if domainWriteModel.State != domain.InstanceDomainStateActive {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-8ls9f", "Errors.Instance.Domain.NotFound")
return nil, errors.ThrowNotFound(nil, "INSTANCE-8ls9f", "Errors.Instance.Domain.NotFound")
}
if domainWriteModel.Generated {
return nil, caos_errs.ThrowPreconditionFailed(nil, "INSTANCE-9hn3n", "Errors.Instance.Domain.GeneratedNotRemovable")
return nil, errors.ThrowPreconditionFailed(nil, "INSTANCE-9hn3n", "Errors.Instance.Domain.GeneratedNotRemovable")
}
return []eventstore.Command{instance.NewDomainRemovedEvent(ctx, &a.Aggregate, instanceDomain)}, nil
}, nil

View File

@ -3,7 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)

View File

@ -5,8 +5,7 @@ import (
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
@ -95,8 +94,8 @@ func SetInstanceCustomTexts(
}
}
func existingInstanceCustomMessageText(ctx context.Context, filter preparation.FilterToQueryReducer, textType string, lang language.Tag) (*command.InstanceCustomMessageTextWriteModel, error) {
writeModel := command.NewInstanceCustomMessageTextWriteModel(ctx, textType, lang)
func existingInstanceCustomMessageText(ctx context.Context, filter preparation.FilterToQueryReducer, textType string, lang language.Tag) (*InstanceCustomMessageTextWriteModel, error) {
writeModel := NewInstanceCustomMessageTextWriteModel(ctx, textType, lang)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err

View File

@ -2,13 +2,97 @@ package command
import (
"context"
"time"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func SetDefaultFeatures(
a *instance.Aggregate,
tierName,
tierDescription string,
state domain.FeaturesState,
stateDescription string,
retention time.Duration,
loginPolicyFactors,
loginPolicyIDP,
loginPolicyPasswordless,
loginPolicyRegistration,
loginPolicyUsernameLogin,
loginPolicyPasswordReset,
passwordComplexityPolicy,
labelPolicyPrivateLabel,
labelPolicyWatermark,
customDomain,
privacyPolicy,
metadataUser,
customTextMessage,
customTextLogin,
lockoutPolicy bool,
actionsAllowed domain.ActionsAllowed,
maxActions int,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if !state.Valid() || state == domain.FeaturesStateUnspecified || state == domain.FeaturesStateRemoved {
return nil, errors.ThrowInvalidArgument(nil, "INSTA-d3r1s", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := defaultFeatures(ctx, filter)
if err != nil {
return nil, err
}
event, hasChanged := writeModel.NewSetEvent(ctx, &a.Aggregate,
tierName,
tierDescription,
state,
stateDescription,
retention,
loginPolicyFactors,
loginPolicyIDP,
loginPolicyPasswordless,
loginPolicyRegistration,
loginPolicyUsernameLogin,
loginPolicyPasswordReset,
passwordComplexityPolicy,
labelPolicyPrivateLabel,
labelPolicyWatermark,
customDomain,
privacyPolicy,
metadataUser,
customTextMessage,
customTextLogin,
lockoutPolicy,
actionsAllowed,
maxActions,
)
if !hasChanged {
return nil, errors.ThrowPreconditionFailed(nil, "INSTA-GE4h2", "Errors.Features.NotChanged")
}
return []eventstore.Command{
event,
}, nil
}, nil
}
}
func defaultFeatures(ctx context.Context, filter preparation.FilterToQueryReducer) (*InstanceFeaturesWriteModel, error) {
features := NewInstanceFeaturesWriteModel(ctx)
events, err := filter(ctx, features.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return features, nil
}
features.AppendEvents(events...)
err = features.Reduce()
return features, err
}
func (c *Commands) SetDefaultFeatures(ctx context.Context, features *domain.Features) (*domain.ObjectDetails, error) {
existingFeatures := NewInstanceFeaturesWriteModel(ctx)
setEvent, err := c.setDefaultFeatures(ctx, existingFeatures, features)
@ -59,7 +143,7 @@ func (c *Commands) setDefaultFeatures(ctx context.Context, existingFeatures *Ins
features.MaxActions,
)
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")
return nil, errors.ThrowPreconditionFailed(nil, "Features-GE4h2", "Errors.Features.NotChanged")
}
return setEvent, nil
}

View File

@ -3,7 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)

View File

@ -3,7 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)

View File

@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"

View File

@ -4,15 +4,71 @@ import (
"context"
"reflect"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/telemetry/tracing"
)
func (c *commandNew) AddInstanceMember(a *instance.Aggregate, userID string, roles ...string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if userID == "" {
return nil, errors.ThrowInvalidArgument(nil, "INSTA-SDSfs", "Errors.Invalid.Argument")
}
if len(domain.CheckForInvalidRoles(roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-4m0fS", "Errors.IAM.MemberInvalid")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists {
return nil, errors.ThrowNotFound(err, "INSTA-GSXOn", "Errors.User.NotFound")
}
if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember {
return nil, errors.ThrowAlreadyExists(err, "INSTA-pFDwe", "Errors.Instance.Member.AlreadyExists")
}
return []eventstore.Command{instance.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil
},
nil
}
}
func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReducer, instanceID, userID string) (isMember bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
OrderAsc().
AddQuery().
AggregateIDs(instanceID).
AggregateTypes(instance.AggregateType).
EventTypes(
instance.MemberAddedEventType,
instance.MemberRemovedEventType,
instance.MemberCascadeRemovedEventType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch e := event.(type) {
case *instance.MemberAddedEvent:
if e.UserID == userID {
isMember = true
}
case *instance.MemberRemovedEvent:
if e.UserID == userID {
isMember = false
}
case *instance.MemberCascadeRemovedEvent:
if e.UserID == userID {
isMember = false
}
}
}
return isMember, nil
}
func (c *Commands) AddInstanceMember(ctx context.Context, member *domain.Member) (*domain.Member, error) {
if member.UserID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-Mf83b", "Errors.IAM.MemberInvalid")

View File

@ -3,7 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)

View File

@ -3,7 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)

View File

@ -3,7 +3,7 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)

View File

@ -173,7 +173,7 @@ func eventFromEventPusherWithInstanceID(instanceID string, event eventstore.Comm
AggregateID: event.Aggregate().ID,
AggregateType: repository.AggregateType(event.Aggregate().Type),
ResourceOwner: sql.NullString{String: event.Aggregate().ResourceOwner, Valid: event.Aggregate().ResourceOwner != ""},
InstanceID: sql.NullString{String: instanceID, Valid: instanceID != ""},
InstanceID: instanceID,
}
}

View File

@ -2,14 +2,78 @@ package command
import (
"context"
"strings"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/repository/org"
user_repo "github.com/caos/zitadel/internal/repository/user"
)
type OrgSetup struct {
Name string
Human AddHuman
}
func (c *commandNew) SetUpOrg(ctx context.Context, o *OrgSetup) (*domain.ObjectDetails, error) {
orgID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
userID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
orgAgg := org.NewAggregate(orgID, orgID)
userAgg := user_repo.NewAggregate(userID, orgID)
cmds, err := preparation.PrepareCommands(ctx, c.es.Filter,
AddOrgCommand(ctx, orgAgg, o.Name),
addHumanCommand(userAgg, &o.Human, c.userPasswordAlg, c.phoneAlg, c.emailAlg, c.initCodeAlg),
c.AddOrgMember(orgAgg, userID, domain.RoleOrgOwner),
)
if err != nil {
return nil, err
}
events, err := c.es.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: orgID,
}, nil
}
//AddOrgCommand defines the commands to create a new org,
// this includes the verified default domain
func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")
}
defaultDomain := domain.NewIAMDomainName(name, authz.GetInstance(ctx).RequestedDomain())
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
org.NewOrgAddedEvent(ctx, &a.Aggregate, name),
org.NewDomainAddedEvent(ctx, &a.Aggregate, defaultDomain),
org.NewDomainVerifiedEvent(ctx, &a.Aggregate, defaultDomain),
org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, defaultDomain),
}, nil
}, nil
}
}
func (c *Commands) getOrg(ctx context.Context, orgID string) (*domain.Org, error) {
writeModel, err := c.getOrgWriteModelByID(ctx, orgID)
if err != nil {

View File

@ -2,20 +2,92 @@ package command
import (
"context"
"strings"
"github.com/caos/logging"
errs "errors"
http_utils "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func AddOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
existing, err := orgDomain(ctx, filter, a.ID, domain)
if err != nil && !errs.Is(err, errors.ThrowNotFound(nil, "", "")) {
return nil, err
}
if existing != nil && existing.Verified {
return nil, errors.ThrowAlreadyExists(nil, "V2-e1wse", "Errors.Already.Exists")
}
return []eventstore.Command{org.NewDomainAddedEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
func VerifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
// no checks required because unique constraints handle it
return []eventstore.Command{org.NewDomainVerifiedEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
func SetPrimaryOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
existing, err := orgDomain(ctx, filter, a.ID, domain)
if err != nil {
return nil, errors.ThrowAlreadyExists(err, "V2-d0Gyw", "Errors.Already.Exists")
}
if existing.Primary {
return nil, errors.ThrowPreconditionFailed(nil, "COMMA-FfoZO", "Errors.Org.DomainAlreadyPrimary")
}
if !existing.Verified {
return nil, errors.ThrowPreconditionFailed(nil, "COMMA-yKA80", "Errors.Org.DomainNotVerified")
}
return []eventstore.Command{org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
func orgDomain(ctx context.Context, filter preparation.FilterToQueryReducer, orgID, domain string) (*OrgDomainWriteModel, error) {
wm := NewOrgDomainWriteModel(orgID, domain)
events, err := filter(ctx, wm.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return nil, errors.ThrowNotFound(nil, "COMMA-kFHpQ", "Errors.Org.DomainNotFound")
}
wm.AppendEvents(events...)
if err = wm.Reduce(); err != nil {
return nil, err
}
return wm, nil
}
func (c *Commands) AddOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, claimedUserIDs []string) (*domain.OrgDomain, error) {
if !orgDomain.IsValid() {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain")
return nil, errors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain")
}
domainWriteModel := NewOrgDomainWriteModel(orgDomain.AggregateID, orgDomain.Domain)
orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel)
@ -36,21 +108,21 @@ func (c *Commands) AddOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain
func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *domain.OrgDomain) (token, url string, err error) {
if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" {
return "", "", caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain")
return "", "", errors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain")
}
checkType, ok := orgDomain.ValidationType.CheckType()
if !ok {
return "", "", caos_errs.ThrowInvalidArgument(nil, "ORG-Gsw31", "Errors.Org.DomainVerificationTypeInvalid")
return "", "", errors.ThrowInvalidArgument(nil, "ORG-Gsw31", "Errors.Org.DomainVerificationTypeInvalid")
}
domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain)
if err != nil {
return "", "", err
}
if domainWriteModel.State != domain.OrgDomainStateActive {
return "", "", caos_errs.ThrowNotFound(nil, "ORG-AGD31", "Errors.Org.DomainNotOnOrg")
return "", "", errors.ThrowNotFound(nil, "ORG-AGD31", "Errors.Org.DomainNotOnOrg")
}
if domainWriteModel.Verified {
return "", "", caos_errs.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified")
return "", "", errors.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified")
}
token, err = orgDomain.GenerateVerificationCode(c.domainVerificationGenerator)
if err != nil {
@ -58,7 +130,7 @@ func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *d
}
url, err = http_utils.TokenUrl(orgDomain.Domain, token, checkType)
if err != nil {
return "", "", caos_errs.ThrowPreconditionFailed(err, "ORG-Bae21", "Errors.Org.DomainVerificationTypeInvalid")
return "", "", errors.ThrowPreconditionFailed(err, "ORG-Bae21", "Errors.Org.DomainVerificationTypeInvalid")
}
orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel)
@ -74,20 +146,20 @@ func (c *Commands) GenerateOrgDomainValidation(ctx context.Context, orgDomain *d
func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain, claimedUserIDs []string) (*domain.ObjectDetails, error) {
if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain")
return nil, errors.ThrowInvalidArgument(nil, "ORG-R24hb", "Errors.Org.InvalidDomain")
}
domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain)
if err != nil {
return nil, err
}
if domainWriteModel.State != domain.OrgDomainStateActive {
return nil, caos_errs.ThrowNotFound(nil, "ORG-Sjdi3", "Errors.Org.DomainNotOnOrg")
return nil, errors.ThrowNotFound(nil, "ORG-Sjdi3", "Errors.Org.DomainNotOnOrg")
}
if domainWriteModel.Verified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified")
return nil, errors.ThrowPreconditionFailed(nil, "ORG-HGw21", "Errors.Org.DomainAlreadyVerified")
}
if domainWriteModel.ValidationCode == nil || domainWriteModel.ValidationType == domain.OrgDomainValidationTypeUnspecified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-SFBB3", "Errors.Org.DomainVerificationMissing")
return nil, errors.ThrowPreconditionFailed(nil, "ORG-SFBB3", "Errors.Org.DomainVerificationMissing")
}
validationCode, err := crypto.DecryptString(domainWriteModel.ValidationCode, c.domainVerificationAlg)
@ -122,22 +194,22 @@ func (c *Commands) ValidateOrgDomain(ctx context.Context, orgDomain *domain.OrgD
events = append(events, org.NewDomainVerificationFailedEvent(ctx, orgAgg, orgDomain.Domain))
_, err = c.eventstore.Push(ctx, events...)
logging.LogWithFields("ORG-dhTE", "orgID", orgAgg.ID, "domain", orgDomain.Domain).OnError(err).Error("NewDomainVerificationFailedEvent push failed")
return nil, caos_errs.ThrowInvalidArgument(err, "ORG-GH3s", "Errors.Org.DomainVerificationFailed")
return nil, errors.ThrowInvalidArgument(err, "ORG-GH3s", "Errors.Org.DomainVerificationFailed")
}
func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) {
if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SsDG2", "Errors.Org.InvalidDomain")
return nil, errors.ThrowInvalidArgument(nil, "ORG-SsDG2", "Errors.Org.InvalidDomain")
}
domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain)
if err != nil {
return nil, err
}
if domainWriteModel.State != domain.OrgDomainStateActive {
return nil, caos_errs.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg")
return nil, errors.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg")
}
if !domainWriteModel.Verified {
return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-Ggd32", "Errors.Org.DomainNotVerified")
return nil, errors.ThrowPreconditionFailed(nil, "ORG-Ggd32", "Errors.Org.DomainNotVerified")
}
orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewDomainPrimarySetEvent(ctx, orgAgg, orgDomain.Domain))
@ -153,17 +225,17 @@ func (c *Commands) SetPrimaryOrgDomain(ctx context.Context, orgDomain *domain.Or
func (c *Commands) RemoveOrgDomain(ctx context.Context, orgDomain *domain.OrgDomain) (*domain.ObjectDetails, error) {
if orgDomain == nil || !orgDomain.IsValid() || orgDomain.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SJsK3", "Errors.Org.InvalidDomain")
return nil, errors.ThrowInvalidArgument(nil, "ORG-SJsK3", "Errors.Org.InvalidDomain")
}
domainWriteModel, err := c.getOrgDomainWriteModel(ctx, orgDomain.AggregateID, orgDomain.Domain)
if err != nil {
return nil, err
}
if domainWriteModel.State != domain.OrgDomainStateActive {
return nil, caos_errs.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg")
return nil, errors.ThrowNotFound(nil, "ORG-GDfA3", "Errors.Org.DomainNotOnOrg")
}
if domainWriteModel.Primary {
return nil, caos_errs.ThrowPreconditionFailed(nil, "ORG-Sjdi3", "Errors.Org.PrimaryDomainNotDeletable")
return nil, errors.ThrowPreconditionFailed(nil, "ORG-Sjdi3", "Errors.Org.PrimaryDomainNotDeletable")
}
orgAgg := OrgAggregateFromWriteModel(&domainWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewDomainRemovedEvent(ctx, orgAgg, orgDomain.Domain, domainWriteModel.Verified))
@ -183,7 +255,7 @@ func (c *Commands) addOrgDomain(ctx context.Context, orgAgg *eventstore.Aggregat
return nil, err
}
if addedDomain.State == domain.OrgDomainStateActive {
return nil, caos_errs.ThrowAlreadyExists(nil, "COMMA-Bd2jj", "Errors.Org.Domain.AlreadyExists")
return nil, errors.ThrowAlreadyExists(nil, "COMMA-Bd2jj", "Errors.Org.Domain.AlreadyExists")
}
events := []eventstore.Command{

View File

@ -9,9 +9,10 @@ import (
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
@ -21,6 +22,206 @@ import (
"github.com/caos/zitadel/internal/repository/user"
)
func TestAddDomain(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
filter preparation.FilterToQueryReducer
}
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: agg,
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: agg,
domain: "domain",
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
},
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainAddedEvent(context.Background(), &agg.Aggregate, "domain"),
},
},
},
{
name: "already verified",
args: args{
a: agg,
domain: "domain",
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"),
org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"),
}, nil
},
},
want: Want{
CreateErr: errors.ThrowAlreadyExists(nil, "", ""),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrgDomain(tt.args.a, tt.args.domain), tt.args.filter, tt.want)
})
}
}
func TestVerifyDomain(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
}
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: org.NewAggregate("test", "test"),
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: org.NewAggregate("test", "test"),
domain: "domain",
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainVerifiedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, VerifyOrgDomain(tt.args.a, tt.args.domain), nil, tt.want)
})
}
}
func TestSetDomainPrimary(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
filter preparation.FilterToQueryReducer
}
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: agg,
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument"),
},
},
{
name: "not exists",
args: args{
a: agg,
domain: "domain",
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
},
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "", ""),
},
},
{
name: "not verified",
args: args{
a: agg,
domain: "domain",
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain")}, nil
},
},
want: Want{
CreateErr: errors.ThrowPreconditionFailed(nil, "", ""),
},
},
{
name: "already primary",
args: args{
a: agg,
domain: "domain",
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"),
org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"),
org.NewDomainPrimarySetEvent(ctx, &agg.Aggregate, "domain"),
}, nil
},
},
want: Want{
CreateErr: errors.ThrowPreconditionFailed(nil, "", ""),
},
},
{
name: "correct",
args: args{
a: agg,
domain: "domain",
filter: func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "domain"),
org.NewDomainVerificationAddedEvent(ctx, &agg.Aggregate, "domain", domain.OrgDomainValidationTypeHTTP, nil),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "domain"),
}, nil
},
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainPrimarySetEvent(context.Background(), &agg.Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, SetPrimaryOrgDomain(tt.args.a, tt.args.domain), tt.args.filter, tt.want)
})
}
}
func TestCommandSide_AddOrgDomain(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
@ -52,7 +253,7 @@ func TestCommandSide_AddOrgDomain(t *testing.T) {
domain: &domain.OrgDomain{},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -86,7 +287,7 @@ func TestCommandSide_AddOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
err: errors.IsErrorAlreadyExists,
},
},
{
@ -187,7 +388,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -204,7 +405,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -224,7 +425,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -253,7 +454,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) {
},
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -294,7 +495,7 @@ func TestCommandSide_GenerateOrgDomainValidation(t *testing.T) {
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -462,7 +663,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -479,7 +680,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -508,7 +709,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -549,7 +750,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -584,7 +785,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -642,7 +843,7 @@ func TestCommandSide_ValidateOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -920,7 +1121,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -937,7 +1138,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -966,7 +1167,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -1000,7 +1201,7 @@ func TestCommandSide_SetPrimaryDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -1107,7 +1308,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -1124,7 +1325,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -1153,7 +1354,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -1199,7 +1400,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) {
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -1318,7 +1519,7 @@ func TestCommandSide_RemoveOrgDomain(t *testing.T) {
}
func invalidDomainVerification(domain, token, verifier string, checkType http.CheckType) error {
return caos_errs.ThrowInvalidArgument(nil, "HTTP-GH422", "Errors.Internal")
return errors.ThrowInvalidArgument(nil, "HTTP-GH422", "Errors.Internal")
}
func validDomainVerification(domain, token, verifier string, checkType http.CheckType) error {

View File

@ -4,24 +4,84 @@ import (
"context"
"reflect"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/telemetry/tracing"
)
func (c *commandNew) AddOrgMember(a *org.Aggregate, userID string, roles ...string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if userID == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument")
}
if len(roles) == 0 {
return nil, errors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument")
}
if len(domain.CheckForInvalidRoles(roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 {
return nil, errors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsUser(ctx, filter, userID, a.ID); err != nil || !exists {
return nil, errors.ThrowNotFound(err, "ORG-GoXOn", "Errors.User.NotFound")
}
if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember {
return nil, errors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists")
}
return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil
},
nil
}
}
func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, orgID, userID string) (isMember bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(orgID).
OrderAsc().
AddQuery().
AggregateIDs(orgID).
AggregateTypes(org.AggregateType).
EventTypes(
org.MemberAddedEventType,
org.MemberRemovedEventType,
org.MemberCascadeRemovedEventType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch e := event.(type) {
case *org.MemberAddedEvent:
if e.UserID == userID {
isMember = true
}
case *org.MemberRemovedEvent:
if e.UserID == userID {
isMember = false
}
case *org.MemberCascadeRemovedEvent:
if e.UserID == userID {
isMember = false
}
}
}
return isMember, nil
}
func (c *Commands) AddOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) {
if member.UserID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-u8fkf", "Errors.Org.MemberInvalid")
return nil, errors.ThrowInvalidArgument(nil, "Org-u8fkf", "Errors.Org.MemberInvalid")
}
addedMember := NewOrgMemberWriteModel(member.AggregateID, member.UserID)
orgAgg := OrgAggregateFromWriteModel(&addedMember.WriteModel)
err := c.checkUserExists(ctx, addedMember.UserID, "")
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "Org-2H8ds", "Errors.User.NotFound")
return nil, errors.ThrowPreconditionFailed(err, "Org-2H8ds", "Errors.User.NotFound")
}
event, err := c.addOrgMember(ctx, orgAgg, addedMember, member)
if err != nil {
@ -40,10 +100,10 @@ func (c *Commands) AddOrgMember(ctx context.Context, member *domain.Member) (*do
func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, addedMember *OrgMemberWriteModel, member *domain.Member) (eventstore.Command, error) {
if !member.IsValid() {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid")
return nil, errors.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(member.Roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
return nil, errors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid")
}
err := c.eventstore.FilterToQueryReducer(ctx, addedMember)
if err != nil {
@ -59,10 +119,10 @@ func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregat
//ChangeOrgMember updates an existing member
func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) {
if !member.IsValid() {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
return nil, errors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid")
}
if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 {
return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid")
return nil, errors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid")
}
existingMember, err := c.orgMemberWriteModelByID(ctx, member.AggregateID, member.UserID)
@ -71,7 +131,7 @@ func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (
}
if reflect.DeepEqual(existingMember.Roles, member.Roles) {
return nil, caos_errs.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged")
return nil, errors.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged")
}
orgAgg := OrgAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewMemberChangedEvent(ctx, orgAgg, member.UserID, member.Roles...))

View File

@ -8,8 +8,9 @@ import (
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
@ -19,6 +20,271 @@ import (
"github.com/caos/zitadel/internal/repository/user"
)
func TestAddMember(t *testing.T) {
type args struct {
a *org.Aggregate
userID string
roles []string
zitadelRoles []authz.RoleMapping
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "no user id",
args: args{
a: agg,
userID: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
},
},
{
name: "no roles",
args: args{
a: agg,
userID: "12342",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument"),
},
},
{
name: "TODO: invalid roles",
args: args{
a: agg,
userID: "123",
roles: []string{"ORG_OWNER"},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "Org-4N8es", ""),
},
},
{
name: "user not exists",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
},
},
filter: NewMultiFilter().Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "ORG-GoXOn", "Errors.User.NotFound"),
},
},
{
name: "already member",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
},
},
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
ctx,
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
ctx,
&org.NewAggregate("id", "ro").Aggregate,
"userID",
),
}, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowAlreadyExists(nil, "ORG-poWwe", "Errors.Org.Member.AlreadyExists"),
},
},
{
name: "correct",
args: args{
a: agg,
userID: "userID",
roles: []string{"ORG_OWNER"},
zitadelRoles: []authz.RoleMapping{
{
Role: "ORG_OWNER",
},
},
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
ctx,
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID", "ORG_OWNER"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, (&commandNew{zitadelRoles: tt.args.zitadelRoles}).AddOrgMember(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want)
})
}
}
func TestIsMember(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
orgID string
userID string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "member added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: true,
wantErr: false,
},
{
name: "member removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
org.NewMemberRemovedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "member cascade removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
org.NewMemberCascadeRemovedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := IsOrgMember(context.Background(), tt.args.filter, tt.args.orgID, tt.args.userID)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}
func TestCommandSide_AddOrgMember(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
@ -54,7 +320,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -76,7 +342,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -113,7 +379,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -163,7 +429,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
err: errors.IsErrorAlreadyExists,
},
},
{
@ -188,7 +454,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
),
),
expectFilter(),
expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "ERROR", "internal"),
expectPushFailed(errors.ThrowAlreadyExists(nil, "ERROR", "internal"),
[]*repository.Event{
eventFromEventPusher(org.NewMemberAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
@ -216,7 +482,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
err: errors.IsErrorAlreadyExists,
},
},
{
@ -335,7 +601,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -356,7 +622,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -383,7 +649,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -418,7 +684,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) {
},
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -530,7 +796,7 @@ func TestCommandSide_RemoveOrgMember(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -547,7 +813,7 @@ func TestCommandSide_RemoveOrgMember(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{

View File

@ -9,7 +9,7 @@ import (
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
@ -20,6 +20,53 @@ import (
"github.com/caos/zitadel/internal/repository/user"
)
func TestAddOrg(t *testing.T) {
type args struct {
a *org.Aggregate
name string
}
ctx := context.Background()
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: agg,
name: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: agg,
name: "caos ag",
},
want: Want{
Commands: []eventstore.Command{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "caos ag"),
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
org.NewDomainPrimarySetEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrgCommand(authz.WithRequestedDomain(context.Background(), "localhost"), tt.args.a, tt.args.name), nil, tt.want)
})
}
}
func TestCommandSide_AddOrg(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
@ -57,7 +104,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -101,7 +148,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -127,7 +174,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
expectFilterOrgMemberNotFound(),
expectPushFailed(caos_errs.ThrowAlreadyExists(nil, "id", "internal"),
expectPushFailed(errors.ThrowAlreadyExists(nil, "id", "internal"),
[]*repository.Event{
eventFromEventPusher(org.NewOrgAddedEvent(
context.Background(),
@ -170,7 +217,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorAlreadyExists,
err: errors.IsErrorAlreadyExists,
},
},
{
@ -196,7 +243,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
),
),
expectFilterOrgMemberNotFound(),
expectPushFailed(caos_errs.ThrowInternal(nil, "id", "internal"),
expectPushFailed(errors.ThrowInternal(nil, "id", "internal"),
[]*repository.Event{
eventFromEventPusher(org.NewOrgAddedEvent(
context.Background(),
@ -239,7 +286,7 @@ func TestCommandSide_AddOrg(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsInternal,
err: errors.IsInternal,
},
},
{
@ -374,7 +421,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
orgID: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -391,7 +438,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
name: "org",
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -409,7 +456,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
),
expectFilter(),
expectPushFailed(
caos_errs.ThrowInternal(nil, "id", "message"),
errors.ThrowInternal(nil, "id", "message"),
[]*repository.Event{
eventFromEventPusher(org.NewOrgChangedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate, "org", "neworg")),
@ -425,7 +472,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) {
name: "neworg",
},
res: res{
err: caos_errs.IsInternal,
err: errors.IsInternal,
},
},
{
@ -596,7 +643,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
orgID: "org1",
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -622,7 +669,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
orgID: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -638,7 +685,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
),
),
expectPushFailed(
caos_errs.ThrowInternal(nil, "id", "message"),
errors.ThrowInternal(nil, "id", "message"),
[]*repository.Event{
eventFromEventPusher(org.NewOrgDeactivatedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate)),
@ -651,7 +698,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) {
orgID: "org1",
},
res: res{
err: caos_errs.IsInternal,
err: errors.IsInternal,
},
},
{
@ -731,7 +778,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
orgID: "org1",
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -753,7 +800,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
orgID: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -774,7 +821,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
),
),
expectPushFailed(
caos_errs.ThrowInternal(nil, "id", "message"),
errors.ThrowInternal(nil, "id", "message"),
[]*repository.Event{
eventFromEventPusher(org.NewOrgReactivatedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate,
@ -788,7 +835,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) {
orgID: "org1",
},
res: res{
err: caos_errs.IsInternal,
err: errors.IsInternal,
},
},
{

33
internal/command/phone.go Normal file
View File

@ -0,0 +1,33 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/ttacon/libphonenumber"
)
type Phone struct {
Number string
Verified bool
}
func FormatPhoneNumber(number string) (string, error) {
if number == "" {
return "", nil
}
phoneNr, err := libphonenumber.Parse(number, libphonenumber.UNKNOWN_REGION)
if err != nil {
return "", errors.ThrowInvalidArgument(nil, "EVENT-so0wa", "Errors.User.Phone.Invalid")
}
number = libphonenumber.Format(phoneNr, libphonenumber.E164)
return number, nil
}
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
}

View File

@ -0,0 +1,57 @@
package command
import (
"testing"
"github.com/caos/zitadel/internal/errors"
)
func TestFormatPhoneNumber(t *testing.T) {
type args struct {
number string
}
tests := []struct {
name string
args args
result *Phone
errFunc func(err error) bool
}{
{
name: "invalid phone number",
args: args{
number: "PhoneNumber",
},
errFunc: errors.IsErrorInvalidArgument,
},
{
name: "format phone +4171 xxx xx xx",
args: args{
number: "+4171 123 45 67",
},
result: &Phone{
Number: "+41711234567",
},
},
{
name: "format non swiss phone +4371 xxx xx xx",
args: args{
number: "+4371 123 45 67",
},
result: &Phone{
Number: "+43711234567",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
formatted, err := FormatPhoneNumber(tt.args.number)
if tt.errFunc == nil && tt.result.Number != formatted {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.number, formatted)
}
if tt.errFunc != nil && !tt.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}

View File

@ -7,7 +7,7 @@ import (
"reflect"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/eventstore"
)
@ -18,13 +18,17 @@ type Want struct {
Commands []eventstore.Command
}
type CommandVerifier interface {
Validate(eventstore.Command) bool
}
//AssertValidation checks if the validation works as inteded
func AssertValidation(t *testing.T, validation preparation.Validation, filter preparation.FilterToQueryReducer, want Want) {
t.Helper()
creates, err := validation()
if !errors.Is(err, want.ValidationErr) {
t.Errorf("wrong validation err = %v, want %v", err, want.ValidationErr)
t.Errorf("wrong validation err = (%[1]T): %[1]v, want (%[2]T): %[2]v", err, want.ValidationErr)
return
}
if err != nil {
@ -32,7 +36,7 @@ func AssertValidation(t *testing.T, validation preparation.Validation, filter pr
}
cmds, err := creates(context.Background(), filter)
if !errors.Is(err, want.CreateErr) {
t.Errorf("wrong create err = %v, want %v", err, want.CreateErr)
t.Errorf("wrong create err = (%[1]T): %[1]v, want (%[2]T): %[2]v", err, want.CreateErr)
return
}
if err != nil {
@ -45,6 +49,12 @@ func AssertValidation(t *testing.T, validation preparation.Validation, filter pr
}
for i, cmd := range want.Commands {
if v, ok := cmd.(CommandVerifier); ok {
if verified := v.Validate(cmds[i]); !verified {
t.Errorf("verification failed on command: = %v, want %v", cmds[i], cmd)
}
continue
}
if !reflect.DeepEqual(cmd, cmds[i]) {
t.Errorf("unexpected command: = %v, want %v", cmds[i], cmd)
}

View File

@ -2,9 +2,12 @@ package command
import (
"context"
"strings"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
@ -52,6 +55,57 @@ func (c *Commands) addProject(ctx context.Context, projectAdd *domain.Project, r
return events, addedProject, nil
}
func AddProjectCommand(
a *project.Aggregate,
name string,
owner string,
projectRoleAssertion bool,
projectRoleCheck bool,
hasProjectCheck bool,
privateLabelingSetting domain.PrivateLabelingSetting,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument")
}
if !privateLabelingSetting.Valid() {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument")
}
if owner == "" {
return nil, errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
project.NewProjectAddedEvent(ctx, &a.Aggregate,
name,
projectRoleAssertion,
projectRoleCheck,
hasProjectCheck,
privateLabelingSetting,
),
project.NewProjectMemberAddedEvent(ctx, &a.Aggregate,
owner,
domain.RoleProjectOwner),
}, nil
}, nil
}
}
func projectWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, resourceOwner string) (project *ProjectWriteModel, err error) {
project = NewProjectWriteModel(projectID, resourceOwner)
events, err := filter(ctx, project.Query())
if err != nil {
return nil, err
}
project.AppendEvents(events...)
if err := project.Reduce(); err != nil {
return nil, err
}
return project, nil
}
func (c *Commands) getProjectByID(ctx context.Context, projectID, resourceOwner string) (*domain.Project, error) {
projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner)
if err != nil {

View File

@ -3,11 +3,23 @@ package command
import (
"context"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/project"
)
type AddApp struct {
Aggregate project.Aggregate
ID string
Name string
}
func newAppClientSecret(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.HashAlgorithm) (value *crypto.CryptoValue, plain string, err error) {
return newCryptoCodeWithPlain(ctx, filter, domain.SecretGeneratorTypeAppSecret, alg)
}
func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) {
if projectID == "" || appChange.GetAppID() == "" || appChange.GetApplicationName() == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.App.Invalid")

View File

@ -2,24 +2,84 @@ package command
import (
"context"
"strings"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
"github.com/caos/zitadel/internal/id"
project_repo "github.com/caos/zitadel/internal/repository/project"
"github.com/caos/zitadel/internal/telemetry/tracing"
)
type addAPIApp struct {
AddApp
AuthMethodType domain.APIAuthMethodType
ClientID string
ClientSecret *crypto.CryptoValue
ClientSecretPlain string
}
func AddAPIAppCommand(app *addAPIApp, clientSecretAlg crypto.HashAlgorithm) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if app.ID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument")
}
if app.Name = strings.TrimSpace(app.Name); app.Name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
project, err := projectWriteModel(ctx, filter, app.Aggregate.ID, app.Aggregate.ResourceOwner)
if err != nil || !project.State.Valid() {
return nil, errors.ThrowNotFound(err, "PROJE-Sf2gb", "Errors.Project.NotFound")
}
app.ClientID, err = domain.NewClientID(id.SonyFlakeGenerator, project.Name)
if err != nil {
return nil, errors.ThrowInternal(err, "V2-f0pgP", "Errors.Internal")
}
//requires client secret
// TODO(release blocking):we have to return the secret
if app.AuthMethodType == domain.APIAuthMethodTypeBasic {
app.ClientSecret, app.ClientSecretPlain, err = newAppClientSecret(ctx, filter, clientSecretAlg)
if err != nil {
return nil, err
}
}
return []eventstore.Command{
project_repo.NewApplicationAddedEvent(
ctx,
&app.Aggregate.Aggregate,
app.ID,
app.Name,
),
project_repo.NewAPIConfigAddedEvent(
ctx,
&app.Aggregate.Aggregate,
app.ID,
app.ClientID,
app.ClientSecret,
app.AuthMethodType,
),
}, nil
}, nil
}
}
func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.APIApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.APIApp, err error) {
if application == nil || application.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Application.Invalid")
return nil, errors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Application.Invalid")
}
project, err := c.getProjectByID(ctx, application.AggregateID, resourceOwner)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-9fnsf", "Errors.Project.NotFound")
return nil, errors.ThrowPreconditionFailed(err, "PROJECT-9fnsf", "Errors.Project.NotFound")
}
addedApplication := NewAPIApplicationWriteModel(application.AggregateID, resourceOwner)
projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel)
@ -43,7 +103,7 @@ func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.AP
func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, apiAppApp *domain.APIApp, resourceOwner string, appSecretGenerator crypto.Generator) (events []eventstore.Command, stringPW string, err error) {
if !apiAppApp.IsValid() {
return nil, "", caos_errs.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid")
return nil, "", errors.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid")
}
apiAppApp.AppID, err = c.idGenerator.Next()
if err != nil {
@ -51,7 +111,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore
}
events = []eventstore.Command{
project.NewApplicationAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.AppName),
project_repo.NewApplicationAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.AppName),
}
var stringPw string
@ -63,7 +123,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore
if err != nil {
return nil, "", err
}
events = append(events, project.NewAPIConfigAddedEvent(ctx,
events = append(events, project_repo.NewAPIConfigAddedEvent(ctx,
projectAgg,
apiAppApp.AppID,
apiAppApp.ClientID,
@ -75,7 +135,7 @@ func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore
func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) {
if apiApp.AppID == "" || apiApp.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid")
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid")
}
existingAPI, err := c.getAPIAppWriteModel(ctx, apiApp.AggregateID, apiApp.AppID, resourceOwner)
@ -83,10 +143,10 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA
return nil, err
}
if existingAPI.State == domain.AppStateUnspecified || existingAPI.State == domain.AppStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2n8uU", "Errors.Project.App.NotExisting")
return nil, errors.ThrowNotFound(nil, "COMMAND-2n8uU", "Errors.Project.App.NotExisting")
}
if !existingAPI.IsAPI() {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Gnwt3", "Errors.Project.App.IsNotAPI")
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Gnwt3", "Errors.Project.App.IsNotAPI")
}
projectAgg := ProjectAggregateFromWriteModel(&existingAPI.WriteModel)
changedEvent, hasChanged, err := existingAPI.NewChangedEvent(
@ -98,7 +158,7 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA
return nil, err
}
if !hasChanged {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-1m88i", "Errors.NoChangesFound")
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-1m88i", "Errors.NoChangesFound")
}
pushedEvents, err := c.eventstore.Push(ctx, changedEvent)
@ -115,7 +175,7 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA
func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, appID, resourceOwner string, appSecretGenerator crypto.Generator) (*domain.APIApp, error) {
if projectID == "" || appID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing")
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-99i83", "Errors.IDMissing")
}
existingAPI, err := c.getAPIAppWriteModel(ctx, projectID, appID, resourceOwner)
@ -123,10 +183,10 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap
return nil, err
}
if existingAPI.State == domain.AppStateUnspecified || existingAPI.State == domain.AppStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-2g66f", "Errors.Project.App.NotExisting")
return nil, errors.ThrowNotFound(nil, "COMMAND-2g66f", "Errors.Project.App.NotExisting")
}
if !existingAPI.IsAPI() {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI")
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI")
}
cryptoSecret, stringPW, err := domain.NewClientSecret(appSecretGenerator)
if err != nil {
@ -135,7 +195,7 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap
projectAgg := ProjectAggregateFromWriteModel(&existingAPI.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewAPIConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret))
pushedEvents, err := c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret))
if err != nil {
return nil, err
}
@ -158,13 +218,13 @@ func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID,
return err
}
if !app.State.Exists() {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-DFnbf", "Errors.Project.App.NoExisting")
return errors.ThrowPreconditionFailed(nil, "COMMAND-DFnbf", "Errors.Project.App.NoExisting")
}
if !app.IsAPI() {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Bf3fw", "Errors.Project.App.IsNotAPI")
return errors.ThrowInvalidArgument(nil, "COMMAND-Bf3fw", "Errors.Project.App.IsNotAPI")
}
if app.ClientSecret == nil {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-D3t5g", "Errors.Project.App.APIConfigInvalid")
return errors.ThrowPreconditionFailed(nil, "COMMAND-D3t5g", "Errors.Project.App.APIConfigInvalid")
}
projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel)
@ -172,12 +232,12 @@ func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID,
err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg)
spanPasswordComparison.EndWithError(err)
if err == nil {
_, err = c.eventstore.Push(ctx, project.NewAPIConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
_, err = c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
return err
}
_, err = c.eventstore.Push(ctx, project.NewAPIConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID))
_, err = c.eventstore.Push(ctx, project_repo.NewAPIConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID))
logging.Log("COMMAND-g3f12").OnError(err).Error("could not push event APIClientSecretCheckFailed")
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid")
return errors.ThrowInvalidArgument(nil, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid")
}
func (c *Commands) getAPIAppWriteModel(ctx context.Context, projectID, appID, resourceOwner string) (*APIApplicationWriteModel, error) {

View File

@ -4,9 +4,10 @@ import (
"context"
"testing"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
@ -16,6 +17,118 @@ import (
"github.com/stretchr/testify/assert"
)
func TestAddAPIConfig(t *testing.T) {
type args struct {
a *project.Aggregate
appID string
name string
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid appID",
args: args{
a: agg,
appID: "",
name: "name",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument"),
},
},
{
name: "invalid name",
args: args{
a: agg,
appID: "appID",
name: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument"),
},
},
{
name: "project not exists",
args: args{
a: agg,
appID: "id",
name: "name",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "PROJE-Sf2gb", "Errors.Project.NotFound"),
},
},
{
name: "correct without client secret",
args: args{
a: agg,
appID: "appID",
name: "name",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
ctx,
&agg.Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&agg.Aggregate,
"appID",
"name",
),
project.NewAPIConfigAddedEvent(ctx, &agg.Aggregate,
"appID",
"",
nil,
domain.APIAuthMethodTypePrivateKeyJWT,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t,
AddAPIAppCommand(
&addAPIApp{
AddApp: AddApp{
Aggregate: *tt.args.a,
ID: tt.args.appID,
Name: tt.args.name,
},
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
},
nil,
), tt.args.filter, tt.want)
})
}
}
func TestCommandSide_AddAPIApplication(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
@ -50,7 +163,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -73,7 +186,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -103,7 +216,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -295,7 +408,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -318,7 +431,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -341,7 +454,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -381,7 +494,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -504,7 +617,7 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -521,7 +634,7 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -539,7 +652,7 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{

View File

@ -2,17 +2,121 @@ package command
import (
"context"
"strings"
"time"
"github.com/caos/logging"
http_util "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
"github.com/caos/zitadel/internal/id"
project_repo "github.com/caos/zitadel/internal/repository/project"
"github.com/caos/zitadel/internal/telemetry/tracing"
)
type addOIDCApp struct {
AddApp
Version domain.OIDCVersion
RedirectUris []string
ResponseTypes []domain.OIDCResponseType
GrantTypes []domain.OIDCGrantType
ApplicationType domain.OIDCApplicationType
AuthMethodType domain.OIDCAuthMethodType
PostLogoutRedirectUris []string
DevMode bool
AccessTokenType domain.OIDCTokenType
AccessTokenRoleAssertion bool
IDTokenRoleAssertion bool
IDTokenUserinfoAssertion bool
ClockSkew time.Duration
AdditionalOrigins []string
ClientID string
ClientSecret *crypto.CryptoValue
ClientSecretPlain string
}
//AddOIDCAppCommand prepares the commands to add an oidc app. The ClientID will be set during the CreateCommands
func AddOIDCAppCommand(app *addOIDCApp, clientSecretAlg crypto.HashAlgorithm) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if app.ID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument")
}
if app.Name = strings.TrimSpace(app.Name); app.Name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument")
}
if app.ClockSkew > time.Second*5 || app.ClockSkew < 0 {
return nil, errors.ThrowInvalidArgument(nil, "V2-PnCMS", "Errors.Invalid.Argument")
}
for _, origin := range app.AdditionalOrigins {
if !http_util.IsOrigin(origin) {
return nil, errors.ThrowInvalidArgument(nil, "V2-DqWPX", "Errors.Invalid.Argument")
}
}
if !domain.ContainsRequiredGrantTypes(app.ResponseTypes, app.GrantTypes) {
return nil, errors.ThrowInvalidArgument(nil, "V2-sLpW1", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) {
project, err := projectWriteModel(ctx, filter, app.Aggregate.ID, app.Aggregate.ResourceOwner)
if err != nil || !project.State.Valid() {
return nil, errors.ThrowNotFound(err, "PROJE-6swVG", "Errors.Project.NotFound")
}
app.ClientID, err = domain.NewClientID(id.SonyFlakeGenerator, project.Name)
if err != nil {
return nil, errors.ThrowInternal(err, "V2-VMSQ1", "Errors.Internal")
}
if app.AuthMethodType == domain.OIDCAuthMethodTypeBasic || app.AuthMethodType == domain.OIDCAuthMethodTypePost {
app.ClientSecret, app.ClientSecretPlain, err = newAppClientSecret(ctx, filter, clientSecretAlg)
if err != nil {
return nil, err
}
}
return []eventstore.Command{
project_repo.NewApplicationAddedEvent(
ctx,
&app.Aggregate.Aggregate,
app.ID,
app.Name,
),
project_repo.NewOIDCConfigAddedEvent(
ctx,
&app.Aggregate.Aggregate,
app.Version,
app.ID,
app.ClientID,
app.ClientSecret,
app.RedirectUris,
app.ResponseTypes,
app.GrantTypes,
app.ApplicationType,
app.AuthMethodType,
app.PostLogoutRedirectUris,
app.DevMode,
app.AccessTokenType,
app.AccessTokenRoleAssertion,
app.IDTokenRoleAssertion,
app.IDTokenUserinfoAssertion,
app.ClockSkew,
app.AdditionalOrigins,
),
}, nil
}, nil
}
}
func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.OIDCApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.OIDCApp, err error) {
if application == nil || application.AggregateID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Application.Invalid")
@ -52,7 +156,7 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor
}
events = []eventstore.Command{
project.NewApplicationAddedEvent(ctx, projectAgg, oidcApp.AppID, oidcApp.AppName),
project_repo.NewApplicationAddedEvent(ctx, projectAgg, oidcApp.AppID, oidcApp.AppName),
}
var stringPw string
@ -64,7 +168,7 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor
if err != nil {
return nil, "", err
}
events = append(events, project.NewOIDCConfigAddedEvent(ctx,
events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx,
projectAgg,
oidcApp.OIDCVersion,
oidcApp.AppID,
@ -164,7 +268,7 @@ func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, a
projectAgg := ProjectAggregateFromWriteModel(&existingOIDC.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, project.NewOIDCConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret))
pushedEvents, err := c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretChangedEvent(ctx, projectAgg, appID, cryptoSecret))
if err != nil {
return nil, err
}
@ -201,10 +305,10 @@ func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID,
err = crypto.CompareHash(app.ClientSecret, []byte(secret), c.userPasswordAlg)
spanPasswordComparison.EndWithError(err)
if err == nil {
_, err = c.eventstore.Push(ctx, project.NewOIDCConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
_, err = c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretCheckSucceededEvent(ctx, projectAgg, app.AppID))
return err
}
_, err = c.eventstore.Push(ctx, project.NewOIDCConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID))
_, err = c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID))
logging.Log("COMMAND-ADfhz").OnError(err).Error("could not push event OIDCClientSecretCheckFailed")
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid")
}

View File

@ -7,9 +7,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
"github.com/caos/zitadel/internal/eventstore/v1/models"
@ -18,6 +19,162 @@ import (
"github.com/caos/zitadel/internal/repository/project"
)
func TestAddOIDCApp(t *testing.T) {
type args struct {
app *addOIDCApp
clientSecretAlg crypto.HashAlgorithm
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid appID",
args: args{
app: &addOIDCApp{
AddApp: AddApp{
Aggregate: *agg,
ID: "",
Name: "name",
},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
Version: domain.OIDCVersionV1,
ApplicationType: domain.OIDCApplicationTypeWeb,
AuthMethodType: domain.OIDCAuthMethodTypeNone,
AccessTokenType: domain.OIDCTokenTypeBearer,
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument"),
},
},
{
name: "invalid name",
args: args{
app: &addOIDCApp{
AddApp: AddApp{
Aggregate: *agg,
ID: "id",
Name: "",
},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
Version: domain.OIDCVersionV1,
ApplicationType: domain.OIDCApplicationTypeWeb,
AuthMethodType: domain.OIDCAuthMethodTypeNone,
AccessTokenType: domain.OIDCTokenTypeBearer,
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument"),
},
},
{
name: "project not exists",
args: args{
app: &addOIDCApp{
AddApp: AddApp{
Aggregate: *agg,
ID: "id",
Name: "name",
},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
Version: domain.OIDCVersionV1,
ApplicationType: domain.OIDCApplicationTypeWeb,
AuthMethodType: domain.OIDCAuthMethodTypeNone,
AccessTokenType: domain.OIDCTokenTypeBearer,
},
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "PROJE-6swVG", ""),
},
},
{
name: "correct",
args: args{
app: &addOIDCApp{
AddApp: AddApp{
Aggregate: *agg,
ID: "id",
Name: "name",
},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
Version: domain.OIDCVersionV1,
ApplicationType: domain.OIDCApplicationTypeWeb,
AuthMethodType: domain.OIDCAuthMethodTypeNone,
AccessTokenType: domain.OIDCTokenTypeBearer,
},
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
ctx,
&agg.Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
project.NewApplicationAddedEvent(ctx, &agg.Aggregate,
"id",
"name",
),
project.NewOIDCConfigAddedEvent(ctx, &agg.Aggregate,
domain.OIDCVersionV1,
"id",
"",
nil,
nil,
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeNone,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t,
AddOIDCAppCommand(
tt.args.app,
tt.args.clientSecretAlg,
), tt.args.filter, tt.want)
})
}
}
func TestCommandSide_AddOIDCApplication(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
@ -52,7 +209,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -75,7 +232,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -105,7 +262,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -274,7 +431,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -298,7 +455,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -322,7 +479,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -347,7 +504,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{
@ -418,7 +575,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsPreconditionFailed,
err: errors.IsPreconditionFailed,
},
},
{
@ -580,7 +737,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -597,7 +754,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
err: errors.IsErrorInvalidArgument,
},
},
{
@ -615,7 +772,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) {
resourceOwner: "org1",
},
res: res{
err: caos_errs.IsNotFound,
err: errors.IsNotFound,
},
},
{

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
@ -985,3 +986,186 @@ func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldNa
)
return event
}
func TestAddProject(t *testing.T) {
type args struct {
a *project.Aggregate
name string
owner string
privateLabelingSetting domain.PrivateLabelingSetting
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid name",
args: args{
a: agg,
name: "",
owner: "owner",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument"),
},
},
{
name: "invalid private labeling setting",
args: args{
a: agg,
name: "name",
owner: "owner",
privateLabelingSetting: -1,
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument"),
},
},
{
name: "invalid owner",
args: args{
a: agg,
name: "name",
owner: "",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
ValidationErr: errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: agg,
name: "ZITADEL",
owner: "CAOS AG",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
Commands: []eventstore.Command{
project.NewProjectAddedEvent(ctx, &agg.Aggregate,
"ZITADEL",
false,
false,
false,
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
),
project.NewProjectMemberAddedEvent(ctx, &agg.Aggregate,
"CAOS AG",
domain.RoleProjectOwner),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddProjectCommand(tt.args.a, tt.args.name, tt.args.owner, false, false, false, tt.args.privateLabelingSetting), nil, tt.want)
})
}
}
// func TestExistsProject(t *testing.T) {
// type args struct {
// filter preparation.FilterToQueryReducer
// id string
// resourceOwner string
// }
// tests := []struct {
// name string
// args args
// wantExists bool
// wantErr bool
// }{
// {
// name: "no events",
// args: args{
// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
// return []eventstore.Event{}, nil
// },
// id: "id",
// resourceOwner: "ro",
// },
// wantExists: false,
// wantErr: false,
// },
// {
// name: "project added",
// args: args{
// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
// return []eventstore.Event{
// project.NewProjectAddedEvent(
// context.Background(),
// &project.NewAggregate("id", "ro").Aggregate,
// "name",
// false,
// false,
// false,
// domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy,
// ),
// }, nil
// },
// id: "id",
// resourceOwner: "ro",
// },
// wantExists: true,
// wantErr: false,
// },
// {
// name: "project removed",
// args: args{
// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
// return []eventstore.Event{
// project.NewProjectAddedEvent(
// context.Background(),
// &project.NewAggregate("id", "ro").Aggregate,
// "name",
// false,
// false,
// false,
// domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy,
// ),
// project.NewProjectRemovedEvent(
// context.Background(),
// &project.NewAggregate("id", "ro").Aggregate,
// "name",
// ),
// }, nil
// },
// id: "id",
// resourceOwner: "ro",
// },
// wantExists: false,
// wantErr: false,
// },
// {
// name: "error durring filter",
// args: args{
// filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
// return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
// },
// id: "id",
// resourceOwner: "ro",
// },
// wantExists: false,
// wantErr: true,
// },
// }
// for _, tt := range tests {
// t.Run(tt.name, func(t *testing.T) {
// gotExists, err := projectWriteModel(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner)
// if (err != nil) != tt.wantErr {
// t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
// return
// }
// if gotExists != tt.wantExists {
// t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
// }
// })
// }
// }

View File

@ -0,0 +1,73 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func (c *commandNew) AddSecretGeneratorConfig(ctx context.Context, typ domain.SecretGeneratorType, config *crypto.GeneratorConfig) (*domain.ObjectDetails, error) {
agg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID())
cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, addSecretGeneratorConfig(agg, typ, config))
if err != nil {
return nil, err
}
events, err := c.es.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: agg.ResourceOwner,
}, nil
}
//AddOrg defines the commands to create a new org,
// this includes the verified default domain
func addSecretGeneratorConfig(a *instance.Aggregate, typ domain.SecretGeneratorType, config *crypto.GeneratorConfig) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if !typ.Valid() {
return nil, errors.ThrowInvalidArgument(nil, "V2-FGqVj", "Errors.InvalidArgument")
}
if config.Length < 1 {
return nil, errors.ThrowInvalidArgument(nil, "V2-jEqCt", "Errors.InvalidArgument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel := NewInstanceSecretGeneratorConfigWriteModel(ctx, typ)
events, err := filter(ctx, writeModel.Query())
if err != nil {
return nil, err
}
writeModel.AppendEvents(events...)
if err = writeModel.Reduce(); err != nil {
return nil, err
}
if writeModel.State == domain.SecretGeneratorStateActive {
return nil, errors.ThrowAlreadyExists(nil, "V2-6CqKo", "Errors.SecretGenerator.AlreadyExists")
}
return []eventstore.Command{
instance.NewSecretGeneratorAddedEvent(
ctx,
&a.Aggregate,
typ,
config.Length,
config.Expiry,
config.IncludeLowerLetters,
config.IncludeUpperLetters,
config.IncludeDigits,
config.IncludeSymbols,
),
}, nil
}, nil
}
}

View File

@ -5,15 +5,15 @@ import (
"fmt"
"time"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/repository/user"
"github.com/caos/zitadel/internal/telemetry/tracing"
)
@ -378,3 +378,38 @@ func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner
}
return writeModel, nil
}
func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(id).
EventTypes(
user.HumanRegisteredType,
user.UserV1RegisteredType,
user.HumanAddedType,
user.UserV1AddedType,
user.MachineAddedEventType,
user.UserRemovedType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch event.(type) {
case *user.HumanRegisteredEvent, *user.HumanAddedEvent, *user.MachineAddedEvent:
exists = true
case *user.UserRemovedEvent:
exists = false
}
}
return exists, nil
}
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
}

View File

@ -4,12 +4,11 @@ import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/errors"
)
func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) {
func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*PolicyDomainWriteModel, error) {
wm, err := orgDomainPolicy(ctx, filter)
if err != nil || wm != nil && wm.State.Exists() {
return wm, err
@ -21,8 +20,8 @@ func domainPolicyWriteModel(ctx context.Context, filter preparation.FilterToQuer
return nil, errors.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")
}
func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) {
policy := command.NewOrgDomainPolicyWriteModel(authz.GetCtxData(ctx).OrgID)
func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PolicyDomainWriteModel, error) {
policy := NewOrgDomainPolicyWriteModel(authz.GetCtxData(ctx).OrgID)
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err
@ -35,8 +34,8 @@ func orgDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReduce
return &policy.PolicyDomainWriteModel, err
}
func instanceDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PolicyDomainWriteModel, error) {
policy := command.NewInstanceDomainPolicyWriteModel(ctx)
func instanceDomainPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PolicyDomainWriteModel, error) {
policy := NewInstanceDomainPolicyWriteModel(ctx)
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err

View File

@ -6,8 +6,7 @@ import (
"testing"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@ -22,7 +21,7 @@ func Test_customDomainPolicy(t *testing.T) {
tests := []struct {
name string
args args
want *command.PolicyDomainWriteModel
want *PolicyDomainWriteModel
wantErr bool
}{
{
@ -58,7 +57,7 @@ func Test_customDomainPolicy(t *testing.T) {
}, nil
},
},
want: &command.PolicyDomainWriteModel{
want: &PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
@ -91,7 +90,7 @@ func Test_defaultDomainPolicy(t *testing.T) {
tests := []struct {
name string
args args
want *command.PolicyDomainWriteModel
want *PolicyDomainWriteModel
wantErr bool
}{
{
@ -127,7 +126,7 @@ func Test_defaultDomainPolicy(t *testing.T) {
}, nil
},
},
want: &command.PolicyDomainWriteModel{
want: &PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",
@ -160,7 +159,7 @@ func Test_DomainPolicy(t *testing.T) {
tests := []struct {
name string
args args
want *command.PolicyDomainWriteModel
want *PolicyDomainWriteModel
wantErr bool
}{
{
@ -186,7 +185,7 @@ func Test_DomainPolicy(t *testing.T) {
}, nil
},
},
want: &command.PolicyDomainWriteModel{
want: &PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
@ -230,7 +229,7 @@ func Test_DomainPolicy(t *testing.T) {
}).
Filter(),
},
want: &command.PolicyDomainWriteModel{
want: &PolicyDomainWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",

View File

@ -4,13 +4,14 @@ import (
"context"
"strings"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/repository/user"
"golang.org/x/text/language"
)
func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (*domain.Human, error) {
@ -19,51 +20,258 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (
return nil, err
}
if !isUserStateExists(human.UserState) {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-M9dsd", "Errors.User.NotFound")
return nil, errors.ThrowNotFound(nil, "COMMAND-M9dsd", "Errors.User.NotFound")
}
return writeModelToHuman(human), nil
}
func (c *Commands) AddHuman(ctx context.Context, orgID string, human *domain.Human, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-XYFk9", "Errors.ResourceOwnerMissing")
type AddHuman struct {
// Username is required
Username string
// FirstName is required
FirstName string
// LastName is required
LastName string
// NickName is required
NickName string
// DisplayName is required
DisplayName string
// Email is required
Email Email
// PreferredLang is required
PreferredLang language.Tag
// Gender is required
Gender domain.Gender
//Phone represents an international phone number
Phone Phone
//Password is optional
Password string
//PasswordChangeRequired is used if the `Password`-field is set
PasswordChangeRequired bool
Passwordless bool
ExternalIDP bool
Register bool
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.DomainPolicy.NotFound")
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) {
return c.v2.AddHuman(ctx, resourceOwner, human)
}
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexityPolicy.NotFound")
func (c *commandNew) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) {
if resourceOwner == "" {
return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
}
events, addedHuman, err := c.addHuman(ctx, orgID, human, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator)
userID, err := c.id.Next()
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, events...)
agg := user.NewAggregate(userID, resourceOwner)
cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, addHumanCommand(agg, human, c.userPasswordAlg, c.phoneAlg, c.emailAlg, c.initCodeAlg))
if err != nil {
return nil, err
}
err = AppendAndReduce(addedHuman, pushedEvents...)
events, err := c.es.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return writeModelToHuman(addedHuman), nil
return &domain.HumanDetails{
ID: userID,
ObjectDetails: domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
},
}, nil
}
type humanCreationCommand interface {
eventstore.Command
AddPhoneData(phoneNumber string)
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
}
func addHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, phoneAlg, emailAlg, initCodeAlg crypto.EncryptionAlgorithm) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if !human.Email.Valid() {
return nil, errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument")
}
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
}
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument")
}
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument")
}
human.ensureDisplayName()
if human.Phone.Number, err = FormatPhoneNumber(human.Phone.Number); err != nil {
return nil, errors.ThrowInvalidArgument(nil, "USER-tD6ax", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
domainPolicy, err := domainPolicyWriteModel(ctx, filter)
if err != nil {
return nil, err
}
if err = userValidateDomain(ctx, a, human.Username, domainPolicy.UserLoginMustBeDomain, filter); err != nil {
return nil, err
}
var createCmd humanCreationCommand
if human.Register {
createCmd = user.NewHumanRegisteredEvent(
ctx,
&a.Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLang,
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain,
)
} else {
createCmd = user.NewHumanAddedEvent(
ctx,
&a.Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLang,
human.Gender,
human.Email.Address,
domainPolicy.UserLoginMustBeDomain,
)
}
if human.Phone.Number != "" {
createCmd.AddPhoneData(human.Phone.Number)
}
if human.Password != "" {
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
return nil, err
}
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
if err != nil {
return nil, err
}
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
}
cmds := make([]eventstore.Command, 0, 3)
cmds = append(cmds, createCmd)
//add init code if
// email not verified or
// user not registered and password set
if human.shouldAddInitCode() {
value, expiry, err := newUserInitCode(ctx, filter, initCodeAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
}
if human.Email.Verified {
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
} else {
value, expiry, err := newEmailCode(ctx, filter, emailAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
}
if human.Phone.Verified {
cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate))
} else if human.Phone.Number != "" {
value, expiry, err := newPhoneCode(ctx, filter, phoneAlg)
if err != nil {
return nil, err
}
cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
}
return cmds, nil
}, nil
}
}
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
if mustBeDomain {
return nil
}
usernameSplit := strings.Split(username, "@")
if len(usernameSplit) != 2 {
return errors.ThrowInvalidArgument(nil, "COMMAND-Dfd21", "Errors.User.Invalid")
}
domainCheck := NewOrgDomainVerifiedWriteModel(usernameSplit[1])
events, err := filter(ctx, domainCheck.Query())
if err != nil {
return err
}
domainCheck.AppendEvents(events...)
if err = domainCheck.Reduce(); err != nil {
return err
}
if domainCheck.Verified && domainCheck.ResourceOwner != a.ResourceOwner {
return errors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")
}
return nil
}
func humanValidatePassword(ctx context.Context, filter preparation.FilterToQueryReducer, password string) error {
passwordComplexity, err := passwordComplexityPolicyWriteModel(ctx, filter)
if err != nil {
return err
}
return passwordComplexity.Validate(password)
}
func (h *AddHuman) ensureDisplayName() {
if strings.TrimSpace(h.DisplayName) != "" {
return
}
h.DisplayName = h.FirstName + " " + h.LastName
}
func (h *AddHuman) shouldAddInitCode() bool {
//user without idp
return !h.Email.Verified ||
//user with idp
!h.ExternalIDP &&
!h.Passwordless &&
h.Password != ""
}
func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) {
if orgID == "" {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-5N8fs", "Errors.ResourceOwnerMissing")
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
if err != nil {
return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.DomainPolicy.NotFound")
return nil, nil, errors.ThrowPreconditionFailed(err, "COMMAND-2N9fs", "Errors.Org.DomainPolicy.NotFound")
}
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID)
if err != nil {
return nil, nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexityPolicy.NotFound")
return nil, nil, errors.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexityPolicy.NotFound")
}
events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator)
if err != nil {
@ -89,53 +297,24 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.
return writeModelToHuman(addedHuman), passwordlessCode, nil
}
func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) {
if orgID == "" || !human.IsValid() {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-67Ms8", "Errors.User.Invalid")
}
if human.Password != nil && human.SecretString != "" {
human.ChangeRequired = true
}
return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator)
}
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
if orgID == "" || !human.IsValid() {
return nil, nil, nil, "", caos_errs.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid")
}
events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator)
if err != nil {
return nil, nil, nil, "", err
}
if passwordless {
var codeEvent eventstore.Command
codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator)
if err != nil {
return nil, nil, nil, "", err
}
events = append(events, codeEvent)
}
return events, humanWriteModel, passwordlessCodeWriteModel, code, nil
}
func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, orgMemberRoles []string, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (*domain.Human, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-GEdf2", "Errors.ResourceOwnerMissing")
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-GEdf2", "Errors.ResourceOwnerMissing")
}
domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.DomainPolicy.NotFound")
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-33M9f", "Errors.Org.DomainPolicy.NotFound")
}
pwPolicy, err := c.getOrgPasswordComplexityPolicy(ctx, orgID)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexityPolicy.NotFound")
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-M5Fsd", "Errors.Org.PasswordComplexityPolicy.NotFound")
}
loginPolicy, err := c.getOrgLoginPolicy(ctx, orgID)
if err != nil {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-Dfg3g", "Errors.Org.LoginPolicy.NotFound")
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-Dfg3g", "Errors.Org.LoginPolicy.NotFound")
}
if !loginPolicy.AllowRegister {
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-SAbr3", "Errors.Org.LoginPolicy.RegistrationNotAllowed")
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-SAbr3", "Errors.Org.LoginPolicy.RegistrationNotAllowed")
}
userEvents, registeredHuman, err := c.registerHuman(ctx, orgID, human, link, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator)
if err != nil {
@ -171,12 +350,41 @@ func (c *Commands) RegisterHuman(ctx context.Context, orgID string, human *domai
return writeModelToHuman(registeredHuman), nil
}
func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Human, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) {
if orgID == "" || !human.IsValid() {
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-67Ms8", "Errors.User.Invalid")
}
if human.Password != nil && human.SecretString != "" {
human.ChangeRequired = true
}
return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator)
}
func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) {
if orgID == "" || !human.IsValid() {
return nil, nil, nil, "", errors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Invalid")
}
events, humanWriteModel, err = c.createHuman(ctx, orgID, human, nil, false, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator)
if err != nil {
return nil, nil, nil, "", err
}
if passwordless {
var codeEvent eventstore.Command
codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator)
if err != nil {
return nil, nil, nil, "", err
}
events = append(events, codeEvent)
}
return events, humanWriteModel, passwordlessCodeWriteModel, code, nil
}
func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) {
if human != nil && human.Username == "" {
human.Username = human.EmailAddress
}
if orgID == "" || !human.IsValid() || link == nil && (human.Password == nil || human.SecretString == "") {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid")
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid")
}
if human.Password != nil && human.SecretString != "" {
human.ChangeRequired = false
@ -184,21 +392,21 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai
return c.createHuman(ctx, orgID, human, link, true, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator)
}
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) ([]eventstore.Command, *HumanWriteModel, error) {
func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, link *domain.UserIDPLink, selfregister, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator crypto.Generator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) {
if err := human.CheckDomainPolicy(domainPolicy); err != nil {
return nil, nil, err
}
if !domainPolicy.UserLoginMustBeDomain {
usernameSplit := strings.Split(human.Username, "@")
if len(usernameSplit) != 2 {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-Dfd21", "Errors.User.Invalid")
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-Dfd21", "Errors.User.Invalid")
}
domainCheck := NewOrgDomainVerifiedWriteModel(usernameSplit[1])
if err := c.eventstore.FilterToQueryReducer(ctx, domainCheck); err != nil {
return nil, nil, err
}
if domainCheck.Verified && domainCheck.ResourceOwner != orgID {
return nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")
return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")
}
}
userID, err := c.idGenerator.Next()
@ -213,10 +421,9 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
}
}
addedHuman := NewHumanWriteModel(human.AggregateID, orgID)
addedHuman = NewHumanWriteModel(human.AggregateID, orgID)
//TODO: adlerhurst maybe we could simplify the code below
userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel)
var events []eventstore.Command
if selfregister {
events = append(events, createRegisterHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain))
@ -259,7 +466,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.
func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) {
if userID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing")
return errors.ThrowInvalidArgument(nil, "COMMAND-2xpX9", "Errors.User.UserIDMissing")
}
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceowner)
@ -267,7 +474,7 @@ func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner s
return err
}
if !isUserStateExists(existingHuman.UserState) {
return caos_errs.ThrowNotFound(nil, "COMMAND-m9cV8", "Errors.User.NotFound")
return errors.ThrowNotFound(nil, "COMMAND-m9cV8", "Errors.User.NotFound")
}
_, err = c.eventstore.Push(ctx,
@ -340,10 +547,10 @@ func createRegisterHumanEvent(ctx context.Context, aggregate *eventstore.Aggrega
func (c *Commands) HumansSignOut(ctx context.Context, agentID string, userIDs []string) error {
if agentID == "" {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
return errors.ThrowInvalidArgument(nil, "COMMAND-2M0ds", "Errors.User.UserIDMissing")
}
if len(userIDs) == 0 {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-M0od3", "Errors.User.UserIDMissing")
return errors.ThrowInvalidArgument(nil, "COMMAND-M0od3", "Errors.User.UserIDMissing")
}
events := make([]eventstore.Command, 0)
for _, userID := range userIDs {

View File

@ -71,7 +71,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) {
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
PhoneNumber: "0711234567",
PhoneNumber: "+41711234567",
},
resourceOwner: "org1",
},
@ -114,7 +114,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) {
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
PhoneNumber: "0711234567",
PhoneNumber: "+41711234567",
},
resourceOwner: "org1",
},
@ -172,7 +172,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) {
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
PhoneNumber: "0719876543",
PhoneNumber: "+41719876543",
IsPhoneVerified: true,
},
resourceOwner: "org1",
@ -239,7 +239,7 @@ func TestCommandSide_ChangeHumanPhone(t *testing.T) {
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
PhoneNumber: "0711234567",
PhoneNumber: "+41711234567",
},
resourceOwner: "org1",
secretGenerator: GetMockSecretGenerator(t),

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,11 @@ import (
"context"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/errors"
)
func passwordComplexityPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) {
func passwordComplexityPolicyWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer) (*PasswordComplexityPolicyWriteModel, error) {
wm, err := customPasswordComplexityPolicy(ctx, filter)
if err != nil || wm != nil && wm.State.Exists() {
return wm, err
@ -21,8 +20,8 @@ func passwordComplexityPolicyWriteModel(ctx context.Context, filter preparation.
return nil, errors.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal")
}
func customPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) {
policy := command.NewOrgPasswordComplexityPolicyWriteModel(authz.GetCtxData(ctx).OrgID)
func customPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PasswordComplexityPolicyWriteModel, error) {
policy := NewOrgPasswordComplexityPolicyWriteModel(authz.GetCtxData(ctx).OrgID)
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err
@ -35,8 +34,8 @@ func customPasswordComplexityPolicy(ctx context.Context, filter preparation.Filt
return &policy.PasswordComplexityPolicyWriteModel, err
}
func defaultPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.PasswordComplexityPolicyWriteModel, error) {
policy := command.NewInstancePasswordComplexityPolicyWriteModel(ctx)
func defaultPasswordComplexityPolicy(ctx context.Context, filter preparation.FilterToQueryReducer) (*PasswordComplexityPolicyWriteModel, error) {
policy := NewInstancePasswordComplexityPolicyWriteModel(ctx)
events, err := filter(ctx, policy.Query())
if err != nil {
return nil, err

View File

@ -6,8 +6,7 @@ import (
"testing"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
@ -22,7 +21,7 @@ func Test_customPasswordComplexityPolicy(t *testing.T) {
tests := []struct {
name string
args args
want *command.PasswordComplexityPolicyWriteModel
want *PasswordComplexityPolicyWriteModel
wantErr bool
}{
{
@ -62,7 +61,7 @@ func Test_customPasswordComplexityPolicy(t *testing.T) {
}, nil
},
},
want: &command.PasswordComplexityPolicyWriteModel{
want: &PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
@ -99,7 +98,7 @@ func Test_defaultPasswordComplexityPolicy(t *testing.T) {
tests := []struct {
name string
args args
want *command.PasswordComplexityPolicyWriteModel
want *PasswordComplexityPolicyWriteModel
wantErr bool
}{
{
@ -139,7 +138,7 @@ func Test_defaultPasswordComplexityPolicy(t *testing.T) {
}, nil
},
},
want: &command.PasswordComplexityPolicyWriteModel{
want: &PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",
@ -176,7 +175,7 @@ func Test_passwordComplexityPolicy(t *testing.T) {
tests := []struct {
name string
args args
want *command.PasswordComplexityPolicyWriteModel
want *PasswordComplexityPolicyWriteModel
wantErr bool
}{
{
@ -206,7 +205,7 @@ func Test_passwordComplexityPolicy(t *testing.T) {
}, nil
},
},
want: &command.PasswordComplexityPolicyWriteModel{
want: &PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "id",
ResourceOwner: "ro",
@ -258,7 +257,7 @@ func Test_passwordComplexityPolicy(t *testing.T) {
}).
Filter(),
},
want: &command.PasswordComplexityPolicyWriteModel{
want: &PasswordComplexityPolicyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: "INSTANCE",
ResourceOwner: "INSTANCE",

View File

@ -11,7 +11,9 @@ import (
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/repository"
@ -1577,3 +1579,155 @@ func TestCommandSide_UserDomainClaimedSent(t *testing.T) {
})
}
}
func TestExistsUser(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
id string
resourceOwner string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "human registered",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewHumanRegisteredEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"firstName",
"lastName",
"nickName",
"displayName",
language.German,
domain.GenderFemale,
"support@zitadel.ch",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "human added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewHumanAddedEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"firstName",
"lastName",
"nickName",
"displayName",
language.German,
domain.GenderFemale,
"support@zitadel.ch",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "machine added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "user removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
context.Background(),
&user.NewAggregate("removed", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
user.NewUserRemovedEvent(
context.Background(),
&user.NewAggregate("removed", "ro").Aggregate,
"userName",
nil,
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-Drebn", "Errors.Internal")
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}

View File

@ -1,36 +0,0 @@
package command
import (
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/action"
iam_repo "github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/repository/keypair"
"github.com/caos/zitadel/internal/repository/org"
proj_repo "github.com/caos/zitadel/internal/repository/project"
usr_repo "github.com/caos/zitadel/internal/repository/user"
usr_grant_repo "github.com/caos/zitadel/internal/repository/usergrant"
)
type Command struct {
es *eventstore.Eventstore
userPasswordAlg crypto.HashAlgorithm
iamDomain string
}
func New(es *eventstore.Eventstore, iamDomain string, defaults sd.SystemDefaults) *Command {
iam_repo.RegisterEventMappers(es)
org.RegisterEventMappers(es)
usr_repo.RegisterEventMappers(es)
usr_grant_repo.RegisterEventMappers(es)
proj_repo.RegisterEventMappers(es)
keypair.RegisterEventMappers(es)
action.RegisterEventMappers(es)
return &Command{
es: es,
iamDomain: iamDomain,
userPasswordAlg: crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost),
}
}

View File

@ -1,95 +0,0 @@
package command
import (
"context"
"time"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func SetDefaultFeatures(
a *instance.Aggregate,
tierName,
tierDescription string,
state domain.FeaturesState,
stateDescription string,
retention time.Duration,
loginPolicyFactors,
loginPolicyIDP,
loginPolicyPasswordless,
loginPolicyRegistration,
loginPolicyUsernameLogin,
loginPolicyPasswordReset,
passwordComplexityPolicy,
labelPolicyPrivateLabel,
labelPolicyWatermark,
customDomain,
privacyPolicy,
metadataUser,
customTextMessage,
customTextLogin,
lockoutPolicy bool,
actionsAllowed domain.ActionsAllowed,
maxActions int,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if !state.Valid() || state == domain.FeaturesStateUnspecified || state == domain.FeaturesStateRemoved {
return nil, errors.ThrowInvalidArgument(nil, "INSTA-d3r1s", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
writeModel, err := defaultFeatures(ctx, filter)
if err != nil {
return nil, err
}
event, hasChanged := writeModel.NewSetEvent(ctx, &a.Aggregate,
tierName,
tierDescription,
state,
stateDescription,
retention,
loginPolicyFactors,
loginPolicyIDP,
loginPolicyPasswordless,
loginPolicyRegistration,
loginPolicyUsernameLogin,
loginPolicyPasswordReset,
passwordComplexityPolicy,
labelPolicyPrivateLabel,
labelPolicyWatermark,
customDomain,
privacyPolicy,
metadataUser,
customTextMessage,
customTextLogin,
lockoutPolicy,
actionsAllowed,
maxActions,
)
if !hasChanged {
return nil, errors.ThrowPreconditionFailed(nil, "INSTA-GE4h2", "Errors.Features.NotChanged")
}
return []eventstore.Command{
event,
}, nil
}, nil
}
}
func defaultFeatures(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.InstanceFeaturesWriteModel, error) {
features := command.NewInstanceFeaturesWriteModel(ctx)
events, err := filter(ctx, features.Query())
if err != nil {
return nil, err
}
if len(events) == 0 {
return features, nil
}
features.AppendEvents(events...)
err = features.Reduce()
return features, err
}

View File

@ -1,64 +0,0 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/instance"
)
func AddInstanceMember(a *instance.Aggregate, userID string, roles ...string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if userID == "" {
return nil, errors.ThrowInvalidArgument(nil, "INSTA-SDSfs", "Errors.Invalid.Argument")
}
// TODO: check roles
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists {
return nil, errors.ThrowNotFound(err, "INSTA-GSXOn", "Errors.User.NotFound")
}
if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember {
return nil, errors.ThrowAlreadyExists(err, "INSTA-pFDwe", "Errors.Instance.Member.AlreadyExists")
}
return []eventstore.Command{instance.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil
},
nil
}
}
func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReducer, instanceID, userID string) (isMember bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
OrderAsc().
AddQuery().
AggregateIDs(instanceID).
AggregateTypes(instance.AggregateType).
EventTypes(
instance.MemberAddedEventType,
instance.MemberRemovedEventType,
instance.MemberCascadeRemovedEventType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch e := event.(type) {
case *instance.MemberAddedEvent:
if e.UserID == userID {
isMember = true
}
case *instance.MemberRemovedEvent:
if e.UserID == userID {
isMember = false
}
case *instance.MemberCascadeRemovedEvent:
if e.UserID == userID {
isMember = false
}
}
}
return isMember, nil
}

View File

@ -1,72 +0,0 @@
package command
import (
"context"
"strings"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/repository/org"
user_repo "github.com/caos/zitadel/internal/repository/user"
)
type OrgSetup struct {
Name string
Human AddHuman
}
func (command *Command) SetUpOrg(ctx context.Context, o *OrgSetup) (*domain.ObjectDetails, error) {
orgID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
userID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
}
orgAgg := org.NewAggregate(orgID, orgID)
userAgg := user_repo.NewAggregate(userID, orgID)
cmds, err := preparation.PrepareCommands(ctx, command.es.Filter,
AddOrg(orgAgg, o.Name, command.iamDomain),
AddHumanCommand(userAgg, &o.Human, command.userPasswordAlg),
AddOrgMember(orgAgg, userID, domain.RoleOrgOwner),
)
if err != nil {
return nil, err
}
events, err := command.es.Push(ctx, cmds...)
if err != nil {
return nil, err
}
return &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: orgID,
}, nil
}
//AddOrg defines the commands to create a new org,
// this includes the verified default domain
func AddOrg(a *org.Aggregate, name, iamDomain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument")
}
defaultDomain := domain.NewIAMDomainName(name, iamDomain)
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
org.NewOrgAddedEvent(ctx, &a.Aggregate, name),
org.NewDomainAddedEvent(ctx, &a.Aggregate, defaultDomain),
org.NewDomainVerifiedEvent(ctx, &a.Aggregate, defaultDomain),
org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, defaultDomain),
}, nil
}, nil
}
}

View File

@ -1,46 +0,0 @@
package command
import (
"context"
"strings"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func AddOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{org.NewDomainAddedEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
func VerifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists
return []eventstore.Command{org.NewDomainVerifiedEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}
func SetPrimaryOrgDomain(a *org.Aggregate, domain string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if domain = strings.TrimSpace(domain); domain == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
//TODO: check if already exists and verified
return []eventstore.Command{org.NewDomainPrimarySetEvent(ctx, &a.Aggregate, domain)}, nil
}, nil
}
}

View File

@ -1,133 +0,0 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func TestAddDomain(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
}
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: org.NewAggregate("test", "test"),
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-r3h4J", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: org.NewAggregate("test", "test"),
domain: "domain",
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainAddedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrgDomain(tt.args.a, tt.args.domain), nil, tt.want)
})
}
}
func TestVerifyDomain(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
}
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: org.NewAggregate("test", "test"),
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: org.NewAggregate("test", "test"),
domain: "domain",
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainVerifiedEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, VerifyOrgDomain(tt.args.a, tt.args.domain), nil, tt.want)
})
}
}
func TestSetDomainPrimary(t *testing.T) {
type args struct {
a *org.Aggregate
domain string
}
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: org.NewAggregate("test", "test"),
domain: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: org.NewAggregate("test", "test"),
domain: "domain",
},
want: Want{
Commands: []eventstore.Command{
org.NewDomainPrimarySetEvent(context.Background(), &org.NewAggregate("test", "test").Aggregate, "domain"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, SetPrimaryOrgDomain(tt.args.a, tt.args.domain), nil, tt.want)
})
}
}

View File

@ -1,65 +0,0 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func AddOrgMember(a *org.Aggregate, userID string, roles ...string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if userID == "" {
return nil, errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument")
}
// TODO: check roles
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsUser(ctx, filter, userID, a.ID); err != nil || !exists {
return nil, errors.ThrowNotFound(err, "ORG-GoXOn", "Errors.User.NotFound")
}
if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember {
return nil, errors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists")
}
return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil
},
nil
}
}
func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, orgID, userID string) (isMember bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(orgID).
OrderAsc().
AddQuery().
AggregateIDs(orgID).
AggregateTypes(org.AggregateType).
EventTypes(
org.MemberAddedEventType,
org.MemberRemovedEventType,
org.MemberCascadeRemovedEventType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch e := event.(type) {
case *org.MemberAddedEvent:
if e.UserID == userID {
isMember = true
}
case *org.MemberRemovedEvent:
if e.UserID == userID {
isMember = false
}
case *org.MemberCascadeRemovedEvent:
if e.UserID == userID {
isMember = false
}
}
}
return isMember, nil
}

View File

@ -1,249 +0,0 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/user"
)
func TestAddMember(t *testing.T) {
type args struct {
a *org.Aggregate
userID string
roles []string
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "no user id",
args: args{
a: agg,
userID: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
},
},
// {
// name: "TODO: invalid roles",
// args: args{
// a: agg,
// userID: "",
// roles: []string{""},
// },
// want: preparation.Want{
// ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"),
// },
// },
{
name: "user not exists",
args: args{
a: agg,
userID: "userID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "ORG-GoXOn", "Errors.User.NotFound"),
},
},
{
name: "already member",
args: args{
a: agg,
userID: "userID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
ctx,
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
ctx,
&org.NewAggregate("id", "ro").Aggregate,
"userID",
),
}, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowAlreadyExists(nil, "ORG-poWwe", "Errors.Org.Member.AlreadyExists"),
},
},
{
name: "correct",
args: args{
a: agg,
userID: "userID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
ctx,
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
}).
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrgMember(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want)
})
}
}
func TestIsMember(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
orgID string
userID string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "member added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: true,
wantErr: false,
},
{
name: "member removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
org.NewMemberRemovedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "member cascade removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewMemberAddedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
org.NewMemberCascadeRemovedEvent(
context.Background(),
&org.NewAggregate("orgID", "ro").Aggregate,
"userID",
),
}, nil
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
},
orgID: "orgID",
userID: "userID",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := IsOrgMember(context.Background(), tt.args.filter, tt.args.orgID, tt.args.userID)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}

View File

@ -1,57 +0,0 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
)
func TestAddOrg(t *testing.T) {
type args struct {
a *org.Aggregate
name string
}
ctx := context.Background()
agg := org.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid domain",
args: args{
a: agg,
name: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: agg,
name: "caos ag",
},
want: Want{
Commands: []eventstore.Command{
org.NewOrgAddedEvent(ctx, &agg.Aggregate, "caos ag"),
org.NewDomainAddedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
org.NewDomainVerifiedEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
org.NewDomainPrimarySetEvent(ctx, &agg.Aggregate, "caos-ag.localhost"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddOrg(tt.args.a, tt.args.name, "localhost"), nil, tt.want)
})
}
}

View File

@ -1,75 +0,0 @@
package command
import (
"context"
"strings"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func AddProject(
a *project.Aggregate,
name string,
owner string,
projectRoleAssertion bool,
projectRoleCheck bool,
hasProjectCheck bool,
privateLabelingSetting domain.PrivateLabelingSetting,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument")
}
if !privateLabelingSetting.Valid() {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument")
}
if owner == "" {
return nil, errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
project.NewProjectAddedEvent(ctx, &a.Aggregate,
name,
projectRoleAssertion,
projectRoleCheck,
hasProjectCheck,
privateLabelingSetting,
),
project.NewProjectMemberAddedEvent(ctx, &a.Aggregate,
owner,
domain.RoleProjectOwner),
}, nil
}, nil
}
}
func ExistsProject(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, resourceOwner string) (exists bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(projectID).
EventTypes(
project.ProjectAddedType,
project.ProjectRemovedType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch event.(type) {
case *project.ProjectAddedEvent:
exists = true
case *project.ProjectRemovedEvent:
exists = false
}
}
return exists, nil
}

View File

@ -1,155 +0,0 @@
package command
import (
"context"
"strings"
"time"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func AddOIDCApp(
a project.Aggregate,
version domain.OIDCVersion,
appID,
name,
clientID string,
clientSecret *crypto.CryptoValue,
redirectUris []string,
responseTypes []domain.OIDCResponseType,
grantTypes []domain.OIDCGrantType,
applicationType domain.OIDCApplicationType,
authMethodType domain.OIDCAuthMethodType,
postLogoutRedirectUris []string,
devMode bool,
accessTokenType domain.OIDCTokenType,
accessTokenRoleAssertion bool,
idTokenRoleAssertion bool,
idTokenUserinfoAssertion bool,
clockSkew time.Duration,
additionalOrigins []string,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if appID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument")
}
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument")
}
if clientID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-ghTsJ", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsProject(ctx, filter, a.ID, a.ResourceOwner); !exists || err != nil {
return nil, errors.ThrowNotFound(err, "PROJE-5LQ0U", "Errors.Project.NotFound")
}
return []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&a.Aggregate,
appID,
name,
),
project.NewOIDCConfigAddedEvent(
ctx,
&a.Aggregate,
version,
appID,
clientID,
clientSecret,
redirectUris,
responseTypes,
grantTypes,
applicationType,
authMethodType,
postLogoutRedirectUris,
devMode,
accessTokenType,
accessTokenRoleAssertion,
idTokenRoleAssertion,
idTokenUserinfoAssertion,
clockSkew,
additionalOrigins,
),
}, nil
}, nil
}
}
func AddAPIApp(
a project.Aggregate,
appID,
name,
clientID string,
clientSecret *crypto.CryptoValue,
authMethodType domain.APIAuthMethodType,
) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if appID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument")
}
if name = strings.TrimSpace(name); name == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument")
}
if clientID == "" {
return nil, errors.ThrowInvalidArgument(nil, "PROJE-XXED5", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
if exists, err := ExistsProject(ctx, filter, a.ID, a.ResourceOwner); !exists || err != nil {
return nil, errors.ThrowNotFound(err, "PROJE-Sf2gb", "Errors.Project.NotFound")
}
return []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&a.Aggregate,
appID,
name,
),
project.NewAPIConfigAddedEvent(
ctx,
&a.Aggregate,
appID,
clientID,
clientSecret,
authMethodType,
),
}, nil
}, nil
}
}
func ExistsApp(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, appID, resourceOwner string) (exists bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(project.AggregateType).
AggregateIDs(projectID).
EventTypes(
project.ApplicationAddedType,
project.ApplicationRemovedType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch e := event.(type) {
case *project.ApplicationAddedEvent:
if e.AppID == appID {
exists = true
}
case *project.ApplicationRemovedEvent:
if e.AppID == appID {
exists = false
}
}
}
return exists, nil
}

View File

@ -1,386 +0,0 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func TestAddOIDCApp(t *testing.T) {
type args struct {
a *project.Aggregate
appID string
name string
clientID string
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid appID",
args: args{
a: agg,
appID: "",
name: "name",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-NnavI", "Errors.Invalid.Argument"),
},
},
{
name: "invalid name",
args: args{
a: agg,
appID: "appID",
name: "",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-Fef31", "Errors.Invalid.Argument"),
},
},
{
name: "invalid clientID",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-ghTsJ", "Errors.Invalid.Argument"),
},
},
{
name: "project not exists",
args: args{
a: agg,
appID: "id",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "PROJE-5LQ0U", "Errors.Project.NotFound"),
},
},
{
name: "correct",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
ctx,
&agg.Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
project.NewApplicationAddedEvent(ctx, &agg.Aggregate,
"appID",
"name",
),
project.NewOIDCConfigAddedEvent(ctx, &agg.Aggregate,
domain.OIDCVersionV1,
"appID",
"clientID",
nil,
nil,
nil,
nil,
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeBasic,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t,
AddOIDCApp(*tt.args.a,
domain.OIDCVersionV1,
tt.args.appID,
tt.args.name,
tt.args.clientID,
nil,
nil,
nil,
nil,
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeBasic,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
), tt.args.filter, tt.want)
})
}
}
func TestAddAPIConfig(t *testing.T) {
type args struct {
a *project.Aggregate
appID string
name string
clientID string
filter preparation.FilterToQueryReducer
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid appID",
args: args{
a: agg,
appID: "",
name: "name",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XHsKt", "Errors.Invalid.Argument"),
},
},
{
name: "invalid name",
args: args{
a: agg,
appID: "appID",
name: "",
clientID: "clientID",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-F7g21", "Errors.Invalid.Argument"),
},
},
{
name: "invalid clientID",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "",
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-XXED5", "Errors.Invalid.Argument"),
},
},
{
name: "project not exists",
args: args{
a: agg,
appID: "id",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowNotFound(nil, "PROJE-Sf2gb", "Errors.Project.NotFound"),
},
},
{
name: "correct",
args: args{
a: agg,
appID: "appID",
name: "name",
clientID: "clientID",
filter: NewMultiFilter().
Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
ctx,
&agg.Aggregate,
"project",
false,
false,
false,
domain.PrivateLabelingSettingUnspecified,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
project.NewApplicationAddedEvent(
ctx,
&agg.Aggregate,
"appID",
"name",
),
project.NewAPIConfigAddedEvent(ctx, &agg.Aggregate,
"appID",
"clientID",
nil,
domain.APIAuthMethodTypeBasic,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t,
AddAPIApp(*tt.args.a,
tt.args.appID,
tt.args.name,
tt.args.clientID,
nil,
domain.APIAuthMethodTypeBasic,
), tt.args.filter, tt.want)
})
}
}
func TestExistsApp(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
appID string
projectID string
resourceOwner string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "app added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewApplicationAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"appID",
"name",
),
}, nil
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "app removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewApplicationAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"appID",
"name",
),
project.NewApplicationRemovedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"appID",
"name",
),
}, nil
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
},
appID: "appID",
projectID: "projectID",
resourceOwner: "ro",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsApp(context.Background(), tt.args.filter, tt.args.projectID, tt.args.appID, tt.args.resourceOwner)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}

View File

@ -1,195 +0,0 @@
package command
import (
"context"
"testing"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/project"
)
func TestAddProject(t *testing.T) {
type args struct {
a *project.Aggregate
name string
owner string
privateLabelingSetting domain.PrivateLabelingSetting
}
ctx := context.Background()
agg := project.NewAggregate("test", "test")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid name",
args: args{
a: agg,
name: "",
owner: "owner",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-C01yo", "Errors.Invalid.Argument"),
},
},
{
name: "invalid private labeling setting",
args: args{
a: agg,
name: "name",
owner: "owner",
privateLabelingSetting: -1,
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "PROJE-AO52V", "Errors.Invalid.Argument"),
},
},
{
name: "invalid owner",
args: args{
a: agg,
name: "name",
owner: "",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
ValidationErr: errors.ThrowPreconditionFailed(nil, "PROJE-hzxwo", "Errors.Invalid.Argument"),
},
},
{
name: "correct",
args: args{
a: agg,
name: "ZITADEL",
owner: "CAOS AG",
privateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
},
want: Want{
Commands: []eventstore.Command{
project.NewProjectAddedEvent(ctx, &agg.Aggregate,
"ZITADEL",
false,
false,
false,
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
),
project.NewProjectMemberAddedEvent(ctx, &agg.Aggregate,
"CAOS AG",
domain.RoleProjectOwner),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddProject(tt.args.a, tt.args.name, tt.args.owner, false, false, false, tt.args.privateLabelingSetting), nil, tt.want)
})
}
}
func TestExistsProject(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
id string
resourceOwner string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "project added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"name",
false,
false,
false,
domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "project removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
project.NewProjectAddedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"name",
false,
false,
false,
domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy,
),
project.NewProjectRemovedEvent(
context.Background(),
&project.NewAggregate("id", "ro").Aggregate,
"name",
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "PROJE-Op26p", "Errors.Internal")
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsProject(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}

View File

@ -1,40 +0,0 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
)
func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) {
events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(resourceOwner).
OrderAsc().
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(id).
EventTypes(
user.HumanRegisteredType,
user.UserV1RegisteredType,
user.HumanAddedType,
user.UserV1AddedType,
user.MachineAddedEventType,
user.UserRemovedType,
).Builder())
if err != nil {
return false, err
}
for _, event := range events {
switch event.(type) {
case *user.HumanRegisteredEvent, *user.HumanAddedEvent, *user.MachineAddedEvent:
exists = true
case *user.UserRemovedEvent:
exists = false
}
}
return exists, nil
}

View File

@ -1,97 +0,0 @@
package command
import (
"context"
"strings"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
)
type AddHuman struct {
// Username is required
Username string
// FirstName is required
FirstName string
// LastName is required
LastName string
// NickName is required
NickName string
// DisplayName is required
DisplayName string
// Email is required
Email string
// PreferredLang is required
PreferredLang language.Tag
// Gender is required
Gender domain.Gender
//TODO: can it also be verified?
Phone string
//Password is optional
Password string
//PasswordChangeRequired is used if the `Password`-field is set
PasswordChangeRequired bool
}
func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if !domain.EmailRegex.MatchString(human.Email) {
return nil, errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument")
}
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument")
}
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
return nil, errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument")
}
human.Phone = strings.TrimSpace(human.Phone)
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
domainPolicy, err := domainPolicyWriteModel(ctx, filter)
if err != nil {
return nil, err
}
cmd := user.NewHumanAddedEvent(
ctx,
&a.Aggregate,
human.Username,
human.FirstName,
human.LastName,
human.NickName,
human.DisplayName,
human.PreferredLang,
human.Gender,
human.Email, //TODO: pass if verified
domainPolicy.UserLoginMustBeDomain,
)
if human.Phone != "" {
cmd.AddPhoneData(human.Phone) //TODO: pass if verified
}
if human.Password != "" {
passwordComplexity, err := passwordComplexityPolicyWriteModel(ctx, filter)
if err != nil {
return nil, err
}
if err = passwordComplexity.Validate(human.Password); err != nil {
return nil, err
}
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
if err != nil {
return nil, err
}
cmd.AddPasswordData(secret, human.PasswordChangeRequired)
}
return []eventstore.Command{cmd}, nil
}, nil
}
}

View File

@ -1,169 +0,0 @@
package command
import (
"context"
"testing"
"github.com/golang/mock/gomock"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/user"
)
func TestAddHumanCommand(t *testing.T) {
type args struct {
a *user.Aggregate
human *AddHuman
passwordAlg crypto.HashAlgorithm
filter preparation.FilterToQueryReducer
}
agg := user.NewAggregate("id", "ro")
tests := []struct {
name string
args args
want Want
}{
{
name: "invalid email",
args: args{
a: agg,
human: &AddHuman{
Email: "invalid",
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-Ec7dM", "Errors.Invalid.Argument"),
},
},
{
name: "invalid first name",
args: args{
a: agg,
human: &AddHuman{
Email: "support@zitadel.ch",
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.Invalid.Argument"),
},
},
{
name: "invalid last name",
args: args{
a: agg,
human: &AddHuman{
Email: "support@zitadel.ch",
FirstName: "hurst",
},
},
want: Want{
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-DiAq8", "Errors.Invalid.Argument"),
},
},
{
name: "invalid password",
args: args{
a: agg,
human: &AddHuman{
Email: "support@zitadel.ch",
FirstName: "gigi",
LastName: "giraffe",
Password: "short",
},
filter: NewMultiFilter().Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
true,
),
}, nil
}).
Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
8,
true,
true,
true,
true,
),
}, nil
}).
Filter(),
},
want: Want{
CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
},
},
{
name: "correct",
args: args{
a: agg,
human: &AddHuman{
Email: "support@zitadel.ch",
FirstName: "gigi",
LastName: "giraffe",
Password: "",
},
passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
filter: NewMultiFilter().Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewDomainPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
true,
),
}, nil
}).
Append(
func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
org.NewPasswordComplexityPolicyAddedEvent(
context.Background(),
&org.NewAggregate("id", "ro").Aggregate,
2,
false,
false,
false,
false,
),
}, nil
}).
Filter(),
},
want: Want{
Commands: []eventstore.Command{
user.NewHumanAddedEvent(
context.Background(),
&agg.Aggregate,
"",
"gigi",
"giraffe",
"",
"",
language.Und,
0,
"support@zitadel.ch",
true,
),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AssertValidation(t, AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg), tt.args.filter, tt.want)
})
}
}

View File

@ -1,166 +0,0 @@
package command
import (
"context"
"testing"
"golang.org/x/text/language"
"github.com/caos/zitadel/internal/command/v2/preparation"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/user"
)
func TestExistsUser(t *testing.T) {
type args struct {
filter preparation.FilterToQueryReducer
id string
resourceOwner string
}
tests := []struct {
name string
args args
wantExists bool
wantErr bool
}{
{
name: "no events",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "human registered",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewHumanRegisteredEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"firstName",
"lastName",
"nickName",
"displayName",
language.German,
domain.GenderFemale,
"support@zitadel.ch",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "human added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewHumanAddedEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"firstName",
"lastName",
"nickName",
"displayName",
language.German,
domain.GenderFemale,
"support@zitadel.ch",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "machine added",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
context.Background(),
&user.NewAggregate("id", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: true,
wantErr: false,
},
{
name: "user removed",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return []eventstore.Event{
user.NewMachineAddedEvent(
context.Background(),
&user.NewAggregate("removed", "ro").Aggregate,
"userName",
"name",
"description",
true,
),
user.NewUserRemovedEvent(
context.Background(),
&user.NewAggregate("removed", "ro").Aggregate,
"userName",
nil,
true,
),
}, nil
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: false,
},
{
name: "error durring filter",
args: args{
filter: func(_ context.Context, _ *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) {
return nil, errors.ThrowInternal(nil, "USER-Drebn", "Errors.Internal")
},
id: "id",
resourceOwner: "ro",
},
wantExists: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner)
if (err != nil) != tt.wantErr {
t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotExists != tt.wantExists {
t.Errorf("ExistsUser() = %v, want %v", gotExists, tt.wantExists)
}
})
}
}

View File

@ -39,7 +39,7 @@ func LoadKey(id string, keyStorage KeyStorage) (string, error) {
return key.Value, nil
}
func LoadKeys(config *KeyConfig, keyStorage KeyStorage) (map[string]string, []string, error) {
func LoadKeys(config *KeyConfig, keyStorage KeyStorage) (Keys, []string, error) {
if config == nil {
return nil, nil, errors.ThrowInvalidArgument(nil, "CRYPT-dJK8s", "config must not be nil")
}
@ -47,7 +47,7 @@ func LoadKeys(config *KeyConfig, keyStorage KeyStorage) (map[string]string, []st
if err != nil {
return nil, nil, err
}
keys := make(map[string]string)
keys := make(Keys)
ids := make([]string, 0, len(config.DecryptionKeyIDs)+1)
if config.EncryptionKeyID != "" {
key, ok := readKeys[config.EncryptionKeyID]

View File

@ -146,10 +146,15 @@ func (a *OIDCApp) OriginsValid() bool {
return true
}
func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType {
grantTypes := make([]OIDCGrantType, 0)
implicit := false
for _, r := range a.ResponseTypes {
func ContainsRequiredGrantTypes(responseTypes []OIDCResponseType, grantTypes []OIDCGrantType) bool {
required := RequiredOIDCGrantTypes(responseTypes)
return ContainsOIDCGrantTypes(required, grantTypes)
}
func RequiredOIDCGrantTypes(responseTypes []OIDCResponseType) (grantTypes []OIDCGrantType) {
var implicit bool
for _, r := range responseTypes {
switch r {
case OIDCResponseTypeCode:
grantTypes = append(grantTypes, OIDCGrantTypeAuthorizationCode)
@ -160,9 +165,23 @@ func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType {
}
}
}
return grantTypes
}
func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType {
return RequiredOIDCGrantTypes(a.ResponseTypes)
}
func ContainsOIDCGrantTypes(shouldContain, list []OIDCGrantType) bool {
for _, should := range shouldContain {
if !containsOIDCGrantType(list, should) {
return false
}
}
return true
}
func containsOIDCGrantType(grantTypes []OIDCGrantType, grantType OIDCGrantType) bool {
for _, gt := range grantTypes {
if gt == grantType {

View File

@ -8,6 +8,11 @@ import (
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
)
type HumanDetails struct {
ID string
ObjectDetails
}
type Human struct {
es_models.ObjectRoot

View File

@ -8,7 +8,9 @@ import (
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
)
var EmailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
var (
EmailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
)
type Email struct {
es_models.ObjectRoot

View File

@ -1,11 +1,12 @@
package domain
import (
"time"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/ttacon/libphonenumber"
"time"
)
const (

View File

@ -22,8 +22,14 @@ const (
ProjectStateActive
ProjectStateInactive
ProjectStateRemoved
projectStateMax
)
func (s ProjectState) Valid() bool {
return s > ProjectStateUnspecified && s < projectStateMax
}
type PrivateLabelingSetting int32
const (

View File

@ -7,6 +7,7 @@ const (
SecretGeneratorTypeInitCode
SecretGeneratorTypeVerifyEmailCode
SecretGeneratorTypeVerifyPhoneCode
SecretGeneratorTypeVerifyDomain
SecretGeneratorTypePasswordResetCode
SecretGeneratorTypePasswordlessInitCode
SecretGeneratorTypeAppSecret
@ -14,6 +15,10 @@ const (
secretGeneratorTypeCount
)
func (t SecretGeneratorType) Valid() bool {
return t > SecretGeneratorTypeUnspecified && t < secretGeneratorTypeCount
}
type SecretGeneratorState int32
const (

View File

@ -79,7 +79,7 @@ func BaseEventFromRepo(event *repository.Event) *BaseEvent {
ID: event.AggregateID,
Type: AggregateType(event.AggregateType),
ResourceOwner: event.ResourceOwner.String,
InstanceID: event.InstanceID.String,
InstanceID: event.InstanceID,
Version: Version(event.Version),
},
EventType: EventType(event.Type),

View File

@ -82,7 +82,7 @@ func commandsToRepository(instanceID string, cmds []Command) (events []*reposito
AggregateID: cmd.Aggregate().ID,
AggregateType: repository.AggregateType(cmd.Aggregate().Type),
ResourceOwner: sql.NullString{String: cmd.Aggregate().ResourceOwner, Valid: cmd.Aggregate().ResourceOwner != ""},
InstanceID: sql.NullString{String: instanceID, Valid: instanceID != ""},
InstanceID: instanceID,
EditorService: cmd.EditorService(),
EditorUser: cmd.EditorUser(),
Type: repository.EventType(cmd.Type()),
@ -178,7 +178,7 @@ func (es *Eventstore) LatestSequence(ctx context.Context, queryFactory *SearchQu
return es.repo.LatestSequence(ctx, query)
}
type queryReducer interface {
type QueryReducer interface {
reducer
//Query returns the SearchQueryFactory for the events needed in reducer
Query() *SearchQueryBuilder
@ -186,7 +186,7 @@ type queryReducer interface {
//FilterToQueryReducer filters the events based on the search query of the query function,
// appends all events to the reducer and calls it's reduce function
func (es *Eventstore) FilterToQueryReducer(ctx context.Context, r queryReducer) error {
func (es *Eventstore) FilterToQueryReducer(ctx context.Context, r QueryReducer) error {
events, err := es.Filter(ctx, r.Query())
if err != nil {
return err

View File

@ -380,7 +380,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "instanceID", Valid: true},
InstanceID: "instanceID",
Type: "test.event",
Version: "v1",
},
@ -418,7 +418,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "instanceID", Valid: true},
InstanceID: "instanceID",
Type: "test.event",
Version: "v1",
},
@ -429,7 +429,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "instanceID", Valid: true},
InstanceID: "instanceID",
Type: "test.event",
Version: "v1",
},
@ -585,7 +585,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "", Valid: false},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -630,7 +630,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -641,7 +641,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -654,7 +654,7 @@ func TestEventstore_aggregatesToEvents(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -772,7 +772,7 @@ func TestEventstore_Push(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -816,7 +816,7 @@ func TestEventstore_Push(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -827,7 +827,7 @@ func TestEventstore_Push(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -882,7 +882,7 @@ func TestEventstore_Push(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -893,7 +893,7 @@ func TestEventstore_Push(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},
@ -906,7 +906,7 @@ func TestEventstore_Push(t *testing.T) {
EditorService: "editorService",
EditorUser: "editorUser",
ResourceOwner: sql.NullString{String: "caos", Valid: true},
InstanceID: sql.NullString{String: "zitadel"},
InstanceID: "zitadel",
Type: "test.event",
Version: "v1",
},

View File

@ -58,7 +58,7 @@ type Event struct {
ResourceOwner sql.NullString
//InstanceID is the instance where this event belongs to
// use the ID of the instance
InstanceID sql.NullString
InstanceID string
}
//EventType is the description of the change

View File

@ -71,7 +71,7 @@ const (
" $7::VARCHAR AS editor_service," +
" IFNULL((resource_owner), $8::VARCHAR) AS resource_owner," +
" $9::VARCHAR AS instance_id," +
" NEXTVAL(CONCAT('eventstore.', IFNULL($9, 'system'), '_seq'))," +
" NEXTVAL(CONCAT('eventstore.', IF($9 <> '', CONCAT('i_', $9), 'system'), '_seq'))," +
" aggregate_sequence AS previous_aggregate_sequence," +
" aggregate_type_sequence AS previous_aggregate_type_sequence " +
"FROM previous_data " +

View File

@ -136,7 +136,7 @@ func Test_prepareColumns(t *testing.T) {
},
},
fields: fields{
dbRow: []interface{}{time.Time{}, repository.EventType(""), uint64(5), Sequence(0), Sequence(0), Data(nil), "", "", sql.NullString{String: ""}, sql.NullString{String: ""}, repository.AggregateType("user"), "hodor", repository.Version("")},
dbRow: []interface{}{time.Time{}, repository.EventType(""), uint64(5), Sequence(0), Sequence(0), Data(nil), "", "", sql.NullString{String: ""}, "", repository.AggregateType("user"), "hodor", repository.Version("")},
},
},
{

View File

@ -68,6 +68,7 @@ type Instance struct {
DefaultLanguage language.Tag
SetupStarted domain.Step
SetupDone domain.Step
Host string
}
func (i *Instance) InstanceID() string {
@ -82,6 +83,10 @@ func (i *Instance) ConsoleClientID() string {
return i.ConsoleID
}
func (i *Instance) RequestedDomain() string {
return i.Host
}
type InstanceSearchQueries struct {
SearchRequest
Queries []SearchQuery
@ -96,7 +101,7 @@ func (q *InstanceSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder
}
func (q *Queries) Instance(ctx context.Context) (*Instance, error) {
stmt, scan := prepareIAMQuery()
stmt, scan := prepareInstanceQuery(authz.GetInstance(ctx).RequestedDomain())
query, args, err := stmt.Where(sq.Eq{
InstanceColumnID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).ToSql()
@ -109,7 +114,7 @@ func (q *Queries) Instance(ctx context.Context) (*Instance, error) {
}
func (q *Queries) InstanceByHost(ctx context.Context, host string) (authz.Instance, error) {
stmt, scan := prepareIAMQuery()
stmt, scan := prepareInstanceQuery(host)
query, args, err := stmt.Where(sq.Eq{
InstanceColumnID.identifier(): "system", //TODO: change column to domain when available
}).ToSql()
@ -129,7 +134,7 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag {
return iam.DefaultLanguage
}
func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
return sq.Select(
InstanceColumnID.identifier(),
InstanceColumnChangeDate.identifier(),
@ -143,17 +148,17 @@ func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
).
From(instanceTable.identifier()).PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*Instance, error) {
iam := new(Instance)
instance := &Instance{Host: host}
lang := ""
err := row.Scan(
&iam.ID,
&iam.ChangeDate,
&iam.Sequence,
&iam.GlobalOrgID,
&iam.IAMProjectID,
&iam.ConsoleID,
&iam.SetupStarted,
&iam.SetupDone,
&instance.ID,
&instance.ChangeDate,
&instance.Sequence,
&instance.GlobalOrgID,
&instance.IAMProjectID,
&instance.ConsoleID,
&instance.SetupStarted,
&instance.SetupDone,
&lang,
)
if err != nil {
@ -162,7 +167,7 @@ func prepareIAMQuery() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
}
return nil, errors.ThrowInternal(err, "QUERY-d9nw", "Errors.Internal")
}
iam.DefaultLanguage = language.Make(lang)
return iam, nil
instance.DefaultLanguage = language.Make(lang)
return instance, nil
}
}

View File

@ -10,6 +10,7 @@ import (
"golang.org/x/text/language"
sq "github.com/Masterminds/squirrel"
"github.com/caos/zitadel/internal/domain"
errs "github.com/caos/zitadel/internal/errors"
)
@ -27,7 +28,9 @@ func Test_InstancePrepares(t *testing.T) {
}{
{
name: "prepareInstanceQuery no result",
prepare: prepareIAMQuery,
prepare: func() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
return prepareInstanceQuery("")
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(`SELECT projections.instances.id,`+
@ -54,7 +57,9 @@ func Test_InstancePrepares(t *testing.T) {
},
{
name: "prepareInstanceQuery found",
prepare: prepareIAMQuery,
prepare: func() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
return prepareInstanceQuery("")
},
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(`SELECT projections.instances.id,`+
@ -105,7 +110,9 @@ func Test_InstancePrepares(t *testing.T) {
},
{
name: "prepareInstanceQuery sql err",
prepare: prepareIAMQuery,
prepare: func() (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
return prepareInstanceQuery("")
},
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(`SELECT projections.instances.id,`+

View File

@ -26,7 +26,7 @@ func testEvent(
Version: "v1",
AggregateID: "agg-id",
ResourceOwner: sql.NullString{String: "ro-id", Valid: true},
InstanceID: sql.NullString{String: "instance-id", Valid: true},
InstanceID: "instance-id",
ID: "event-id",
EditorService: "editor-svc",
EditorUser: "editor-user",

View File

@ -8,7 +8,6 @@ import (
"github.com/caos/zitadel/internal/eventstore/handler"
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
"github.com/caos/zitadel/internal/repository/instance"
"github.com/caos/zitadel/internal/repository/project"
)
const (
@ -63,7 +62,7 @@ func NewSecretGeneratorProjection(ctx context.Context, config crdb.StatementHand
func (p *SecretGeneratorProjection) reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{
{
Aggregate: project.AggregateType,
Aggregate: instance.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: instance.SecretGeneratorAddedEventType,

View File

@ -73,7 +73,7 @@ func (e *ConsoleSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstrain
func NewIAMConsoleSetEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
clientID string,
clientID *string,
) *ConsoleSetEvent {
return &ConsoleSetEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -81,7 +81,7 @@ func NewIAMConsoleSetEvent(
aggregate,
ConsoleSetEventType,
),
ClientID: clientID,
ClientID: *clientID,
}
}

View File

@ -58,6 +58,22 @@ func NewAPIConfigAddedEvent(
}
}
func (e *APIConfigAddedEvent) Validate(cmd eventstore.Command) bool {
c, ok := cmd.(*APIConfigAddedEvent)
if !ok {
return false
}
if e.AppID != c.AppID {
return false
}
if e.AuthMethodType != c.AuthMethodType {
return false
}
return true
}
func APIConfigAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &APIConfigAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),

View File

@ -97,6 +97,92 @@ func NewOIDCConfigAddedEvent(
}
}
func (e *OIDCConfigAddedEvent) Validate(cmd eventstore.Command) bool {
c, ok := cmd.(*OIDCConfigAddedEvent)
if !ok {
return false
}
if e.Version != c.Version {
return false
}
if e.AppID != c.AppID {
return false
}
if e.ClientID != "" && e.ClientID != c.ClientID {
return false
}
if e.ClientSecret != c.ClientSecret {
return false
}
if len(e.RedirectUris) != len(c.RedirectUris) {
return false
}
for i, uri := range e.RedirectUris {
if uri != c.RedirectUris[i] {
return false
}
}
if len(e.ResponseTypes) != len(c.ResponseTypes) {
return false
}
for i, typ := range e.ResponseTypes {
if typ != c.ResponseTypes[i] {
return false
}
}
if len(e.GrantTypes) != len(c.GrantTypes) {
return false
}
for i, typ := range e.GrantTypes {
if typ != c.GrantTypes[i] {
return false
}
}
if e.ApplicationType != c.ApplicationType {
return false
}
if e.AuthMethodType != c.AuthMethodType {
return false
}
if len(e.PostLogoutRedirectUris) != len(c.PostLogoutRedirectUris) {
return false
}
for i, uri := range e.PostLogoutRedirectUris {
if uri != c.PostLogoutRedirectUris[i] {
return false
}
}
if e.DevMode != c.DevMode {
return false
}
if e.AccessTokenType != c.AccessTokenType {
return false
}
if e.AccessTokenRoleAssertion != c.AccessTokenRoleAssertion {
return false
}
if e.IDTokenRoleAssertion != c.IDTokenRoleAssertion {
return false
}
if e.IDTokenUserinfoAssertion != c.IDTokenUserinfoAssertion {
return false
}
if e.ClockSkew != c.ClockSkew {
return false
}
if len(e.AdditionalOrigins) != len(c.AdditionalOrigins) {
return false
}
for i, origin := range e.AdditionalOrigins {
if origin != c.AdditionalOrigins[i] {
return false
}
}
return true
}
func OIDCConfigAddedEventMapper(event *repository.Event) (eventstore.Event, error) {
e := &OIDCConfigAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),