feat: user commands (#75)

* feat: eventstore repository

* fix: remove gorm

* version

* feat: pkg

* feat: add some files for project

* feat: eventstore without eventstore-lib

* rename files

* gnueg

* fix: key json

* fix: add object

* fix: change imports

* fix: internal models

* fix: some imports

* fix: global model

* feat: add global view functions

* fix: add some functions on repo

* feat(eventstore): sdk

* fix(eventstore): search query

* fix(eventstore): rename app to eventstore

* delete empty test

* remove unused func

* merge master

* fix(eventstore): tests

* fix(models): delete unused struct

* fix: some funcitons

* feat(eventstore): implemented push events

* fix: move project eventstore to project package

* fix: change project eventstore funcs

* feat(eventstore): overwrite context data

* fix: change project eventstore

* fix: add project repo to mgmt server

* feat(types): SQL-config

* fix: commented code

* feat(eventstore): options to overwrite editor

* feat: auth interceptor and cockroach migrations

* fix: migrations

* fix: fix filter

* fix: not found on getbyid

* fix: use global sql config

* fix: add sequence

* fix: add some tests

* fix(eventstore): nullable sequence

* fix: add some tests

* merge

* fix: add some tests

* fix(migrations): correct statements for sequence

* fix: add some tests

* fix: add some tests

* fix: changes from mr

* fix: changes from mr

* fix: add some tests

* Update internal/eventstore/models/field.go

Co-Authored-By: livio-a <livio.a@gmail.com>

* fix(eventstore): code quality

* fix: add types to aggregate/Event-types

* fix: try tests

* fix(eventstore): rename modifier* to editor*

* fix(eventstore): delete editor_org

* fix(migrations): remove editor_org field,
rename modifier_* to editor_*

* fix: query tests

* fix: use prepare funcs

* fix: go mod

* fix: generate files

* fix(eventstore): tests

* fix(eventstore): rename modifier to editor

* fix(migrations): add cluster migration,
fix(migrations): fix typo of host in clean clsuter

* fix(eventstore): move health

* fix(eventstore): AggregateTypeFilter aggregateType as param

* code quality

* fix: go tests

* feat: add member funcs

* feat: add member model

* feat: add member events

* feat: add member repo model

* fix: better error func testing

* fix: project member funcs

* fix: add tests

* fix: add tests

* feat: implement member requests

* fix: merge master

* fix: merge master

* fix: read existing in project repo

* fix: fix tests

* feat: add internal cache

* feat: add cache mock

* fix: return values of cache mock

* feat: add project role

* fix: add cache config

* fix: add role to eventstore

* fix: use eventstore sdk

* fix: use eventstore sdk

* fix: add project role grpc requests

* fix: fix getby id

* fix: changes for mr

* fix: change value to interface

* feat: add app event creations

* fix: searchmethods

* Update internal/project/model/project_member.go

Co-Authored-By: Silvan <silvan.reusser@gmail.com>

* fix: use get project func

* fix: append events

* fix: check if value is string on equal ignore case

* fix: add changes test

* fix: add go mod

* fix: add some tests

* fix: return err not nil

* fix: return err not nil

* fix: add aggregate funcs and tests

* fix: add oidc aggregate funcs and tests

* fix: add oidc

* fix: add some tests

* fix: tests

* fix: oidc validation

* fix: generate client secret

* fix: generate client id

* fix: test change app

* fix: deactivate/reactivate application

* fix: change oidc config

* fix: change oidc config secret

* fix: implement grpc app funcs

* fix: add application requests

* fix: converter

* fix: converter

* fix: converter and generate clientid

* fix: tests

* feat: project grant aggregate

* feat: project grant

* fix: project grant check if role existing

* fix: project grant requests

* fix: project grant fixes

* fix: project grant member model

* fix: project grant member aggregate

* fix: project grant member eventstore

* fix: project grant member requests

* feat: user model

* feat: user command side

* user command side

* profile requests

* local config with gopass and more

* Update internal/user/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/address.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/address.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/mfa.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/mfa.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/model/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/model/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/user_test.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/eventstore_mock_test.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* changes from mr review

* save files into basedir

* changes from mr review

* changes from mr review

* Update internal/usergrant/repository/eventsourcing/cache.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update internal/usergrant/repository/eventsourcing/cache.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* changes requested on mr

* fix generate codes

* fix return if no events

* password code

* Update internal/user/repository/eventsourcing/model/password.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* requests of mr

* check email

Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
Co-authored-by: livio-a <livio.a@gmail.com>
This commit is contained in:
Fabi 2020-05-11 10:16:27 +02:00 committed by GitHub
parent 380e4d0643
commit 49d86fdabb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 12791 additions and 2916 deletions

1
cmd/zitadel/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
local_*

22
cmd/zitadel/caos_local.sh Normal file
View File

@ -0,0 +1,22 @@
BASEDIR=$(dirname "$0")
# Tracing
gopass citadel-secrets/citadel/developer/default/citadel-svc-account-eventstore-local | base64 -D > "$BASEDIR/local_svc-account-tracing.json"
export GOOGLE_APPLICATION_CREDENTIALS="$BASEDIR/local_svc-account-tracing.json"
export ZITADEL_TRACING_PROJECT_ID=caos-citadel-test
export ZITADEL_TRACING_FRACTION=0.1
# Log
export ZITADEL_LOG_LEVEL=debug
# Cockroach
export ZITADEL_EVENTSTORE_HOST=localhost
export ZITADEL_EVENTSTORE_PORT=26257
# Keys
gopass citadel-secrets/citadel/developer/default/keys.yaml > "$BASEDIR/local_keys.yaml"
export ZITADEL_KEY_PATH="$BASEDIR/local_keys.yaml"
export ZITADEL_USER_VERIFICATION_KEY=UserVerificationKey_1
export ZITADEL_OTP_VERIFICATION_KEY=OTPVerificationKey_1

View File

@ -31,8 +31,8 @@ type Config struct {
}
func main() {
var configPaths config.ArrayFlags
flag.Var(&configPaths, "config-files", "path to the config files")
configPaths := config.NewArrayFlags("authz.yaml", "startup.yaml", "system-defaults.yaml")
flag.Var(configPaths, "config-files", "paths to the config files")
managementEnabled := flag.Bool("management", true, "enable management api")
authEnabled := flag.Bool("auth", true, "enable auth api")
loginEnabled := flag.Bool("login", true, "enable login ui")
@ -41,7 +41,7 @@ func main() {
flag.Parse()
conf := new(Config)
err := config.Read(conf, configPaths...)
err := config.Read(conf, configPaths.Values()...)
logging.Log("MAIN-FaF2r").OnError(err).Fatal("cannot read config")
ctx := context.Background()

View File

@ -1,12 +1,12 @@
Tracing:
Type: google
Config:
ProjectID: $TRACING_PROJECT_ID
ProjectID: $ZITADEL_TRACING_PROJECT_ID
MetricPrefix: ZITADEL-V1
Fraction: 1
Fraction: $ZITADEL_TRACING_FRACTION
Log:
Level: debug
Level: $ZITADEL_LOG_LEVEL
Formatter:
Format: text
@ -22,8 +22,8 @@ Mgmt:
ServiceName: 'ManagementAPI'
Repository:
SQL:
Host: $CR_HOST
Port: $CR_PORT
Host: $ZITADEL_EVENTSTORE_HOST
Port: $ZITADEL_EVENTSTORE_PORT
User: 'management'
Database: 'management'
SSLmode: disable
@ -32,7 +32,6 @@ Mgmt:
Config:
MaxCacheSizeInByte: 10485760 #10mb
Auth:
API:
GRPC:

View File

@ -1,8 +1,44 @@
SecretGenerators:
PasswordSaltCost: 14
ClientSecretGenerator:
Length: 64
IncludeLowerLetters: true
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: true
SystemDefaults:
UserVerificationKey:
EncryptionKeyID: $ZITADEL_USER_VERIFICATION_KEY
SecretGenerators:
PasswordSaltCost: 14
ClientSecretGenerator:
Length: 64
IncludeLowerLetters: true
IncludeUpperLetters: true
IncludeDigits: true
IncludeSymbols: true
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
Multifactors:
OTP:
Issuer: 'Zitadel'
VerificationKey:
EncryptionKeyID: $ZITADEL_OTP_VERIFICATION_KEY

View File

@ -31,3 +31,4 @@ cockroachdb/cockroach:v19.2.2 start --insecure
#### Should show eventstore, management, admin, auth
`show databases;`

1
go.mod
View File

@ -34,6 +34,7 @@ require (
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/nicksnyder/go-i18n/v2 v2.0.3
github.com/pquerna/otp v1.2.0
github.com/rs/cors v1.7.0
github.com/sirupsen/logrus v1.5.0 // indirect
github.com/sony/sonyflake v1.0.0

4
go.sum
View File

@ -54,6 +54,8 @@ github.com/aws/aws-sdk-go v1.30.4 h1:dpQgypC3rld2Uuz+/2u+0nbfmmyEWxau6v1hdAlvoc8
github.com/aws/aws-sdk-go v1.30.4/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.30.16 h1:1eabOZiVGl+XB02/VG9NpR+3fngaNc74pgb5RmyzgLY=
github.com/aws/aws-sdk-go v1.30.16/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/caos/logging v0.0.1 h1:YSGtO2/+5OWdwilBCou50akoDHAT/OhkbrolkVlR6U0=
github.com/caos/logging v0.0.1 h1:YSGtO2/+5OWdwilBCou50akoDHAT/OhkbrolkVlR6U0=
github.com/caos/logging v0.0.1/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
@ -262,6 +264,8 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

View File

@ -9,13 +9,29 @@ var _ flag.Value = (*ArrayFlags)(nil)
//ArrayFlags implements the flag/Value interface
//allowing to set multiple string flags with the same name
type ArrayFlags []string
type ArrayFlags struct {
defaultValues []string
values []string
}
func NewArrayFlags(defaults ...string) *ArrayFlags {
return &ArrayFlags{
defaultValues: defaults,
}
}
func (i *ArrayFlags) Values() []string {
if len(i.values) == 0 {
return i.defaultValues
}
return i.values
}
func (i *ArrayFlags) String() string {
return strings.Join(*i, ";")
return strings.Join(i.Values(), ";")
}
func (i *ArrayFlags) Set(value string) error {
*i = append(*i, value)
i.values = append(i.values, value)
return nil
}

View File

@ -3,10 +3,25 @@ package systemdefaults
import "github.com/caos/zitadel/internal/crypto"
type SystemDefaults struct {
SecretGenerator SecretGenerator
SecretGenerators SecretGenerators
UserVerificationKey *crypto.KeyConfig
Multifactors MultifactorConfig
}
type SecretGenerator struct {
PasswordSaltCost int
ClientSecretGenerator crypto.GeneratorConfig
type SecretGenerators struct {
PasswordSaltCost int
ClientSecretGenerator crypto.GeneratorConfig
InitializeUserCode crypto.GeneratorConfig
EmailVerificationCode crypto.GeneratorConfig
PhoneVerificationCode crypto.GeneratorConfig
PasswordVerificationCode crypto.GeneratorConfig
}
type MultifactorConfig struct {
OTP OTPConfig
}
type OTPConfig struct {
Issuer string
VerificationKey *crypto.KeyConfig
}

View File

@ -0,0 +1,59 @@
package crypto
import (
"github.com/caos/zitadel/internal/errors"
"github.com/golang/mock/gomock"
"testing"
)
func CreateMockEncryptionAlg(ctrl *gomock.Controller) EncryptionAlgorithm {
mCrypto := NewMockEncryptionAlgorithm(ctrl)
mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc")
mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id")
mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"})
mCrypto.EXPECT().Encrypt(gomock.Any()).DoAndReturn(
func(code []byte) ([]byte, error) {
return code, nil
},
)
mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn(
func(code []byte, keyID string) (string, error) {
if keyID != "id" {
return "", errors.ThrowInternal(nil, "id", "invalid key id")
}
return string(code), nil
},
)
return mCrypto
}
func createMockHashAlg(t *testing.T) HashAlgorithm {
mCrypto := NewMockHashAlgorithm(gomock.NewController(t))
mCrypto.EXPECT().Algorithm().AnyTimes().Return("hash")
mCrypto.EXPECT().Hash(gomock.Any()).DoAndReturn(
func(code []byte) ([]byte, error) {
return code, nil
},
)
mCrypto.EXPECT().CompareHash(gomock.Any(), gomock.Any()).DoAndReturn(
func(hashed, comparer []byte) error {
if string(hashed) != string(comparer) {
return errors.ThrowInternal(nil, "id", "invalid")
}
return nil
},
)
return mCrypto
}
func createMockCrypto(t *testing.T) Crypto {
mCrypto := NewMockCrypto(gomock.NewController(t))
mCrypto.EXPECT().Algorithm().AnyTimes().Return("crypto")
return mCrypto
}
func createMockGenerator(t *testing.T, crypto Crypto) Generator {
mGenerator := NewMockGenerator(gomock.NewController(t))
mGenerator.EXPECT().Alg().AnyTimes().Return(crypto)
return mGenerator
}

View File

@ -5,62 +5,8 @@ import (
"time"
"github.com/golang/mock/gomock"
"github.com/caos/zitadel/internal/errors"
)
func createMockEncryptionAlg(t *testing.T) EncryptionAlgorithm {
mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t))
mCrypto.EXPECT().Algorithm().AnyTimes().Return("enc")
mCrypto.EXPECT().EncryptionKeyID().AnyTimes().Return("id")
mCrypto.EXPECT().DecryptionKeyIDs().AnyTimes().Return([]string{"id"})
mCrypto.EXPECT().Encrypt(gomock.Any()).DoAndReturn(
func(code []byte) ([]byte, error) {
return code, nil
},
)
mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn(
func(code []byte, keyID string) (string, error) {
if keyID != "id" {
return "", errors.ThrowInternal(nil, "id", "invalid key id")
}
return string(code), nil
},
)
return mCrypto
}
func createMockHashAlg(t *testing.T) HashAlgorithm {
mCrypto := NewMockHashAlgorithm(gomock.NewController(t))
mCrypto.EXPECT().Algorithm().AnyTimes().Return("hash")
mCrypto.EXPECT().Hash(gomock.Any()).DoAndReturn(
func(code []byte) ([]byte, error) {
return code, nil
},
)
mCrypto.EXPECT().CompareHash(gomock.Any(), gomock.Any()).DoAndReturn(
func(hashed, comparer []byte) error {
if string(hashed) != string(comparer) {
return errors.ThrowInternal(nil, "id", "invalid")
}
return nil
},
)
return mCrypto
}
func createMockCrypto(t *testing.T) Crypto {
mCrypto := NewMockCrypto(gomock.NewController(t))
mCrypto.EXPECT().Algorithm().AnyTimes().Return("crypto")
return mCrypto
}
func createMockGenerator(t *testing.T, crypto Crypto) Generator {
mGenerator := NewMockGenerator(gomock.NewController(t))
mGenerator.EXPECT().Alg().AnyTimes().Return(crypto)
return mGenerator
}
func TestIsCodeExpired(t *testing.T) {
type args struct {
creationDate time.Time
@ -152,7 +98,7 @@ func TestVerifyCode(t *testing.T) {
Crypted: []byte("code"),
},
verificationCode: "code",
g: createMockGenerator(t, createMockEncryptionAlg(t)),
g: createMockGenerator(t, CreateMockEncryptionAlg(gomock.NewController(t))),
},
false,
},
@ -197,7 +143,7 @@ func Test_verifyEncryptedCode(t *testing.T) {
args{
cryptoCode: nil,
verificationCode: "",
alg: createMockEncryptionAlg(t),
alg: CreateMockEncryptionAlg(gomock.NewController(t)),
},
true,
},
@ -209,7 +155,7 @@ func Test_verifyEncryptedCode(t *testing.T) {
Crypted: nil,
},
verificationCode: "",
alg: createMockEncryptionAlg(t),
alg: CreateMockEncryptionAlg(gomock.NewController(t)),
},
true,
},
@ -222,7 +168,7 @@ func Test_verifyEncryptedCode(t *testing.T) {
Crypted: nil,
},
verificationCode: "",
alg: createMockEncryptionAlg(t),
alg: CreateMockEncryptionAlg(gomock.NewController(t)),
},
true,
},
@ -235,7 +181,7 @@ func Test_verifyEncryptedCode(t *testing.T) {
Crypted: nil,
},
verificationCode: "wrong",
alg: createMockEncryptionAlg(t),
alg: CreateMockEncryptionAlg(gomock.NewController(t)),
},
true,
},
@ -249,7 +195,7 @@ func Test_verifyEncryptedCode(t *testing.T) {
Crypted: []byte("code"),
},
verificationCode: "wrong",
alg: createMockEncryptionAlg(t),
alg: CreateMockEncryptionAlg(gomock.NewController(t)),
},
true,
},
@ -263,7 +209,7 @@ func Test_verifyEncryptedCode(t *testing.T) {
Crypted: []byte("code"),
},
verificationCode: "code",
alg: createMockEncryptionAlg(t),
alg: CreateMockEncryptionAlg(gomock.NewController(t)),
},
false,
},

View File

@ -23,3 +23,6 @@ func (o *ObjectRoot) AppendEvent(event *Event) {
o.Sequence = event.Sequence
}
func (o *ObjectRoot) IsZero() bool {
return o.AggregateID == ""
}

View File

@ -6,6 +6,8 @@ import (
es_int "github.com/caos/zitadel/internal/eventstore"
es_proj "github.com/caos/zitadel/internal/project/repository/eventsourcing"
es_usr "github.com/caos/zitadel/internal/user/repository/eventsourcing"
es_grant "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing"
)
type Config struct {
@ -17,6 +19,8 @@ type Config struct {
type EsRepository struct {
//spooler *es_spooler.Spooler
ProjectRepo
UserRepo
UserGrantRepo
}
func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) {
@ -42,9 +46,24 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
if err != nil {
return nil, err
}
user, err := es_usr.StartUser(es_usr.UserConfig{
Eventstore: es,
Cache: conf.Eventstore.Cache,
}, systemDefaults)
if err != nil {
return nil, err
}
usergrant, err := es_grant.StartUserGrant(es_grant.UserGrantConfig{
Eventstore: es,
Cache: conf.Eventstore.Cache,
})
if err != nil {
return nil, err
}
return &EsRepository{
ProjectRepo{project},
UserRepo{user},
UserGrantRepo{usergrant},
}, nil
}

View File

@ -0,0 +1,87 @@
package eventsourcing
import (
"context"
usr_model "github.com/caos/zitadel/internal/user/model"
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
)
type UserRepo struct {
UserEvents *usr_event.UserEventstore
}
func (repo *UserRepo) UserByID(ctx context.Context, id string) (project *usr_model.User, err error) {
return repo.UserEvents.UserByID(ctx, id)
}
func (repo *UserRepo) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) {
return repo.UserEvents.CreateUser(ctx, user)
}
func (repo *UserRepo) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) {
return repo.UserEvents.RegisterUser(ctx, user, resourceOwner)
}
func (repo *UserRepo) DeactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
return repo.UserEvents.DeactivateUser(ctx, id)
}
func (repo *UserRepo) ReactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
return repo.UserEvents.ReactivateUser(ctx, id)
}
func (repo *UserRepo) LockUser(ctx context.Context, id string) (*usr_model.User, error) {
return repo.UserEvents.LockUser(ctx, id)
}
func (repo *UserRepo) UnlockUser(ctx context.Context, id string) (*usr_model.User, error) {
return repo.UserEvents.UnlockUser(ctx, id)
}
func (repo *UserRepo) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
return repo.UserEvents.SetOneTimePassword(ctx, password)
}
func (repo *UserRepo) RequestSetPassword(ctx context.Context, id string, notifyType usr_model.NotificationType) error {
return repo.UserEvents.RequestSetPassword(ctx, id, notifyType)
}
func (repo *UserRepo) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
return repo.UserEvents.ProfileByID(ctx, userID)
}
func (repo *UserRepo) ChangeProfile(ctx context.Context, profile *usr_model.Profile) (*usr_model.Profile, error) {
return repo.UserEvents.ChangeProfile(ctx, profile)
}
func (repo *UserRepo) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) {
return repo.UserEvents.EmailByID(ctx, userID)
}
func (repo *UserRepo) ChangeEmail(ctx context.Context, email *usr_model.Email) (*usr_model.Email, error) {
return repo.UserEvents.ChangeEmail(ctx, email)
}
func (repo *UserRepo) CreateEmailVerificationCode(ctx context.Context, userID string) error {
return repo.UserEvents.CreateEmailVerificationCode(ctx, userID)
}
func (repo *UserRepo) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) {
return repo.UserEvents.PhoneByID(ctx, userID)
}
func (repo *UserRepo) ChangePhone(ctx context.Context, email *usr_model.Phone) (*usr_model.Phone, error) {
return repo.UserEvents.ChangePhone(ctx, email)
}
func (repo *UserRepo) CreatePhoneVerificationCode(ctx context.Context, userID string) error {
return repo.UserEvents.CreatePhoneVerificationCode(ctx, userID)
}
func (repo *UserRepo) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) {
return repo.UserEvents.AddressByID(ctx, userID)
}
func (repo *UserRepo) ChangeAddress(ctx context.Context, address *usr_model.Address) (*usr_model.Address, error) {
return repo.UserEvents.ChangeAddress(ctx, address)
}

View File

@ -0,0 +1,35 @@
package eventsourcing
import (
"context"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
grant_event "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing"
)
type UserGrantRepo struct {
UserGrantEvents *grant_event.UserGrantEventStore
}
func (repo *UserGrantRepo) UserGrantByID(ctx context.Context, grantID string) (*grant_model.UserGrant, error) {
return repo.UserGrantEvents.UserGrantByID(ctx, grantID)
}
func (repo *UserGrantRepo) AddUserGrant(ctx context.Context, grant *grant_model.UserGrant) (*grant_model.UserGrant, error) {
return repo.UserGrantEvents.AddUserGrant(ctx, grant)
}
func (repo *UserGrantRepo) ChangeUserGrant(ctx context.Context, grant *grant_model.UserGrant) (*grant_model.UserGrant, error) {
return repo.UserGrantEvents.ChangeUserGrant(ctx, grant)
}
func (repo *UserGrantRepo) DeactivateUserGrant(ctx context.Context, grantID string) (*grant_model.UserGrant, error) {
return repo.UserGrantEvents.DeactivateUserGrant(ctx, grantID)
}
func (repo *UserGrantRepo) ReactivateUserGrant(ctx context.Context, grantID string) (*grant_model.UserGrant, error) {
return repo.UserGrantEvents.ReactivateUserGrant(ctx, grantID)
}
func (repo *UserGrantRepo) RemoveUserGrant(ctx context.Context, grantID string) error {
return repo.UserGrantEvents.RemoveUserGrant(ctx, grantID)
}

View File

@ -3,4 +3,6 @@ package repository
type Repository interface {
Health() error
ProjectRepository
UserRepository
UserGrantRepository
}

View File

@ -0,0 +1,33 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/user/model"
)
type UserRepository interface {
UserByID(ctx context.Context, id string) (*model.User, error)
CreateUser(ctx context.Context, user *model.User) (*model.User, error)
RegisterUser(ctx context.Context, user *model.User, resourceOwner string) (*model.User, error)
DeactivateUser(ctx context.Context, id string) (*model.User, error)
ReactivateUser(ctx context.Context, id string) (*model.User, error)
LockUser(ctx context.Context, id string) (*model.User, error)
UnlockUser(ctx context.Context, id string) (*model.User, error)
SetOneTimePassword(ctx context.Context, password *model.Password) (*model.Password, error)
RequestSetPassword(ctx context.Context, id string, notifyType model.NotificationType) error
ProfileByID(ctx context.Context, userID string) (*model.Profile, error)
ChangeProfile(ctx context.Context, profile *model.Profile) (*model.Profile, error)
EmailByID(ctx context.Context, userID string) (*model.Email, error)
ChangeEmail(ctx context.Context, email *model.Email) (*model.Email, error)
CreateEmailVerificationCode(ctx context.Context, userID string) error
PhoneByID(ctx context.Context, userID string) (*model.Phone, error)
ChangePhone(ctx context.Context, email *model.Phone) (*model.Phone, error)
CreatePhoneVerificationCode(ctx context.Context, userID string) error
AddressByID(ctx context.Context, userID string) (*model.Address, error)
ChangeAddress(ctx context.Context, address *model.Address) (*model.Address, error)
}

View File

@ -0,0 +1,15 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/usergrant/model"
)
type UserGrantRepository interface {
UserGrantByID(ctx context.Context, grantID string) (*model.UserGrant, error)
AddUserGrant(ctx context.Context, grant *model.UserGrant) (*model.UserGrant, error)
ChangeUserGrant(ctx context.Context, grant *model.UserGrant) (*model.UserGrant, error)
DeactivateUserGrant(ctx context.Context, grantID string) (*model.UserGrant, error)
ReactivateUserGrant(ctx context.Context, grantID string) (*model.UserGrant, error)
RemoveUserGrant(ctx context.Context, grantID string) error
}

12
internal/model/mfa.go Normal file
View File

@ -0,0 +1,12 @@
package model
import "github.com/caos/zitadel/internal/crypto"
type Multifactors struct {
OTP OTP
}
type OTP struct {
Issuer string
CryptoMFA crypto.EncryptionAlgorithm
}

View File

@ -33,8 +33,8 @@ func StartProject(conf ProjectConfig, systemDefaults sd.SystemDefaults) (*Projec
if err != nil {
return nil, err
}
passwordAlg := crypto.NewBCrypt(systemDefaults.SecretGenerator.PasswordSaltCost)
pwGenerator := crypto.NewHashGenerator(systemDefaults.SecretGenerator.ClientSecretGenerator, passwordAlg)
passwordAlg := crypto.NewBCrypt(systemDefaults.SecretGenerators.PasswordSaltCost)
pwGenerator := crypto.NewHashGenerator(systemDefaults.SecretGenerators.ClientSecretGenerator, passwordAlg)
idGenerator := sonyflake.NewSonyflake(sonyflake.Settings{})
return &ProjectEventstore{
Eventstore: conf.Eventstore,

View File

@ -0,0 +1,13 @@
package model
import es_models "github.com/caos/zitadel/internal/eventstore/models"
type Address struct {
es_models.ObjectRoot
Country string
Locality string
PostalCode string
Region string
StreetAddress string
}

View File

@ -0,0 +1,44 @@
package model
import (
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"time"
)
type Email struct {
es_models.ObjectRoot
EmailAddress string
IsEmailVerified bool
}
type EmailCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue
Expiry time.Duration
}
func (e *Email) IsValid() bool {
return e.EmailAddress != ""
}
func (e *Email) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) (*EmailCode, error) {
var emailCode *EmailCode
if e.IsEmailVerified {
return emailCode, nil
}
emailCode = new(EmailCode)
return emailCode, emailCode.GenerateEmailCode(emailGenerator)
}
func (code *EmailCode) GenerateEmailCode(emailGenerator crypto.Generator) error {
emailCodeCrypto, _, err := crypto.NewCode(emailGenerator)
if err != nil {
return err
}
code.Code = emailCodeCrypto
code.Expiry = emailGenerator.Expiry()
return nil
}

View File

@ -0,0 +1,23 @@
package model
import (
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
)
type OTP struct {
es_models.ObjectRoot
Secret *crypto.CryptoValue
SecretString string
Url string
State MfaState
}
type MfaState int32
const (
MFASTATE_UNSPECIFIED MfaState = iota
MFASTATE_NOTREADY
MFASTATE_READY
)

View File

@ -0,0 +1,47 @@
package model
import (
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"time"
)
type Password struct {
es_models.ObjectRoot
SecretString string
SecretCrypto *crypto.CryptoValue
ChangeRequired bool
}
type PasswordCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue
Expiry time.Duration
NotificationType NotificationType
}
type NotificationType int32
const (
NOTIFICATIONTYPE_EMAIL NotificationType = iota
NOTIFICATIONTYPE_SMS
)
func (p *Password) IsValid() bool {
return p.AggregateID != "" && p.SecretString != ""
}
func (p *Password) HashPasswordIfExisting(passwordAlg crypto.HashAlgorithm, onetime bool) error {
if p.SecretString == "" {
return nil
}
secret, err := crypto.Hash([]byte(p.SecretString), passwordAlg)
if err != nil {
return err
}
p.SecretCrypto = secret
p.ChangeRequired = onetime
return nil
}

View File

@ -0,0 +1,67 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"time"
)
type Phone struct {
es_models.ObjectRoot
PhoneNumber string
IsPhoneVerified bool
}
type PhoneCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue
Expiry time.Duration
}
func (p *Phone) IsValid() bool {
return p.PhoneNumber != ""
}
func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
u.Phone = new(Phone)
u.Phone.setData(event)
u.IsPhoneVerified = false
return nil
}
func (u *User) appendUserPhoneVerifiedEvent() error {
u.IsPhoneVerified = true
return nil
}
func (p *Phone) setData(event *es_models.Event) error {
p.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, p); err != nil {
logging.Log("EVEN-dlo9s").WithError(err).Error("could not unmarshal event data")
return err
}
return nil
}
func (p *Phone) GeneratePhoneCodeIfNeeded(phoneGenerator crypto.Generator) (*PhoneCode, error) {
var phoneCode *PhoneCode
if p.IsPhoneVerified {
return phoneCode, nil
}
phoneCode = new(PhoneCode)
return phoneCode, phoneCode.GeneratePhoneCode(phoneGenerator)
}
func (code *PhoneCode) GeneratePhoneCode(phoneGenerator crypto.Generator) error {
phoneCodeCrypto, _, err := crypto.NewCode(phoneGenerator)
if err != nil {
return err
}
code.Code = phoneCodeCrypto
code.Expiry = phoneGenerator.Expiry()
return nil
}

View File

@ -0,0 +1,22 @@
package model
import (
es_models "github.com/caos/zitadel/internal/eventstore/models"
"golang.org/x/text/language"
)
type Profile struct {
es_models.ObjectRoot
UserName string
FirstName string
LastName string
NickName string
DisplayName string
PreferredLanguage language.Tag
Gender Gender
}
func (p *Profile) IsValid() bool {
return p.FirstName != "" && p.LastName != ""
}

126
internal/user/model/user.go Normal file
View File

@ -0,0 +1,126 @@
package model
import (
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"time"
)
type User struct {
es_models.ObjectRoot
State UserState
*Password
*Profile
*Email
*Phone
*Address
InitCode *InitUserCode
EmailCode *EmailCode
PhoneCode *PhoneCode
PasswordCode *PasswordCode
OTP *OTP
}
type InitUserCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue
Expiry time.Duration
}
type UserState int32
const (
USERSTATE_UNSPECIFIED UserState = iota
USERSTATE_ACTIVE
USERSTATE_INACTIVE
USERSTATE_DELETED
USERSTATE_LOCKED
USERSTATE_SUSPEND
USERSTATE_INITIAL
)
type Gender int32
const (
GENDER_UNDEFINED Gender = iota
GENDER_FEMALE
GENDER_MALE
GENDER_DIVERSE
)
func (u *User) SetEmailAsUsername() {
if u.Profile != nil && u.UserName == "" && u.Email != nil {
u.UserName = u.EmailAddress
}
}
func (u *User) IsValid() bool {
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.UserName != "" && u.Email != nil && u.EmailAddress != ""
}
func (u *User) IsInitialState() bool {
return u.Email == nil || !u.IsEmailVerified || u.Password == nil || u.SecretString == ""
}
func (u *User) IsActive() bool {
return u.State == USERSTATE_ACTIVE
}
func (u *User) IsInitial() bool {
return u.State == USERSTATE_INITIAL
}
func (u *User) IsInactive() bool {
return u.State == USERSTATE_INACTIVE
}
func (u *User) IsLocked() bool {
return u.State == USERSTATE_LOCKED
}
func (u *User) IsOTPReady() bool {
return u.OTP != nil && u.OTP.State == MFASTATE_READY
}
func (u *User) HashPasswordIfExisting(passwordAlg crypto.HashAlgorithm, onetime bool) error {
if u.Password != nil {
return u.Password.HashPasswordIfExisting(passwordAlg, onetime)
}
return nil
}
func (u *User) GenerateInitCodeIfNeeded(initGenerator crypto.Generator) error {
u.InitCode = new(InitUserCode)
if !u.IsInitialState() {
return nil
}
return u.InitCode.GenerateInitUserCode(initGenerator)
}
func (u *User) GeneratePhoneCodeIfNeeded(phoneGenerator crypto.Generator) error {
u.PhoneCode = new(PhoneCode)
if u.Phone == nil || u.IsPhoneVerified {
return nil
}
return u.PhoneCode.GeneratePhoneCode(phoneGenerator)
}
func (u *User) GenerateEmailCodeIfNeeded(emailGenerator crypto.Generator) error {
u.EmailCode = new(EmailCode)
if u.Email == nil || u.IsEmailVerified {
return nil
}
return u.EmailCode.GenerateEmailCode(emailGenerator)
}
func (init *InitUserCode) GenerateInitUserCode(generator crypto.Generator) error {
initCodeCrypto, _, err := crypto.NewCode(generator)
if err != nil {
return err
}
init.Code = initCodeCrypto
init.Expiry = generator.Expiry()
return nil
}

View File

@ -0,0 +1,35 @@
package eventsourcing
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/cache"
"github.com/caos/zitadel/internal/cache/config"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
)
type UserCache struct {
userCache cache.Cache
}
func StartCache(conf *config.CacheConfig) (*UserCache, error) {
userCache, err := conf.Config.NewCache()
logging.Log("EVENT-vDneN").OnError(err).Panic("unable to create user cache")
return &UserCache{userCache: userCache}, nil
}
func (c *UserCache) getUser(ID string) *model.User {
user := &model.User{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
if err := c.userCache.Get(ID, user); err != nil {
logging.Log("EVENT-4eTZh").WithError(err).Debug("error in getting cache")
}
return user
}
func (c *UserCache) cacheUser(user *model.User) {
err := c.userCache.Set(user.AggregateID, user)
if err != nil {
logging.Log("EVENT-ThnBb").WithError(err).Debug("error in setting project cache")
}
}

View File

@ -0,0 +1,18 @@
package eventsourcing
import (
"github.com/caos/zitadel/internal/crypto"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
)
func (es *UserEventstore) generatePasswordCode(passwordCode *model.PasswordCode, notifyType usr_model.NotificationType) error {
passwordCodeCrypto, _, err := crypto.NewCode(es.PasswordVerificationCode)
if err != nil {
return err
}
passwordCode.Code = passwordCodeCrypto
passwordCode.Expiry = es.PasswordVerificationCode.Expiry()
passwordCode.NotificationType = int32(notifyType)
return nil
}

View File

@ -0,0 +1,729 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/cache/config"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_int "github.com/caos/zitadel/internal/eventstore"
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
global_model "github.com/caos/zitadel/internal/model"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
"github.com/pquerna/otp/totp"
"github.com/sony/sonyflake"
"strconv"
)
type UserEventstore struct {
es_int.Eventstore
userCache *UserCache
idGenerator *sonyflake.Sonyflake
PasswordAlg crypto.HashAlgorithm
InitializeUserCode crypto.Generator
EmailVerificationCode crypto.Generator
PhoneVerificationCode crypto.Generator
PasswordVerificationCode crypto.Generator
Multifactors global_model.Multifactors
}
type UserConfig struct {
es_int.Eventstore
Cache *config.CacheConfig
PasswordSaltCost int
}
func StartUser(conf UserConfig, systemDefaults sd.SystemDefaults) (*UserEventstore, error) {
userCache, err := StartCache(conf.Cache)
if err != nil {
return nil, err
}
idGenerator := sonyflake.NewSonyflake(sonyflake.Settings{})
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey)
if err != nil {
return nil, err
}
initCodeGen := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.InitializeUserCode, aesCrypto)
emailVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.EmailVerificationCode, aesCrypto)
phoneVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PhoneVerificationCode, aesCrypto)
passwordVerificationCode := crypto.NewEncryptionGenerator(systemDefaults.SecretGenerators.PasswordVerificationCode, aesCrypto)
aesOtpCrypto, err := crypto.NewAESCrypto(systemDefaults.Multifactors.OTP.VerificationKey)
if err != nil {
return nil, err
}
mfa := global_model.Multifactors{
OTP: global_model.OTP{
CryptoMFA: aesOtpCrypto,
Issuer: systemDefaults.Multifactors.OTP.Issuer,
},
}
return &UserEventstore{
Eventstore: conf.Eventstore,
userCache: userCache,
idGenerator: idGenerator,
InitializeUserCode: initCodeGen,
EmailVerificationCode: emailVerificationCode,
PhoneVerificationCode: phoneVerificationCode,
PasswordVerificationCode: passwordVerificationCode,
Multifactors: mfa,
}, nil
}
func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.User, error) {
user := es.userCache.getUser(id)
query, err := UserByIDQuery(user.AggregateID, user.Sequence)
if err != nil {
return nil, err
}
err = es_sdk.Filter(ctx, es.FilterEvents, user.AppendEvents, query)
if err != nil && caos_errs.IsNotFound(err) && user.Sequence == 0 {
return nil, err
}
es.userCache.cacheUser(user)
return model.UserToModel(user), nil
}
func (es *UserEventstore) CreateUser(ctx context.Context, user *usr_model.User) (*usr_model.User, error) {
user.SetEmailAsUsername()
if !user.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "Name is required")
}
//TODO: Check Uniqueness
id, err := es.idGenerator.NextID()
if err != nil {
return nil, err
}
user.AggregateID = strconv.FormatUint(id, 10)
err = user.HashPasswordIfExisting(es.PasswordAlg, true)
if err != nil {
return nil, err
}
err = user.GenerateInitCodeIfNeeded(es.InitializeUserCode)
if err != nil {
return nil, err
}
err = user.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
if err != nil {
return nil, err
}
repoUser := model.UserFromModel(user)
repoInitCode := model.InitCodeFromModel(user.InitCode)
repoPhoneCode := model.PhoneCodeFromModel(user.PhoneCode)
createAggregate := UserCreateAggregate(es.AggregateCreator(), repoUser, repoInitCode, repoPhoneCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.UserToModel(repoUser), nil
}
func (es *UserEventstore) RegisterUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*usr_model.User, error) {
user.SetEmailAsUsername()
if !user.IsValid() || user.Password == nil || user.SecretString == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "user is invalid")
}
//TODO: Check Uniqueness
id, err := es.idGenerator.NextID()
if err != nil {
return nil, err
}
user.AggregateID = strconv.FormatUint(id, 10)
err = user.HashPasswordIfExisting(es.PasswordAlg, false)
if err != nil {
return nil, err
}
err = user.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
if err != nil {
return nil, err
}
repoUser := model.UserFromModel(user)
repoEmailCode := model.EmailCodeFromModel(user.EmailCode)
createAggregate := UserRegisterAggregate(es.AggregateCreator(), repoUser, resourceOwner, repoEmailCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, createAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.UserToModel(repoUser), nil
}
func (es *UserEventstore) DeactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if existing.IsInactive() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-die45", "cant deactivate inactive user")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserDeactivateAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) ReactivateUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if !existing.IsInactive() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do94s", "user must be inactive")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserReactivateAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) LockUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if !existing.IsActive() && !existing.IsInitial() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di83s", "user must be active or initial")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserLockAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) UnlockUser(ctx context.Context, id string) (*usr_model.User, error) {
existing, err := es.UserByID(ctx, id)
if err != nil {
return nil, err
}
if !existing.IsLocked() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dks83", "user must be locked")
}
repoExisting := model.UserFromModel(existing)
aggregate := UserUnlockAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, aggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.UserToModel(repoExisting), nil
}
func (es *UserEventstore) InitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d8diw", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.InitCode != nil {
return user.InitCode, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "init code not found")
}
func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, userID string) (*usr_model.InitUserCode, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
initCode := new(usr_model.InitUserCode)
err = initCode.GenerateInitUserCode(es.InitializeUserCode)
if err != nil {
return nil, err
}
repoUser := model.UserFromModel(user)
repoInitCode := model.InitCodeFromModel(initCode)
agg := UserInitCodeAggregate(es.AggregateCreator(), repoUser, repoInitCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.InitCodeToModel(repoUser.InitCode), nil
}
func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := SkipMfaAggregate(es.AggregateCreator(), repoUser)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) UserPasswordByID(ctx context.Context, userID string) (*usr_model.Password, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Password != nil {
return user.Password, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-d8e2", "password not found")
}
func (es *UserEventstore) SetOneTimePassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
return es.changedPassword(ctx, password, true)
}
func (es *UserEventstore) SetPassword(ctx context.Context, password *usr_model.Password) (*usr_model.Password, error) {
return es.changedPassword(ctx, password, false)
}
func (es *UserEventstore) changedPassword(ctx context.Context, password *usr_model.Password, onetime bool) (*usr_model.Password, error) {
if !password.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-dosi3", "password invalid")
}
user, err := es.UserByID(ctx, password.AggregateID)
if err != nil {
return nil, err
}
err = password.HashPasswordIfExisting(es.PasswordAlg, onetime)
if err != nil {
return nil, err
}
repoUser := model.UserFromModel(user)
repoPassword := model.PasswordFromModel(password)
agg := PasswordChangeAggregate(es.AggregateCreator(), repoUser, repoPassword)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoUser)
return model.PasswordToModel(repoUser.Password), nil
}
func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string, notifyType usr_model.NotificationType) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
passwordCode := new(model.PasswordCode)
err = es.generatePasswordCode(passwordCode, notifyType)
if err != nil {
return err
}
repoUser := model.UserFromModel(user)
agg := RequestSetPassword(es.AggregateCreator(), repoUser, passwordCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
if err != nil {
return err
}
es.userCache.cacheUser(repoUser)
return nil
}
func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Profile != nil {
return user.Profile, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dk23f", "profile not found")
}
func (es *UserEventstore) ChangeProfile(ctx context.Context, profile *usr_model.Profile) (*usr_model.Profile, error) {
if !profile.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-d82i3", "profile is invalid")
}
existing, err := es.UserByID(ctx, profile.AggregateID)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.ProfileFromModel(profile)
updateAggregate := ProfileChangeAggregate(es.AggregateCreator(), repoExisting, repoNew)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.ProfileToModel(repoExisting.Profile), nil
}
func (es *UserEventstore) EmailByID(ctx context.Context, userID string) (*usr_model.Email, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Email != nil {
return user.Email, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dki89", "email not found")
}
func (es *UserEventstore) ChangeEmail(ctx context.Context, email *usr_model.Email) (*usr_model.Email, error) {
if !email.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "email is invalid")
}
existing, err := es.UserByID(ctx, email.AggregateID)
if err != nil {
return nil, err
}
emailCode, err := email.GenerateEmailCodeIfNeeded(es.EmailVerificationCode)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.EmailFromModel(email)
repoEmailCode := model.EmailCodeFromModel(emailCode)
updateAggregate := EmailChangeAggregate(es.AggregateCreator(), repoExisting, repoNew, repoEmailCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.EmailToModel(repoExisting.Email), nil
}
func (es *UserEventstore) VerifyEmail(ctx context.Context, userID, verificationCode string) error {
if userID == "" || verificationCode == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9fd", "userId or Code empty")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.EmailCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-lso9w", "code not found")
}
if err := crypto.VerifyCode(existing.EmailCode.CreationDate, existing.EmailCode.Expiry, existing.EmailCode.Code, verificationCode, es.EmailVerificationCode); err != nil {
return err
}
repoExisting := model.UserFromModel(existing)
updateAggregate := EmailVerifiedAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-lco09", "userID missing")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.Email == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "no email existing")
}
if existing.IsEmailVerified {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-pdo9s", "email already verified")
}
emailCode := new(usr_model.EmailCode)
err = emailCode.GenerateEmailCode(es.EmailVerificationCode)
if err != nil {
return err
}
repoExisting := model.UserFromModel(existing)
repoEmailCode := model.EmailCodeFromModel(emailCode)
updateAggregate := EmailVerificationCodeAggregate(es.AggregateCreator(), repoExisting, repoEmailCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Phone != nil {
return user.Phone, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-pos9e", "phone not found")
}
func (es *UserEventstore) ChangePhone(ctx context.Context, phone *usr_model.Phone) (*usr_model.Phone, error) {
if !phone.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9s4", "phone is invalid")
}
existing, err := es.UserByID(ctx, phone.AggregateID)
if err != nil {
return nil, err
}
phoneCode, err := phone.GeneratePhoneCodeIfNeeded(es.PhoneVerificationCode)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.PhoneFromModel(phone)
repoPhoneCode := model.PhoneCodeFromModel(phoneCode)
updateAggregate := PhoneChangeAggregate(es.AggregateCreator(), repoExisting, repoNew, repoPhoneCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.PhoneToModel(repoExisting.Phone), nil
}
func (es *UserEventstore) VerifyPhone(ctx context.Context, userID, verificationCode string) error {
if userID == "" || verificationCode == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dsi8s", "userId or Code empty")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.PhoneCode == nil {
return caos_errs.ThrowNotFound(nil, "EVENT-slp0s", "code not found")
}
if err := crypto.VerifyCode(existing.PhoneCode.CreationDate, existing.PhoneCode.Expiry, existing.PhoneCode.Code, verificationCode, es.PhoneVerificationCode); err != nil {
return err
}
repoExisting := model.UserFromModel(existing)
updateAggregate := PhoneVerifiedAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userID string) error {
if userID == "" {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9sw", "userID missing")
}
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.Phone == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp9fs", "no phone existing")
}
if existing.IsPhoneVerified {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sleis", "phone already verified")
}
phoneCode := new(usr_model.PhoneCode)
err = phoneCode.GeneratePhoneCode(es.PhoneVerificationCode)
if err != nil {
return err
}
repoExisting := model.UserFromModel(existing)
repoPhoneCode := model.PhoneCodeFromModel(phoneCode)
updateAggregate := PhoneVerificationCodeAggregate(es.AggregateCreator(), repoExisting, repoPhoneCode)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di8ws", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.Address != nil {
return user.Address, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-so9wa", "address not found")
}
func (es *UserEventstore) ChangeAddress(ctx context.Context, address *usr_model.Address) (*usr_model.Address, error) {
existing, err := es.UserByID(ctx, address.AggregateID)
if err != nil {
return nil, err
}
repoExisting := model.UserFromModel(existing)
repoNew := model.AddressFromModel(address)
updateAggregate := AddressChangeAggregate(es.AggregateCreator(), repoExisting, repoNew)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
return model.AddressToModel(repoExisting.Address), nil
}
func (es *UserEventstore) OTPByID(ctx context.Context, userID string) (*usr_model.OTP, error) {
if userID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing")
}
user, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if user.OTP != nil {
return user.OTP, nil
}
return nil, caos_errs.ThrowNotFound(nil, "EVENT-dps09", "otp not found")
}
func (es *UserEventstore) AddOTP(ctx context.Context, userID string) (*usr_model.OTP, error) {
existing, err := es.UserByID(ctx, userID)
if err != nil {
return nil, err
}
if existing.IsOTPReady() {
return nil, caos_errs.ThrowAlreadyExists(nil, "EVENT-do9se", "user has already configured otp")
}
key, err := totp.Generate(totp.GenerateOpts{Issuer: es.Multifactors.OTP.Issuer, AccountName: userID})
if err != nil {
return nil, err
}
encryptedSecret, err := crypto.Encrypt([]byte(key.Secret()), es.Multifactors.OTP.CryptoMFA)
if err != nil {
return nil, err
}
repoOtp := &model.OTP{Secret: encryptedSecret}
repoExisting := model.UserFromModel(existing)
updateAggregate := MfaOTPAddAggregate(es.AggregateCreator(), repoExisting, repoOtp)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return nil, err
}
es.userCache.cacheUser(repoExisting)
otp := model.OTPToModel(repoExisting.OTP)
otp.Url = key.URL()
otp.SecretString = key.Secret()
return otp, nil
}
func (es *UserEventstore) RemoveOTP(ctx context.Context, userID string) error {
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.OTP == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "no otp existing")
}
repoExisting := model.UserFromModel(existing)
updateAggregate := MfaOTPRemoveAggregate(es.AggregateCreator(), repoExisting)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, updateAggregate)
if err != nil {
return err
}
es.userCache.cacheUser(repoExisting)
return nil
}
func (es *UserEventstore) CheckMfaOTP(ctx context.Context, userID, code string) error {
existing, err := es.UserByID(ctx, userID)
if err != nil {
return err
}
if existing.OTP == nil {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0de", "no otp existing")
}
decrypt, err := crypto.DecryptString(existing.OTP.Secret, es.Multifactors.OTP.CryptoMFA)
if err != nil {
return err
}
valid := totp.Validate(code, decrypt)
if !valid {
return caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Invalid code")
}
return nil
}

View File

@ -0,0 +1,429 @@
package eventsourcing
import (
"encoding/json"
mock_cache "github.com/caos/zitadel/internal/cache/mock"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/eventstore/mock"
es_models "github.com/caos/zitadel/internal/eventstore/models"
global_model "github.com/caos/zitadel/internal/model"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
"github.com/golang/mock/gomock"
"github.com/sony/sonyflake"
"time"
)
func GetMockedEventstore(ctrl *gomock.Controller, mockEs *mock.MockEventstore) *UserEventstore {
return &UserEventstore{
Eventstore: mockEs,
userCache: GetMockCache(ctrl),
idGenerator: GetSonyFlacke(),
}
}
func GetMockedEventstoreWithPw(ctrl *gomock.Controller, mockEs *mock.MockEventstore, init, email, phone, password bool) *UserEventstore {
es := &UserEventstore{
Eventstore: mockEs,
userCache: GetMockCache(ctrl),
idGenerator: GetSonyFlacke(),
}
if init {
es.InitializeUserCode = GetMockPwGenerator(ctrl)
}
if email {
es.EmailVerificationCode = GetMockPwGenerator(ctrl)
}
if phone {
es.PhoneVerificationCode = GetMockPwGenerator(ctrl)
}
if password {
es.PasswordVerificationCode = GetMockPwGenerator(ctrl)
hash := crypto.NewMockHashAlgorithm(ctrl)
hash.EXPECT().Hash(gomock.Any()).Return(nil, nil)
hash.EXPECT().Algorithm().Return("bcrypt")
es.PasswordAlg = hash
}
return es
}
func GetMockCache(ctrl *gomock.Controller) *UserCache {
mockCache := mock_cache.NewMockCache(ctrl)
mockCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockCache.EXPECT().Set(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
return &UserCache{userCache: mockCache}
}
func GetSonyFlacke() *sonyflake.Sonyflake {
return sonyflake.NewSonyflake(sonyflake.Settings{})
}
func GetMockPwGenerator(ctrl *gomock.Controller) crypto.Generator {
alg := crypto.CreateMockEncryptionAlg(ctrl)
generator := crypto.NewMockGenerator(ctrl)
generator.EXPECT().Length().Return(uint(10))
generator.EXPECT().Runes().Return([]rune("abcdefghijklmnopqrstuvwxyz"))
generator.EXPECT().Alg().AnyTimes().Return(alg)
generator.EXPECT().Expiry().Return(time.Hour * 1)
return generator
}
func GetMockUserByIDOK(ctrl *gomock.Controller, user model.User) *UserEventstore {
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockUserByIDNoEvents(ctrl *gomock.Controller) *UserEventstore {
events := []*es_models.Event{}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUser(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserWithPWGenerator(ctrl *gomock.Controller, init, email, phone, password bool) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
Email: &model.Email{
EmailAddress: "EmailAddress",
},
Phone: &model.Phone{
PhoneNumber: "PhoneNumber",
},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, init, email, phone, password)
}
func GetMockManipulateUserWithInitCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, true, false, false, false)
}
func GetMockManipulateUserWithPasswordAndEmailCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, true)
}
func GetMockManipulateUserWithEmailCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, false)
}
func GetMockManipulateUserWithPhoneCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, true, false)
}
func GetMockManipulateUserWithPasswordCodeGen(ctrl *gomock.Controller, user model.User) *UserEventstore {
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, false, true)
}
func GetMockManipulateUserWithOTPGen(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
es := GetMockedEventstore(ctrl, mockEs)
hash := crypto.NewMockEncryptionAlgorithm(ctrl)
hash.EXPECT().Algorithm().Return("aes")
hash.EXPECT().Encrypt(gomock.Any()).Return(nil, nil)
hash.EXPECT().EncryptionKeyID().Return("id")
es.Multifactors = global_model.Multifactors{OTP: global_model.OTP{
Issuer: "Issuer",
CryptoMFA: hash,
}}
return es
}
func GetMockManipulateInactiveUser(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
&es_models.Event{AggregateID: "AggregateID", Sequence: 2, Type: model.UserDeactivated},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateLockedUser(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: data},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserLocked},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserWithInitCode(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
}
code := model.InitUserCode{Expiry: time.Hour * 30}
dataUser, _ := json.Marshal(user)
dataCode, _ := json.Marshal(code)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.InitializedUserCodeAdded, Data: dataCode},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserWithEmailCode(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
Email: &model.Email{
EmailAddress: "EmailAddress",
},
}
code := model.EmailCode{Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("code"),
}}
dataUser, _ := json.Marshal(user)
dataCode, _ := json.Marshal(code)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserEmailCodeAdded, Data: dataCode},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, false, true, false, false)
}
func GetMockManipulateUserVerifiedEmail(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
Email: &model.Email{
EmailAddress: "EmailAddress",
},
}
dataUser, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserEmailVerified},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserWithPhoneCode(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
Phone: &model.Phone{
PhoneNumber: "PhoneNumber",
},
}
code := model.PhoneCode{Code: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("code"),
}}
dataUser, _ := json.Marshal(user)
dataCode, _ := json.Marshal(code)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserPhoneCodeAdded, Data: dataCode},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstoreWithPw(ctrl, mockEs, false, false, true, false)
}
func GetMockManipulateUserVerifiedPhone(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
Phone: &model.Phone{
PhoneNumber: "PhoneNumber",
},
}
dataUser, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserPhoneVerified},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserFull(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
FirstName: "FirstName",
LastName: "LastName",
},
Password: &model.Password{
Secret: &crypto.CryptoValue{Algorithm: "bcrypt", KeyID: "KeyID"},
ChangeRequired: true,
},
Email: &model.Email{
EmailAddress: "EmailAddress",
},
Phone: &model.Phone{
PhoneNumber: "PhoneNumber",
},
Address: &model.Address{
Country: "Country",
},
}
dataUser, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserWithOTP(ctrl *gomock.Controller) *UserEventstore {
user := model.User{
Profile: &model.Profile{
UserName: "UserName",
},
}
otp := model.OTP{Secret: &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("code"),
}}
dataUser, _ := json.Marshal(user)
dataOtp, _ := json.Marshal(otp)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserAdded, Data: dataUser},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.MfaOtpAdded, Data: dataOtp},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserNoEvents(ctrl *gomock.Controller) *UserEventstore {
events := []*es_models.Event{}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,77 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
)
type Address struct {
es_models.ObjectRoot
Country string `json:"country,omitempty"`
Locality string `json:"locality,omitempty"`
PostalCode string `json:"postalCode,omitempty"`
Region string `json:"region,omitempty"`
StreetAddress string `json:"streetAddress,omitempty"`
}
func (a *Address) Changes(changed *Address) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if a.Country != changed.Country {
changes["country"] = changed.Country
}
if a.Locality != changed.Locality {
changes["locality"] = changed.Locality
}
if a.PostalCode != changed.PostalCode {
changes["postalCode"] = changed.PostalCode
}
if a.Region != changed.Region {
changes["region"] = changed.Region
}
if a.StreetAddress != changed.StreetAddress {
changes["streetAddress"] = changed.StreetAddress
}
return changes
}
func AddressFromModel(address *model.Address) *Address {
return &Address{
ObjectRoot: address.ObjectRoot,
Country: address.Country,
Locality: address.Locality,
PostalCode: address.PostalCode,
Region: address.Region,
StreetAddress: address.StreetAddress,
}
}
func AddressToModel(address *Address) *model.Address {
return &model.Address{
ObjectRoot: address.ObjectRoot,
Country: address.Country,
Locality: address.Locality,
PostalCode: address.PostalCode,
Region: address.Region,
StreetAddress: address.StreetAddress,
}
}
func (u *User) appendUserAddressChangedEvent(event *es_models.Event) error {
if u.Address == nil {
u.Address = new(Address)
}
return u.Address.setData(event)
}
func (a *Address) setData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-clos0").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-so92s", "could not unmarshal event")
}
return nil
}

View File

@ -0,0 +1,92 @@
package model
import (
"encoding/json"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"testing"
)
func TestAddressChanges(t *testing.T) {
type args struct {
existing *Address
new *Address
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all fields changed",
args: args{
existing: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
new: &Address{Country: "CountryChanged", Locality: "LocalityChanged", PostalCode: "PostalCodeChanged", Region: "RegionChanged", StreetAddress: "StreetAddressChanged"},
},
res: res{
changesLen: 5,
},
},
{
name: "no fields changed",
args: args{
existing: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
new: &Address{Country: "Country", Locality: "Locality", PostalCode: "PostalCode", Region: "Region", StreetAddress: "StreetAddress"},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existing.Changes(tt.args.new)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}
func TestAppendUserAddressChangedEvent(t *testing.T) {
type args struct {
user *User
address *Address
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user address event",
args: args{
user: &User{Address: &Address{Locality: "Locality", Country: "Country"}},
address: &Address{Locality: "LocalityChanged", PostalCode: "PostalCode"},
event: &es_models.Event{},
},
result: &User{Address: &Address{Locality: "LocalityChanged", Country: "Country", PostalCode: "PostalCode"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.address != nil {
data, _ := json.Marshal(tt.args.address)
tt.args.event.Data = data
}
tt.args.user.appendUserAddressChangedEvent(tt.args.event)
if tt.args.user.Address.Locality != tt.result.Address.Locality {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
if tt.args.user.Address.Country != tt.result.Address.Country {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
if tt.args.user.Address.PostalCode != tt.result.Address.PostalCode {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@ -0,0 +1,102 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
"time"
)
type Email struct {
es_models.ObjectRoot
EmailAddress string `json:"email,omitempty"`
IsEmailVerified bool `json:"-"`
isEmailUnique bool `json:"-"`
}
type EmailCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
}
func (e *Email) Changes(changed *Email) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if changed.EmailAddress != "" && e.EmailAddress != changed.EmailAddress {
changes["email"] = changed.EmailAddress
}
return changes
}
func EmailFromModel(email *model.Email) *Email {
return &Email{
ObjectRoot: email.ObjectRoot,
EmailAddress: email.EmailAddress,
IsEmailVerified: email.IsEmailVerified,
}
}
func EmailToModel(email *Email) *model.Email {
return &model.Email{
ObjectRoot: email.ObjectRoot,
EmailAddress: email.EmailAddress,
IsEmailVerified: email.IsEmailVerified,
}
}
func EmailCodeFromModel(code *model.EmailCode) *EmailCode {
if code == nil {
return nil
}
return &EmailCode{
ObjectRoot: code.ObjectRoot,
Expiry: code.Expiry,
Code: code.Code,
}
}
func EmailCodeToModel(code *EmailCode) *model.EmailCode {
return &model.EmailCode{
ObjectRoot: code.ObjectRoot,
Expiry: code.Expiry,
Code: code.Code,
}
}
func (u *User) appendUserEmailChangedEvent(event *es_models.Event) error {
u.Email = new(Email)
return u.Email.setData(event)
}
func (u *User) appendUserEmailCodeAddedEvent(event *es_models.Event) error {
u.EmailCode = new(EmailCode)
return u.EmailCode.setData(event)
}
func (u *User) appendUserEmailVerifiedEvent() {
u.IsEmailVerified = true
}
func (a *Email) setData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-dlo9s").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-sl9xw", "could not unmarshal event")
}
return nil
}
func (a *EmailCode) setData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-lo9s").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-s8uws", "could not unmarshal event")
}
return nil
}

View File

@ -0,0 +1,152 @@
package model
import (
"encoding/json"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"testing"
"time"
)
func TestEmailChanges(t *testing.T) {
type args struct {
existing *Email
new *Email
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all fields changed",
args: args{
existing: &Email{EmailAddress: "Email", IsEmailVerified: true},
new: &Email{EmailAddress: "EmailChanged", IsEmailVerified: false},
},
res: res{
changesLen: 1,
},
},
{
name: "no fields changed",
args: args{
existing: &Email{EmailAddress: "Email", IsEmailVerified: true},
new: &Email{EmailAddress: "Email", IsEmailVerified: false},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existing.Changes(tt.args.new)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}
func TestAppendUserEmailChangedEvent(t *testing.T) {
type args struct {
user *User
email *Email
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user email event",
args: args{
user: &User{Email: &Email{EmailAddress: "EmailAddress"}},
email: &Email{EmailAddress: "EmailAddressChanged"},
event: &es_models.Event{},
},
result: &User{Email: &Email{EmailAddress: "EmailAddressChanged"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.email != nil {
data, _ := json.Marshal(tt.args.email)
tt.args.event.Data = data
}
tt.args.user.appendUserEmailChangedEvent(tt.args.event)
if tt.args.user.Email.EmailAddress != tt.result.Email.EmailAddress {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserEmailCodeAddedEvent(t *testing.T) {
type args struct {
user *User
code *EmailCode
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user email code added event",
args: args{
user: &User{Email: &Email{EmailAddress: "EmailAddress"}},
code: &EmailCode{Expiry: time.Hour * 1},
event: &es_models.Event{},
},
result: &User{EmailCode: &EmailCode{Expiry: time.Hour * 1}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.code != nil {
data, _ := json.Marshal(tt.args.code)
tt.args.event.Data = data
}
tt.args.user.appendUserEmailCodeAddedEvent(tt.args.event)
if tt.args.user.EmailCode.Expiry != tt.result.EmailCode.Expiry {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserEmailVerifiedEvent(t *testing.T) {
type args struct {
user *User
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user email event",
args: args{
user: &User{Email: &Email{EmailAddress: "EmailAddress"}},
event: &es_models.Event{},
},
result: &User{Email: &Email{EmailAddress: "EmailAddress", IsEmailVerified: true}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendUserEmailVerifiedEvent()
if tt.args.user.Email.IsEmailVerified != tt.result.Email.IsEmailVerified {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@ -0,0 +1,57 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
)
type OTP struct {
es_models.ObjectRoot
Secret *crypto.CryptoValue `json:"otpSecret,omitempty"`
State int32 `json:"-"`
}
func OTPFromModel(otp *model.OTP) *OTP {
return &OTP{
ObjectRoot: otp.ObjectRoot,
Secret: otp.Secret,
State: int32(otp.State),
}
}
func OTPToModel(otp *OTP) *model.OTP {
return &model.OTP{
ObjectRoot: otp.ObjectRoot,
Secret: otp.Secret,
State: model.MfaState(otp.State),
}
}
func (u *User) appendOtpAddedEvent(event *es_models.Event) error {
u.OTP = &OTP{
State: int32(model.MFASTATE_NOTREADY),
}
return u.OTP.setData(event)
}
func (u *User) appendOtpVerifiedEvent() {
u.OTP.State = int32(model.MFASTATE_READY)
}
func (u *User) appendOtpRemovedEvent() {
u.OTP = nil
}
func (o *OTP) setData(event *es_models.Event) error {
o.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, o); err != nil {
logging.Log("EVEN-d9soe").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-lo023", "could not unmarshal event")
}
return nil
}

View File

@ -0,0 +1,109 @@
package model
import (
"encoding/json"
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
"testing"
)
func TestAppendMfaOTPAddedEvent(t *testing.T) {
type args struct {
user *User
otp *OTP
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user otp event",
args: args{
user: &User{},
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
event: &es_models.Event{},
},
result: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFASTATE_NOTREADY)}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.otp != nil {
data, _ := json.Marshal(tt.args.otp)
tt.args.event.Data = data
}
tt.args.user.appendOtpAddedEvent(tt.args.event)
if tt.args.user.OTP.State != tt.result.OTP.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.OTP.State, tt.args.user.OTP.State)
}
})
}
}
func TestAppendMfaOTPVerifyEvent(t *testing.T) {
type args struct {
user *User
otp *OTP
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append otp verify event",
args: args{
user: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
otp: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}},
event: &es_models.Event{},
},
result: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}, State: int32(model.MFASTATE_READY)}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.otp != nil {
data, _ := json.Marshal(tt.args.otp)
tt.args.event.Data = data
}
tt.args.user.appendOtpVerifiedEvent()
if tt.args.user.OTP.State != tt.result.OTP.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.OTP.State, tt.args.user.OTP.State)
}
})
}
}
func TestAppendMfaOTPRemoveEvent(t *testing.T) {
type args struct {
user *User
otp *OTP
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append otp verify event",
args: args{
user: &User{OTP: &OTP{Secret: &crypto.CryptoValue{KeyID: "KeyID"}}},
event: &es_models.Event{},
},
result: &User{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendOtpRemovedEvent()
if tt.args.user.OTP != nil {
t.Errorf("got wrong result: actual: %v ", tt.result.OTP)
}
})
}
}

View File

@ -0,0 +1,84 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
"time"
)
type Password struct {
es_models.ObjectRoot
Secret *crypto.CryptoValue `json:"secret,omitempty"`
ChangeRequired bool `json:"changeRequired,omitempty"`
}
type PasswordCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
NotificationType int32 `json:"notificationType,omitempty"`
}
func PasswordFromModel(password *model.Password) *Password {
return &Password{
ObjectRoot: password.ObjectRoot,
Secret: password.SecretCrypto,
ChangeRequired: password.ChangeRequired,
}
}
func PasswordToModel(password *Password) *model.Password {
return &model.Password{
ObjectRoot: password.ObjectRoot,
SecretCrypto: password.Secret,
ChangeRequired: password.ChangeRequired,
}
}
func PasswordCodeToModel(code *PasswordCode) *model.PasswordCode {
return &model.PasswordCode{
ObjectRoot: code.ObjectRoot,
Expiry: code.Expiry,
Code: code.Code,
NotificationType: model.NotificationType(code.NotificationType),
}
}
func (u *User) appendUserPasswordChangedEvent(event *es_models.Event) error {
u.Password = new(Password)
err := u.Password.setData(event)
if err != nil {
return err
}
u.Password.ObjectRoot.CreationDate = event.CreationDate
return nil
}
func (u *User) appendPasswordSetRequestedEvent(event *es_models.Event) error {
u.PasswordCode = new(PasswordCode)
return u.PasswordCode.setData(event)
}
func (pw *Password) setData(event *es_models.Event) error {
pw.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, pw); err != nil {
logging.Log("EVEN-dks93").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-sl9xlo2rsw", "could not unmarshal event")
}
return nil
}
func (a *PasswordCode) setData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-lo0y2").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-q21dr", "could not unmarshal event")
}
return nil
}

View File

@ -0,0 +1,78 @@
package model
import (
"encoding/json"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"testing"
"time"
)
func TestAppendUserPasswordChangedEvent(t *testing.T) {
type args struct {
user *User
pw *Password
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append init user code event",
args: args{
user: &User{},
pw: &Password{ChangeRequired: true},
event: &es_models.Event{},
},
result: &User{Password: &Password{ChangeRequired: true}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.pw != nil {
data, _ := json.Marshal(tt.args.pw)
tt.args.event.Data = data
}
tt.args.user.appendUserPasswordChangedEvent(tt.args.event)
if tt.args.user.Password.ChangeRequired != tt.result.Password.ChangeRequired {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendPasswordSetRequestedEvent(t *testing.T) {
type args struct {
user *User
code *PasswordCode
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user phone code added event",
args: args{
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
code: &PasswordCode{Expiry: time.Hour * 1},
event: &es_models.Event{},
},
result: &User{PasswordCode: &PasswordCode{Expiry: time.Hour * 1}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.code != nil {
data, _ := json.Marshal(tt.args.code)
tt.args.event.Data = data
}
tt.args.user.appendPasswordSetRequestedEvent(tt.args.event)
if tt.args.user.PasswordCode.Expiry != tt.result.PasswordCode.Expiry {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@ -0,0 +1,100 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
"time"
)
type Phone struct {
es_models.ObjectRoot
PhoneNumber string `json:"phone,omitempty"`
IsPhoneVerified bool `json:"-"`
}
type PhoneCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
}
func (p *Phone) Changes(changed *Phone) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if changed.PhoneNumber != "" && p.PhoneNumber != changed.PhoneNumber {
changes["phone"] = changed.PhoneNumber
}
return changes
}
func PhoneFromModel(phone *model.Phone) *Phone {
return &Phone{
ObjectRoot: phone.ObjectRoot,
PhoneNumber: phone.PhoneNumber,
IsPhoneVerified: phone.IsPhoneVerified,
}
}
func PhoneToModel(phone *Phone) *model.Phone {
return &model.Phone{
ObjectRoot: phone.ObjectRoot,
PhoneNumber: phone.PhoneNumber,
IsPhoneVerified: phone.IsPhoneVerified,
}
}
func PhoneCodeFromModel(code *model.PhoneCode) *PhoneCode {
if code == nil {
return nil
}
return &PhoneCode{
ObjectRoot: code.ObjectRoot,
Expiry: code.Expiry,
Code: code.Code,
}
}
func PhoneCodeToModel(code *PhoneCode) *model.PhoneCode {
return &model.PhoneCode{
ObjectRoot: code.ObjectRoot,
Expiry: code.Expiry,
Code: code.Code,
}
}
func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
u.Phone = new(Phone)
return u.Phone.setData(event)
}
func (u *User) appendUserPhoneCodeAddedEvent(event *es_models.Event) error {
u.PhoneCode = new(PhoneCode)
return u.PhoneCode.setData(event)
}
func (u *User) appendUserPhoneVerifiedEvent() {
u.IsPhoneVerified = true
}
func (p *Phone) setData(event *es_models.Event) error {
p.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, p); err != nil {
logging.Log("EVEN-lco9s").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-lre56", "could not unmarshal event")
}
return nil
}
func (a *PhoneCode) setData(event *es_models.Event) error {
a.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, a); err != nil {
logging.Log("EVEN-sk8ws").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-7hdj3", "could not unmarshal event")
}
return nil
}

View File

@ -0,0 +1,152 @@
package model
import (
"encoding/json"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"testing"
"time"
)
func TestPhoneChanges(t *testing.T) {
type args struct {
existing *Phone
new *Phone
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all fields changed",
args: args{
existing: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
new: &Phone{PhoneNumber: "PhoneChanged", IsPhoneVerified: false},
},
res: res{
changesLen: 1,
},
},
{
name: "no fields changed",
args: args{
existing: &Phone{PhoneNumber: "Phone", IsPhoneVerified: true},
new: &Phone{PhoneNumber: "Phone", IsPhoneVerified: false},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existing.Changes(tt.args.new)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}
func TestAppendUserPhoneChangedEvent(t *testing.T) {
type args struct {
user *User
phone *Phone
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user phone event",
args: args{
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
phone: &Phone{PhoneNumber: "PhoneNumberChanged"},
event: &es_models.Event{},
},
result: &User{Phone: &Phone{PhoneNumber: "PhoneNumberChanged"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.phone != nil {
data, _ := json.Marshal(tt.args.phone)
tt.args.event.Data = data
}
tt.args.user.appendUserPhoneChangedEvent(tt.args.event)
if tt.args.user.Phone.PhoneNumber != tt.result.Phone.PhoneNumber {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserPhoneCodeAddedEvent(t *testing.T) {
type args struct {
user *User
code *PhoneCode
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user phone code added event",
args: args{
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
code: &PhoneCode{Expiry: time.Hour * 1},
event: &es_models.Event{},
},
result: &User{PhoneCode: &PhoneCode{Expiry: time.Hour * 1}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.code != nil {
data, _ := json.Marshal(tt.args.code)
tt.args.event.Data = data
}
tt.args.user.appendUserPhoneCodeAddedEvent(tt.args.event)
if tt.args.user.PhoneCode.Expiry != tt.result.PhoneCode.Expiry {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUserPhoneVerifiedEvent(t *testing.T) {
type args struct {
user *User
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append user phone event",
args: args{
user: &User{Phone: &Phone{PhoneNumber: "PhoneNumber"}},
event: &es_models.Event{},
},
result: &User{Phone: &Phone{PhoneNumber: "PhoneNumber", IsPhoneVerified: true}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendUserPhoneVerifiedEvent()
if tt.args.user.Phone.IsPhoneVerified != tt.result.Phone.IsPhoneVerified {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@ -0,0 +1,70 @@
package model
import (
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
"golang.org/x/text/language"
)
type Profile struct {
es_models.ObjectRoot
UserName string `json:"userName,omitempty"`
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
NickName string `json:"nickName,omitempty"`
DisplayName string `json:"displayName,omitempty"`
PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"`
Gender int32 `json:"gender,omitempty"`
isUserNameUnique bool `json:"-"`
}
func (p *Profile) Changes(changed *Profile) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if changed.FirstName != "" && p.FirstName != changed.FirstName {
changes["firstName"] = changed.FirstName
}
if changed.LastName != "" && p.LastName != changed.LastName {
changes["lastName"] = changed.LastName
}
if changed.NickName != p.NickName {
changes["nickName"] = changed.NickName
}
if changed.DisplayName != p.DisplayName {
changes["displayName"] = changed.DisplayName
}
if p.PreferredLanguage != language.Und && changed.PreferredLanguage != p.PreferredLanguage {
changes["preferredLanguage"] = changed.PreferredLanguage
}
if p.Gender > 0 && changed.Gender != p.Gender {
changes["gender"] = changed.Gender
}
return changes
}
func ProfileFromModel(profile *model.Profile) *Profile {
return &Profile{
ObjectRoot: profile.ObjectRoot,
UserName: profile.UserName,
FirstName: profile.FirstName,
LastName: profile.LastName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: profile.PreferredLanguage,
Gender: int32(profile.Gender),
}
}
func ProfileToModel(profile *Profile) *model.Profile {
return &model.Profile{
ObjectRoot: profile.ObjectRoot,
UserName: profile.UserName,
FirstName: profile.FirstName,
LastName: profile.LastName,
NickName: profile.NickName,
DisplayName: profile.DisplayName,
PreferredLanguage: profile.PreferredLanguage,
Gender: model.Gender(profile.Gender),
}
}

View File

@ -0,0 +1,71 @@
package model
import (
user_model "github.com/caos/zitadel/internal/user/model"
"golang.org/x/text/language"
"testing"
)
func TestProfileChanges(t *testing.T) {
type args struct {
existing *Profile
new *Profile
}
type res struct {
changesLen int
}
tests := []struct {
name string
args args
res res
}{
{
name: "all attributes changed",
args: args{
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
new: &Profile{UserName: "UserNameChanged", FirstName: "FirstNameChanged", LastName: "LastNameChanged", NickName: "NickNameChanged", DisplayName: "DisplayNameChanged", PreferredLanguage: language.English, Gender: int32(user_model.GENDER_MALE)},
},
res: res{
changesLen: 6,
},
},
{
name: "no changes",
args: args{
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
new: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
},
res: res{
changesLen: 0,
},
},
{
name: "username changed",
args: args{
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
new: &Profile{UserName: "UserNameChanged", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
},
res: res{
changesLen: 0,
},
},
{
name: "empty names",
args: args{
existing: &Profile{UserName: "UserName", FirstName: "FirstName", LastName: "LastName", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
new: &Profile{UserName: "UserName", FirstName: "", LastName: "", NickName: "NickName", DisplayName: "DisplayName", PreferredLanguage: language.German, Gender: int32(user_model.GENDER_FEMALE)},
},
res: res{
changesLen: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changes := tt.args.existing.Changes(tt.args.new)
if len(changes) != tt.res.changesLen {
t.Errorf("got wrong changes len: expected: %v, actual: %v ", tt.res.changesLen, len(changes))
}
})
}
}

View File

@ -0,0 +1,47 @@
package model
import "github.com/caos/zitadel/internal/eventstore/models"
const (
UserAggregate models.AggregateType = "user"
UserUserNameAggregate models.AggregateType = "user.username"
UserEmailAggregate models.AggregateType = "user.email"
UserAdded models.EventType = "user.added"
UserRegistered models.EventType = "user.selfregistered"
InitializedUserCodeAdded models.EventType = "user.initialization.code.added"
InitializedUserCodeSent models.EventType = "user.initialization.code.sent"
UserUserNameReserved models.EventType = "user.username.reserved"
UserUserNameReleased models.EventType = "user.username.released"
UserEmailReserved models.EventType = "user.email.reserved"
UserEmailReleased models.EventType = "user.email.released"
UserLocked models.EventType = "user.locked"
UserUnlocked models.EventType = "user.unlocked"
UserDeactivated models.EventType = "user.deactivated"
UserReactivated models.EventType = "user.reactivated"
UserDeleted models.EventType = "user.deleted"
UserPasswordChanged models.EventType = "user.password.changed"
UserPasswordCodeAdded models.EventType = "user.password.code.added"
UserPasswordCodeSent models.EventType = "user.password.code.sent"
UserEmailChanged models.EventType = "user.email.changed"
UserEmailVerified models.EventType = "user.email.verified"
UserEmailCodeAdded models.EventType = "user.email.code.added"
UserEmailCodeSent models.EventType = "user.email.code.sent"
UserPhoneChanged models.EventType = "user.phone.changed"
UserPhoneVerified models.EventType = "user.phone.verified"
UserPhoneCodeAdded models.EventType = "user.phone.code.added"
UserPhoneCodeSent models.EventType = "user.phone.code.sent"
UserProfileChanged models.EventType = "user.profile.changed"
UserAddressChanged models.EventType = "user.address.changed"
MfaOtpAdded models.EventType = "user.mfa.otp.added"
MfaOtpVerified models.EventType = "user.mfa.otp.verified"
MfaOtpRemoved models.EventType = "user.mfa.otp.removed"
MfaInitSkipped models.EventType = "user.mfa.init.skipped"
)

View File

@ -0,0 +1,246 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
"time"
)
const (
UserVersion = "v1"
)
type User struct {
es_models.ObjectRoot
State int32 `json:"-"`
*Password
*Profile
*Email
*Phone
*Address
InitCode *InitUserCode
EmailCode *EmailCode
PhoneCode *PhoneCode
PasswordCode *PasswordCode
OTP *OTP
}
type InitUserCode struct {
es_models.ObjectRoot
Code *crypto.CryptoValue `json:"code,omitempty"`
Expiry time.Duration `json:"expiry,omitempty"`
}
func UserFromModel(user *model.User) *User {
converted := &User{
ObjectRoot: user.ObjectRoot,
State: int32(user.State),
}
if user.Password != nil {
converted.Password = PasswordFromModel(user.Password)
}
if user.Profile != nil {
converted.Profile = ProfileFromModel(user.Profile)
}
if user.Email != nil {
converted.Email = EmailFromModel(user.Email)
}
if user.Phone != nil {
converted.Phone = PhoneFromModel(user.Phone)
}
if user.Address != nil {
converted.Address = AddressFromModel(user.Address)
}
if user.OTP != nil {
converted.OTP = OTPFromModel(user.OTP)
}
return converted
}
func UserToModel(user *User) *model.User {
converted := &model.User{
ObjectRoot: user.ObjectRoot,
State: model.UserState(user.State),
}
if user.Password != nil {
converted.Password = PasswordToModel(user.Password)
}
if user.Profile != nil {
converted.Profile = ProfileToModel(user.Profile)
}
if user.Email != nil {
converted.Email = EmailToModel(user.Email)
}
if user.Phone != nil {
converted.Phone = PhoneToModel(user.Phone)
}
if user.Address != nil {
converted.Address = AddressToModel(user.Address)
}
if user.InitCode != nil {
converted.InitCode = InitCodeToModel(user.InitCode)
}
if user.EmailCode != nil {
converted.EmailCode = EmailCodeToModel(user.EmailCode)
}
if user.PhoneCode != nil {
converted.PhoneCode = PhoneCodeToModel(user.PhoneCode)
}
if user.PasswordCode != nil {
converted.PasswordCode = PasswordCodeToModel(user.PasswordCode)
}
if user.OTP != nil {
converted.OTP = OTPToModel(user.OTP)
}
return converted
}
func InitCodeFromModel(code *model.InitUserCode) *InitUserCode {
if code == nil {
return nil
}
return &InitUserCode{
ObjectRoot: code.ObjectRoot,
Expiry: code.Expiry,
Code: code.Code,
}
}
func InitCodeToModel(code *InitUserCode) *model.InitUserCode {
return &model.InitUserCode{
ObjectRoot: code.ObjectRoot,
Expiry: code.Expiry,
Code: code.Code,
}
}
func (p *User) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
if err := p.AppendEvent(event); err != nil {
return err
}
}
return nil
}
func (u *User) AppendEvent(event *es_models.Event) (err error) {
u.ObjectRoot.AppendEvent(event)
switch event.Type {
case UserAdded,
UserRegistered,
UserProfileChanged:
u.setData(event)
case UserDeactivated:
u.appendDeactivatedEvent()
case UserReactivated:
u.appendReactivatedEvent()
case UserLocked:
u.appendLockedEvent()
case UserUnlocked:
u.appendUnlockedEvent()
case InitializedUserCodeAdded:
u.appendInitUsercodeCreatedEvent(event)
case UserPasswordChanged:
err = u.appendUserPasswordChangedEvent(event)
case UserPasswordCodeAdded:
err = u.appendPasswordSetRequestedEvent(event)
case UserEmailChanged:
err = u.appendUserEmailChangedEvent(event)
case UserEmailCodeAdded:
err = u.appendUserEmailCodeAddedEvent(event)
case UserEmailVerified:
u.appendUserEmailVerifiedEvent()
case UserPhoneChanged:
err = u.appendUserPhoneChangedEvent(event)
case UserPhoneCodeAdded:
err = u.appendUserPhoneCodeAddedEvent(event)
case UserPhoneVerified:
u.appendUserPhoneVerifiedEvent()
case UserAddressChanged:
err = u.appendUserAddressChangedEvent(event)
case MfaOtpAdded:
err = u.appendOtpAddedEvent(event)
case MfaOtpVerified:
u.appendOtpVerifiedEvent()
case MfaOtpRemoved:
u.appendOtpRemovedEvent()
}
if err != nil {
return err
}
u.ComputeObject()
return nil
}
func (u *User) ComputeObject() {
if u.State == 0 {
if u.Email != nil && u.IsEmailVerified {
u.State = int32(model.USERSTATE_ACTIVE)
} else {
u.State = int32(model.USERSTATE_INITIAL)
}
}
if u.Password != nil && u.Password.ObjectRoot.IsZero() {
u.Password.ObjectRoot = u.ObjectRoot
}
if u.Profile != nil && u.Profile.ObjectRoot.IsZero() {
u.Profile.ObjectRoot = u.ObjectRoot
}
if u.Email != nil && u.Email.ObjectRoot.IsZero() {
u.Email.ObjectRoot = u.ObjectRoot
}
if u.Phone != nil && u.Phone.ObjectRoot.IsZero() {
u.Phone.ObjectRoot = u.ObjectRoot
}
if u.Address != nil && u.Address.ObjectRoot.IsZero() {
u.Address.ObjectRoot = u.ObjectRoot
}
}
func (u *User) setData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, u); err != nil {
logging.Log("EVEN-8ujgd").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-sj4jd", "could not unmarshal event")
}
return nil
}
func (u *User) appendDeactivatedEvent() {
u.State = int32(model.USERSTATE_INACTIVE)
}
func (u *User) appendReactivatedEvent() {
u.State = int32(model.USERSTATE_ACTIVE)
}
func (u *User) appendLockedEvent() {
u.State = int32(model.USERSTATE_LOCKED)
}
func (u *User) appendUnlockedEvent() {
u.State = int32(model.USERSTATE_ACTIVE)
}
func (u *User) appendInitUsercodeCreatedEvent(event *es_models.Event) error {
initCode := new(InitUserCode)
err := initCode.setData(event)
if err != nil {
return err
}
initCode.ObjectRoot.CreationDate = event.CreationDate
u.InitCode = initCode
return nil
}
func (c *InitUserCode) setData(event *es_models.Event) error {
c.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, c); err != nil {
logging.Log("EVEN-7duwe").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-lo34s", "could not unmarshal event")
}
return nil
}

View File

@ -0,0 +1,152 @@
package model
import (
"encoding/json"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/model"
"testing"
"time"
)
func TestAppendDeactivatedEvent(t *testing.T) {
type args struct {
user *User
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append deactivate event",
args: args{
user: &User{},
},
result: &User{State: int32(model.USERSTATE_INACTIVE)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendDeactivatedEvent()
if tt.args.user.State != tt.result.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendReactivatedEvent(t *testing.T) {
type args struct {
user *User
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append reactivate event",
args: args{
user: &User{},
},
result: &User{State: int32(model.USERSTATE_ACTIVE)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendReactivatedEvent()
if tt.args.user.State != tt.result.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendLockEvent(t *testing.T) {
type args struct {
user *User
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append lock event",
args: args{
user: &User{},
},
result: &User{State: int32(model.USERSTATE_LOCKED)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendLockedEvent()
if tt.args.user.State != tt.result.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendUnlockEvent(t *testing.T) {
type args struct {
user *User
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append unlock event",
args: args{
user: &User{},
},
result: &User{State: int32(model.USERSTATE_ACTIVE)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.user.appendUnlockedEvent()
if tt.args.user.State != tt.result.State {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}
func TestAppendInitUserCodeEvent(t *testing.T) {
type args struct {
user *User
code *InitUserCode
event *es_models.Event
}
tests := []struct {
name string
args args
result *User
}{
{
name: "append init user code event",
args: args{
user: &User{},
code: &InitUserCode{Expiry: time.Hour * 30},
event: &es_models.Event{},
},
result: &User{InitCode: &InitUserCode{Expiry: time.Hour * 30}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.code != nil {
data, _ := json.Marshal(tt.args.code)
tt.args.event.Data = data
}
tt.args.user.appendInitUsercodeCreatedEvent(tt.args.event)
if tt.args.user.InitCode.Expiry != tt.result.InitCode.Expiry {
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, tt.args.user)
}
})
}
}

View File

@ -0,0 +1,348 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
)
func UserByIDQuery(id string, latestSequence uint64) (*es_models.SearchQuery, error) {
if id == "" {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d8isw", "id should be filled")
}
return UserQuery(latestSequence).
AggregateIDFilter(id), nil
}
func UserQuery(latestSequence uint64) *es_models.SearchQuery {
return es_models.NewSearchQuery().
AggregateTypeFilter(model.UserAggregate).
LatestSequenceFilter(latestSequence)
}
func UserAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User) (*es_models.Aggregate, error) {
if user == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dis83", "existing user should not be nil")
}
return aggCreator.NewAggregate(ctx, user.AggregateID, model.UserAggregate, model.UserVersion, user.Sequence)
}
func UserAggregateOverwriteContext(ctx context.Context, aggCreator *es_models.AggregateCreator, user *model.User, resourceOwnerID string, userID string) (*es_models.Aggregate, error) {
if user == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dis83", "existing user should not be nil")
}
return aggCreator.NewAggregate(ctx, user.AggregateID, model.UserAggregate, model.UserVersion, user.Sequence, es_models.OverwriteResourceOwner(resourceOwnerID), es_models.OverwriteEditorUser(userID))
}
func UserCreateAggregate(aggCreator *es_models.AggregateCreator, user *model.User, initCode *model.InitUserCode, phoneCode *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if user == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, user)
if err != nil {
return nil, err
}
agg, err = agg.AppendEvent(model.UserAdded, user)
if err != nil {
return nil, err
}
if user.Email != nil && user.EmailAddress != "" && user.IsEmailVerified {
agg, err = agg.AppendEvent(model.UserEmailVerified, nil)
if err != nil {
return nil, err
}
}
if user.Phone != nil && user.PhoneNumber != "" && user.IsPhoneVerified {
agg, err = agg.AppendEvent(model.UserPhoneVerified, nil)
if err != nil {
return nil, err
}
}
if user.Password != nil {
agg, err = agg.AppendEvent(model.UserPasswordCodeAdded, user.Password)
if err != nil {
return nil, err
}
}
if initCode != nil {
agg, err = agg.AppendEvent(model.InitializedUserCodeAdded, initCode)
if err != nil {
return nil, err
}
}
if phoneCode != nil {
agg, err = agg.AppendEvent(model.UserPhoneCodeAdded, phoneCode)
if err != nil {
return nil, err
}
}
return agg, err
}
}
func UserRegisterAggregate(aggCreator *es_models.AggregateCreator, user *model.User, resourceOwner string, emailCode *model.EmailCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if user == nil || resourceOwner == "" || emailCode == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-duxk2", "user, resourceowner, emailcode should not be nothing")
}
agg, err := UserAggregateOverwriteContext(ctx, aggCreator, user, resourceOwner, user.AggregateID)
if err != nil {
return nil, err
}
agg, err = agg.AppendEvent(model.UserRegistered, user)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserEmailCodeAdded, emailCode)
}
}
func UserDeactivateAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return userStateAggregate(aggCreator, user, model.UserDeactivated)
}
func UserReactivateAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return userStateAggregate(aggCreator, user, model.UserReactivated)
}
func UserLockAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return userStateAggregate(aggCreator, user, model.UserLocked)
}
func UserUnlockAggregate(aggCreator *es_models.AggregateCreator, user *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return userStateAggregate(aggCreator, user, model.UserUnlocked)
}
func userStateAggregate(aggCreator *es_models.AggregateCreator, user *model.User, state es_models.EventType) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, user)
if err != nil {
return nil, err
}
return agg.AppendEvent(state, nil)
}
}
func UserInitCodeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, code *model.InitUserCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if code == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d8i23", "code should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.InitializedUserCodeAdded, code)
}
}
func SkipMfaAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.MfaInitSkipped, nil)
}
}
func PasswordChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, password *model.Password) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if password == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d9832", "password should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserPasswordChanged, password)
}
}
func RequestSetPassword(aggCreator *es_models.AggregateCreator, existing *model.User, request *model.PasswordCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if request == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d8ei2", "password set request should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserPasswordCodeAdded, request)
}
}
func ProfileChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, profile *model.Profile) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if profile == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dhr74", "profile should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
changes := existing.Profile.Changes(profile)
return agg.AppendEvent(model.UserProfileChanged, changes)
}
}
func EmailChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, email *model.Email, code *model.EmailCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if email == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dki8s", "email should not be nil")
}
if (!email.IsEmailVerified && code == nil) || (email.IsEmailVerified && code != nil) {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-id934", "email has to be verified or code must be sent")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
changes := existing.Email.Changes(email)
agg, err = agg.AppendEvent(model.UserEmailChanged, changes)
if err != nil {
return nil, err
}
if existing.Email == nil {
existing.Email = new(model.Email)
}
if email.IsEmailVerified {
return agg.AppendEvent(model.UserEmailVerified, code)
}
if code != nil {
return agg.AppendEvent(model.UserEmailCodeAdded, code)
}
return agg, nil
}
}
func EmailVerifiedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserEmailVerified, nil)
}
}
func EmailVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, code *model.EmailCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if code == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dki8s", "code should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserEmailCodeAdded, code)
}
}
func PhoneChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, phone *model.Phone, code *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if phone == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dkso3", "phone should not be nil")
}
if (!phone.IsPhoneVerified && code == nil) || (phone.IsPhoneVerified && code != nil) {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dksi8", "phone has to be verified or code must be sent")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
if existing.Phone == nil {
existing.Phone = new(model.Phone)
}
changes := existing.Phone.Changes(phone)
agg, err = agg.AppendEvent(model.UserPhoneChanged, changes)
if err != nil {
return nil, err
}
if phone.IsPhoneVerified {
return agg.AppendEvent(model.UserPhoneVerified, code)
}
if code != nil {
return agg.AppendEvent(model.UserPhoneCodeAdded, code)
}
return agg, nil
}
}
func PhoneVerifiedAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserPhoneVerified, nil)
}
}
func PhoneVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, code *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if code == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dsue2", "code should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserPhoneCodeAdded, code)
}
}
func AddressChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, address *model.Address) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if address == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dkx9s", "address should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
if existing.Address == nil {
existing.Address = new(model.Address)
}
changes := existing.Address.Changes(address)
return agg.AppendEvent(model.UserAddressChanged, changes)
}
}
func MfaOTPAddAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, otp *model.OTP) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if otp == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dkx9s", "otp should not be nil")
}
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.MfaOtpAdded, otp)
}
}
func MfaOTPVerifyAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.MfaOtpVerified, nil)
}
}
func MfaOTPRemoveAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.MfaOtpRemoved, nil)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
package model
import es_models "github.com/caos/zitadel/internal/eventstore/models"
type UserGrant struct {
es_models.ObjectRoot
State UserGrantState
UserID string
ProjectID string
RoleKeys []string
}
type UserGrantState int32
const (
USERGRANTSTATE_ACTIVE UserGrantState = iota
USERGRANTSTATE_INACTIVE
USERGRANTSTATE_REMOVED
)
func (u *UserGrant) IsValid() bool {
return u.ProjectID != "" && u.UserID != ""
}
func (u *UserGrant) IsActive() bool {
return u.State == USERGRANTSTATE_ACTIVE
}
func (u *UserGrant) IsInactive() bool {
return u.State == USERGRANTSTATE_INACTIVE
}

View File

@ -0,0 +1,34 @@
package eventsourcing
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/cache"
"github.com/caos/zitadel/internal/cache/config"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
)
type UserGrantCache struct {
userGrantCache cache.Cache
}
func StartCache(conf *config.CacheConfig) (*UserGrantCache, error) {
userGrantCache, err := conf.Config.NewCache()
logging.Log("EVENT-vDneN").OnError(err).Panic("unable to create user cache")
return &UserGrantCache{userGrantCache: userGrantCache}, nil
}
func (c *UserGrantCache) getUserGrant(ID string) *model.UserGrant {
user := &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: ID}}
err := c.userGrantCache.Get(ID, user)
logging.Log("EVENT-4eTZh").OnError(err).Debug("error in getting cache")
return user
}
func (c *UserGrantCache) cacheUserGrant(grant *model.UserGrant) {
err := c.userGrantCache.Set(grant.AggregateID, grant)
logging.Log("EVENT-ThnBb").OnError(err).Debug("error in setting project cache")
}

View File

@ -0,0 +1,159 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/cache/config"
caos_errs "github.com/caos/zitadel/internal/errors"
es_int "github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/models"
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
"github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
"github.com/sony/sonyflake"
"strconv"
)
type UserGrantEventStore struct {
es_int.Eventstore
userGrantCache *UserGrantCache
idGenerator *sonyflake.Sonyflake
}
type UserGrantConfig struct {
es_int.Eventstore
Cache *config.CacheConfig
}
func StartUserGrant(conf UserGrantConfig) (*UserGrantEventStore, error) {
userGrantCache, err := StartCache(conf.Cache)
if err != nil {
return nil, err
}
idGenerator := sonyflake.NewSonyflake(sonyflake.Settings{})
return &UserGrantEventStore{
Eventstore: conf.Eventstore,
userGrantCache: userGrantCache,
idGenerator: idGenerator,
}, nil
}
func (es *UserGrantEventStore) UserGrantByID(ctx context.Context, id string) (*grant_model.UserGrant, error) {
grant := es.userGrantCache.getUserGrant(id)
query, err := UserGrantByIDQuery(grant.AggregateID, grant.Sequence)
if err != nil {
return nil, err
}
err = es_sdk.Filter(ctx, es.FilterEvents, grant.AppendEvents, query)
if err != nil && caos_errs.IsNotFound(err) && grant.Sequence == 0 {
return nil, err
}
es.userGrantCache.cacheUserGrant(grant)
if grant.State == int32(grant_model.USERGRANTSTATE_REMOVED) {
return nil, caos_errs.ThrowNotFound(nil, "EVENT-2ks8d", "UserGrant not found")
}
return model.UserGrantToModel(grant), nil
}
func (es *UserGrantEventStore) AddUserGrant(ctx context.Context, grant *grant_model.UserGrant) (*grant_model.UserGrant, error) {
if grant == nil || !grant.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-sdiw3", "User grant invalid")
}
//TODO: Check Uniqueness
id, err := es.idGenerator.NextID()
if err != nil {
return nil, err
}
grant.AggregateID = strconv.FormatUint(id, 10)
repoGrant := model.UserGrantFromModel(grant)
addAggregate := UserGrantAddedAggregate(es.Eventstore.AggregateCreator(), repoGrant)
err = es_sdk.Push(ctx, es.PushAggregates, repoGrant.AppendEvents, addAggregate)
if err != nil {
return nil, err
}
return model.UserGrantToModel(repoGrant), nil
}
func (es *UserGrantEventStore) ChangeUserGrant(ctx context.Context, grant *grant_model.UserGrant) (*grant_model.UserGrant, error) {
if grant == nil {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo0s9", "invalid grant")
}
existing, err := es.UserGrantByID(ctx, grant.AggregateID)
if err != nil {
return nil, err
}
repoExisting := model.UserGrantFromModel(existing)
repoGrant := model.UserGrantFromModel(grant)
projectAggregate := UserGrantChangedAggregate(es.Eventstore.AggregateCreator(), repoExisting, repoGrant)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, projectAggregate)
if err != nil {
return nil, err
}
es.userGrantCache.cacheUserGrant(repoExisting)
return model.UserGrantToModel(repoExisting), nil
}
func (es *UserGrantEventStore) RemoveUserGrant(ctx context.Context, grantID string) error {
existing, err := es.UserGrantByID(ctx, grantID)
if err != nil {
return err
}
repoExisting := model.UserGrantFromModel(existing)
repoGrant := &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: grantID}}
projectAggregate := UserGrantRemovedAggregate(es.Eventstore.AggregateCreator(), repoExisting, repoGrant)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, projectAggregate)
if err != nil {
return err
}
es.userGrantCache.cacheUserGrant(repoExisting)
return nil
}
func (es *UserGrantEventStore) DeactivateUserGrant(ctx context.Context, grantID string) (*grant_model.UserGrant, error) {
if grantID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-8si34", "grantID missing")
}
existing, err := es.UserGrantByID(ctx, grantID)
if err != nil {
return nil, err
}
if !existing.IsActive() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9sw", "deactivate only possible for active grant")
}
repoExisting := model.UserGrantFromModel(existing)
repoGrant := &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: grantID}}
projectAggregate := UserGrantDeactivatedAggregate(es.Eventstore.AggregateCreator(), repoExisting, repoGrant)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, projectAggregate)
if err != nil {
return nil, err
}
es.userGrantCache.cacheUserGrant(repoGrant)
return model.UserGrantToModel(repoExisting), nil
}
func (es *UserGrantEventStore) ReactivateUserGrant(ctx context.Context, grantID string) (*grant_model.UserGrant, error) {
if grantID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-sksiw", "grantID missing")
}
existing, err := es.UserGrantByID(ctx, grantID)
if err != nil {
return nil, err
}
if !existing.IsInactive() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-lo9sw", "reactivate only possible for inactive grant")
}
repoExisting := model.UserGrantFromModel(existing)
repoGrant := &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: grantID}}
projectAggregate := UserGrantReactivatedAggregate(es.Eventstore.AggregateCreator(), repoExisting, repoGrant)
err = es_sdk.Push(ctx, es.PushAggregates, repoExisting.AppendEvents, projectAggregate)
if err != nil {
return nil, err
}
es.userGrantCache.cacheUserGrant(repoExisting)
return model.UserGrantToModel(repoExisting), nil
}

View File

@ -0,0 +1,112 @@
package eventsourcing
import (
"encoding/json"
mock_cache "github.com/caos/zitadel/internal/cache/mock"
"github.com/caos/zitadel/internal/eventstore/mock"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
"github.com/golang/mock/gomock"
"github.com/sony/sonyflake"
)
func GetMockedEventstore(ctrl *gomock.Controller, mockEs *mock.MockEventstore) *UserGrantEventStore {
return &UserGrantEventStore{
Eventstore: mockEs,
userGrantCache: GetMockCache(ctrl),
idGenerator: GetSonyFlacke(),
}
}
func GetMockCache(ctrl *gomock.Controller) *UserGrantCache {
mockCache := mock_cache.NewMockCache(ctrl)
mockCache.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockCache.EXPECT().Set(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
return &UserGrantCache{userGrantCache: mockCache}
}
func GetSonyFlacke() *sonyflake.Sonyflake {
return sonyflake.NewSonyflake(sonyflake.Settings{})
}
func GetMockUserGrantByIDOK(ctrl *gomock.Controller) *UserGrantEventStore {
user := model.UserGrant{
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserGrantAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockUserGrantByIDRemoved(ctrl *gomock.Controller) *UserGrantEventStore {
user := model.UserGrant{
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserGrantAdded, Data: data},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserGrantRemoved},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockUserGrantByIDNoEvents(ctrl *gomock.Controller) *UserGrantEventStore {
events := []*es_models.Event{}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserGrant(ctrl *gomock.Controller) *UserGrantEventStore {
user := model.UserGrant{
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserGrantAdded, Data: data},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserGrantInactive(ctrl *gomock.Controller) *UserGrantEventStore {
user := model.UserGrant{
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"},
}
data, _ := json.Marshal(user)
events := []*es_models.Event{
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserGrantAdded, Data: data},
&es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: model.UserGrantDeactivated},
}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}
func GetMockManipulateUserGrantNoEvents(ctrl *gomock.Controller) *UserGrantEventStore {
events := []*es_models.Event{}
mockEs := mock.NewMockEventstore(ctrl)
mockEs.EXPECT().FilterEvents(gomock.Any(), gomock.Any()).Return(events, nil)
mockEs.EXPECT().AggregateCreator().Return(es_models.NewAggregateCreator("TEST"))
mockEs.EXPECT().PushAggregates(gomock.Any(), gomock.Any()).Return(nil)
return GetMockedEventstore(ctrl, mockEs)
}

View File

@ -0,0 +1,439 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/model"
"github.com/golang/mock/gomock"
"reflect"
"testing"
)
func TestUserByID(t *testing.T) {
ctrl := gomock.NewController(t)
type args struct {
es *UserGrantEventStore
grant *model.UserGrant
}
type res struct {
grant *model.UserGrant
wantErr bool
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "user from events, ok",
args: args{
es: GetMockUserGrantByIDOK(ctrl),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, UserID: "UserID", ProjectID: "ProjectID", RoleKeys: []string{"Key"}},
},
},
{
name: "no events found",
args: args{
es: GetMockUserGrantByIDNoEvents(ctrl),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsNotFound,
},
},
{
name: "no id",
args: args{
es: GetMockUserGrantByIDOK(ctrl),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "removed state",
args: args{
es: GetMockUserGrantByIDRemoved(ctrl),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.args.es.UserGrantByID(nil, tt.args.grant.AggregateID)
if !tt.res.wantErr && result.AggregateID != tt.res.grant.AggregateID {
t.Errorf("got wrong result name: expected: %v, actual: %v ", tt.res.grant.AggregateID, result.AggregateID)
}
if tt.res.wantErr && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestAddUserGrant(t *testing.T) {
ctrl := gomock.NewController(t)
type args struct {
es *UserGrantEventStore
ctx context.Context
grant *model.UserGrant
}
type res struct {
result *model.UserGrant
wantErr bool
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "add grant, ok",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
ProjectID: "ProjectID",
UserID: "UserID",
RoleKeys: []string{"Key"},
},
},
res: res{
result: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
ProjectID: "ProjectID",
UserID: "UserID",
RoleKeys: []string{"Key"},
},
},
},
{
name: "invalid grant",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.args.es.AddUserGrant(tt.args.ctx, tt.args.grant)
if !tt.res.wantErr && result.AggregateID == "" {
t.Errorf("result has no id")
}
if !tt.res.wantErr && result.UserID == "" {
t.Errorf("result has no id")
}
if tt.res.wantErr && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestChangeUserGrant(t *testing.T) {
ctrl := gomock.NewController(t)
type args struct {
es *UserGrantEventStore
ctx context.Context
grant *model.UserGrant
}
type res struct {
result *model.UserGrant
wantErr bool
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "change grant, ok",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
RoleKeys: []string{"KeyChanged"},
},
},
res: res{
result: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
RoleKeys: []string{"KeyChanged"},
},
},
},
{
name: "invalid grant",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: nil,
},
res: res{
wantErr: true,
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "existing user not found",
args: args{
es: GetMockManipulateUserGrantNoEvents(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
RoleKeys: []string{"KeyChanged"},
},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.args.es.ChangeUserGrant(tt.args.ctx, tt.args.grant)
if !tt.res.wantErr && result.AggregateID == "" {
t.Errorf("result has no id")
}
if !tt.res.wantErr && !reflect.DeepEqual(result.RoleKeys, tt.res.result.RoleKeys) {
t.Errorf("got wrong result name: expected: %v, actual: %v ", tt.res.result.RoleKeys, result.RoleKeys)
}
if tt.res.wantErr && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestRemoveUserGrant(t *testing.T) {
ctrl := gomock.NewController(t)
type args struct {
es *UserGrantEventStore
ctx context.Context
grant *model.UserGrant
}
type res struct {
wantErr bool
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "remove grant, ok",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
},
{
name: "no grantID",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "existing grant not found",
args: args{
es: GetMockManipulateUserGrantNoEvents(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.args.es.RemoveUserGrant(tt.args.ctx, tt.args.grant.AggregateID)
if !tt.res.wantErr && err != nil {
t.Errorf("should not get err")
}
if tt.res.wantErr && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestDeactivateUserGrant(t *testing.T) {
ctrl := gomock.NewController(t)
type args struct {
es *UserGrantEventStore
ctx context.Context
grant *model.UserGrant
}
type res struct {
result *model.UserGrant
wantErr bool
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "deactivate, ok",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
result: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
ProjectID: "ProjectID",
State: model.USERGRANTSTATE_INACTIVE,
},
},
},
{
name: "no grant id",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "grant not existing",
args: args{
es: GetMockManipulateUserGrantNoEvents(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.args.es.DeactivateUserGrant(tt.args.ctx, tt.args.grant.AggregateID)
if !tt.res.wantErr && result.AggregateID == "" {
t.Errorf("result has no id")
}
if !tt.res.wantErr && result.ProjectID != tt.res.result.ProjectID {
t.Errorf("got wrong result AppID: expected: %v, actual: %v ", tt.res.result.ProjectID, result.ProjectID)
}
if !tt.res.wantErr && result.State != tt.res.result.State {
t.Errorf("got wrong result state: expected: %v, actual: %v ", tt.res.result.State, result.State)
}
if tt.res.wantErr && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestReactivateUserGrant(t *testing.T) {
ctrl := gomock.NewController(t)
type args struct {
es *UserGrantEventStore
ctx context.Context
grant *model.UserGrant
}
type res struct {
result *model.UserGrant
wantErr bool
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "reactivate, ok",
args: args{
es: GetMockManipulateUserGrantInactive(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
result: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1},
State: model.USERGRANTSTATE_ACTIVE,
},
},
},
{
name: "no grant id",
args: args{
es: GetMockManipulateUserGrant(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "grant not existing",
args: args{
es: GetMockManipulateUserGrantNoEvents(ctrl),
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
},
res: res{
wantErr: true,
errFunc: caos_errs.IsNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := tt.args.es.ReactivateUserGrant(tt.args.ctx, tt.args.grant.AggregateID)
if !tt.res.wantErr && result.AggregateID == "" {
t.Errorf("result has no id")
}
if !tt.res.wantErr && result.State != tt.res.result.State {
t.Errorf("got wrong result state: expected: %v, actual: %v ", tt.res.result.State, result.State)
}
if tt.res.wantErr && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}

View File

@ -0,0 +1,14 @@
package model
import "github.com/caos/zitadel/internal/eventstore/models"
const (
UserGrantAggregate models.AggregateType = "usergrant"
UserGrantUniqueAggregate models.AggregateType = "usergrant-unique"
UserGrantAdded models.EventType = "user.grant.added"
UserGrantChanged models.EventType = "user.grant.changed"
UserGrantRemoved models.EventType = "user.grant.removed"
UserGrantDeactivated models.EventType = "user.grant.deactivated"
UserGrantReactivated models.EventType = "user.grant.reactivated"
)

View File

@ -0,0 +1,93 @@
package model
import (
"encoding/json"
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/model"
"reflect"
)
const (
UserGrantVersion = "v1"
)
type UserGrant struct {
es_models.ObjectRoot
State int32 `json:"-"`
UserID string `json:"userId,omitempty"`
ProjectID string `json:"projectId,omitempty"`
RoleKeys []string `json:"roleKeys,omitempty"`
}
type UserGrantID struct {
es_models.ObjectRoot
GrantID string `json:"grantId"`
}
func (g *UserGrant) Changes(changed *UserGrant) map[string]interface{} {
changes := make(map[string]interface{}, 1)
if !reflect.DeepEqual(g.RoleKeys, changed.RoleKeys) {
changes["roleKeys"] = changed.RoleKeys
}
return changes
}
func UserGrantFromModel(grant *model.UserGrant) *UserGrant {
return &UserGrant{
ObjectRoot: grant.ObjectRoot,
UserID: grant.UserID,
ProjectID: grant.ProjectID,
State: int32(grant.State),
RoleKeys: grant.RoleKeys,
}
}
func UserGrantToModel(grant *UserGrant) *model.UserGrant {
return &model.UserGrant{
ObjectRoot: grant.ObjectRoot,
UserID: grant.UserID,
ProjectID: grant.ProjectID,
State: model.UserGrantState(grant.State),
RoleKeys: grant.RoleKeys,
}
}
func (g *UserGrant) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
if err := g.AppendEvent(event); err != nil {
return err
}
}
return nil
}
func (g *UserGrant) AppendEvent(event *es_models.Event) error {
g.ObjectRoot.AppendEvent(event)
switch event.Type {
case UserGrantAdded,
UserGrantChanged:
return g.setData(event)
case UserGrantDeactivated:
g.appendGrantStateEvent(model.USERGRANTSTATE_INACTIVE)
case UserGrantReactivated:
g.appendGrantStateEvent(model.USERGRANTSTATE_ACTIVE)
case UserGrantRemoved:
g.appendGrantStateEvent(model.USERGRANTSTATE_REMOVED)
}
return nil
}
func (g *UserGrant) appendGrantStateEvent(state model.UserGrantState) {
g.State = int32(state)
}
func (g *UserGrant) setData(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, g); err != nil {
logging.Log("EVEN-lso9x").WithError(err).Error("could not unmarshal event data")
return caos_errs.ThrowInternal(err, "MODEL-o0se3", "could not unmarshal event")
}
return nil
}

View File

@ -0,0 +1,60 @@
package model
import (
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/model"
"testing"
)
func TestAppendGrantStateEvent(t *testing.T) {
type args struct {
existing *UserGrant
grant *UserGrantID
event *es_models.Event
state model.UserGrantState
}
tests := []struct {
name string
args args
result *UserGrant
}{
{
name: "append deactivate grant event",
args: args{
existing: &UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, UserID: "UserID", ProjectID: "ProjectID", RoleKeys: []string{"Key"}},
grant: &UserGrantID{GrantID: "GrantID"},
event: &es_models.Event{},
state: model.USERGRANTSTATE_INACTIVE,
},
result: &UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, UserID: "UserID", ProjectID: "ProjectID", RoleKeys: []string{"Key"}, State: int32(model.USERGRANTSTATE_INACTIVE)},
},
{
name: "append reactivate grant event",
args: args{
existing: &UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, UserID: "UserID", ProjectID: "ProjectID", RoleKeys: []string{"Key"}},
grant: &UserGrantID{GrantID: "GrantID"},
event: &es_models.Event{},
state: model.USERGRANTSTATE_ACTIVE,
},
result: &UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, UserID: "UserID", ProjectID: "ProjectID", RoleKeys: []string{"Key"}, State: int32(model.USERGRANTSTATE_ACTIVE)},
},
{
name: "append remove grant event",
args: args{
existing: &UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, UserID: "UserID", ProjectID: "ProjectID", RoleKeys: []string{"Key"}},
grant: &UserGrantID{GrantID: "GrantID"},
event: &es_models.Event{},
state: model.USERGRANTSTATE_REMOVED,
},
result: &UserGrant{ObjectRoot: es_models.ObjectRoot{AggregateID: "ID"}, UserID: "UserID", ProjectID: "ProjectID", RoleKeys: []string{"Key"}, State: int32(model.USERGRANTSTATE_REMOVED)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.existing.appendGrantStateEvent(tt.args.state)
if tt.args.existing.State != tt.result.State {
t.Errorf("got wrong result: actual: %v, expected: %v ", tt.result.State, tt.args.existing.State)
}
})
}
}

View File

@ -0,0 +1,92 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
)
func UserGrantByIDQuery(id string, latestSequence uint64) (*es_models.SearchQuery, error) {
if id == "" {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-ols34", "id should be filled")
}
return UserGrantQuery(latestSequence).
AggregateIDFilter(id), nil
}
func UserGrantQuery(latestSequence uint64) *es_models.SearchQuery {
return es_models.NewSearchQuery().
AggregateTypeFilter(model.UserGrantAggregate).
LatestSequenceFilter(latestSequence)
}
func UserGrantAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, grant *model.UserGrant) (*es_models.Aggregate, error) {
if grant == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-dis83", "existing grant should not be nil")
}
return aggCreator.NewAggregate(ctx, grant.AggregateID, model.UserGrantAggregate, model.UserGrantVersion, grant.Sequence)
}
func UserGrantAddedAggregate(aggCreator *es_models.AggregateCreator, grant *model.UserGrant) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := UserGrantAggregate(ctx, aggCreator, grant)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserGrantAdded, grant)
}
}
func UserGrantChangedAggregate(aggCreator *es_models.AggregateCreator, existing *model.UserGrant, grant *model.UserGrant) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if grant == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-osl8x", "grant should not be nil")
}
agg, err := UserGrantAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
changes := existing.Changes(grant)
return agg.AppendEvent(model.UserGrantChanged, changes)
}
}
func UserGrantDeactivatedAggregate(aggCreator *es_models.AggregateCreator, existing *model.UserGrant, grant *model.UserGrant) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if grant == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-lo21s", "grant should not be nil")
}
agg, err := UserGrantAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserGrantDeactivated, nil)
}
}
func UserGrantReactivatedAggregate(aggCreator *es_models.AggregateCreator, existing *model.UserGrant, grant *model.UserGrant) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if grant == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-mks34", "grant should not be nil")
}
agg, err := UserGrantAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserGrantReactivated, nil)
}
}
func UserGrantRemovedAggregate(aggCreator *es_models.AggregateCreator, existing *model.UserGrant, grant *model.UserGrant) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
if grant == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-lo21s", "grant should not be nil")
}
agg, err := UserGrantAggregate(ctx, aggCreator, existing)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.UserGrantRemoved, nil)
}
}

View File

@ -0,0 +1,405 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
"testing"
)
func TestUserGrantAddedAggregate(t *testing.T) {
type args struct {
ctx context.Context
grant *model.UserGrant
aggCreator *models.AggregateCreator
}
type res struct {
eventLen int
eventType models.EventType
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "usergrant added ok",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
grant: &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}, UserID: "UserID", ProjectID: "ProjectID"},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
eventLen: 1,
eventType: model.UserGrantAdded,
},
},
{
name: "grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
grant: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := UserGrantAddedAggregate(tt.args.aggCreator, tt.args.grant)(tt.args.ctx)
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
}
if tt.res.errFunc == nil && agg.Events[0].Type != tt.res.eventType {
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventType, agg.Events[0].Type.String())
}
if tt.res.errFunc == nil && agg.Events[0].Data == nil {
t.Errorf("should have data in event")
}
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestUserGrantChangedAggregate(t *testing.T) {
type args struct {
ctx context.Context
existing *model.UserGrant
new *model.UserGrant
aggCreator *models.AggregateCreator
}
type res struct {
eventLen int
eventTypes []models.EventType
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "change project grant",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"}},
new: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"KeyChanged"},
},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
eventLen: 1,
eventTypes: []models.EventType{model.UserGrantChanged},
},
},
{
name: "existing grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: nil,
new: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"}},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"}},
new: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := UserGrantChangedAggregate(tt.args.aggCreator, tt.args.existing, tt.args.new)(tt.args.ctx)
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
}
for i := 0; i < tt.res.eventLen; i++ {
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
}
if tt.res.errFunc == nil && agg.Events[i].Data == nil {
t.Errorf("should have data in event")
}
}
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestUserGrantRemovedAggregate(t *testing.T) {
type args struct {
ctx context.Context
existing *model.UserGrant
new *model.UserGrant
aggCreator *models.AggregateCreator
}
type res struct {
eventLen int
eventTypes []models.EventType
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "remove app",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"}},
new: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
eventLen: 1,
eventTypes: []models.EventType{model.UserGrantRemoved},
},
},
{
name: "existing project nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
UserID: "UserID",
ProjectID: "ProjectID",
RoleKeys: []string{"Key"}},
new: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := UserGrantRemovedAggregate(tt.args.aggCreator, tt.args.existing, tt.args.new)(tt.args.ctx)
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
}
for i := 0; i < tt.res.eventLen; i++ {
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
}
}
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestUserGrantDeactivatedAggregate(t *testing.T) {
type args struct {
ctx context.Context
existing *model.UserGrant
new *model.UserGrant
aggCreator *models.AggregateCreator
}
type res struct {
eventLen int
eventTypes []models.EventType
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "deactivate project grant",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
},
new: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
eventLen: 1,
eventTypes: []models.EventType{model.UserGrantDeactivated},
},
},
{
name: "existing grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
new: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := UserGrantDeactivatedAggregate(tt.args.aggCreator, tt.args.existing, tt.args.new)(tt.args.ctx)
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
}
for i := 0; i < tt.res.eventLen; i++ {
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
}
}
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func TestUserGrantReactivatedAggregate(t *testing.T) {
type args struct {
ctx context.Context
existing *model.UserGrant
new *model.UserGrant
aggCreator *models.AggregateCreator
}
type res struct {
eventLen int
eventTypes []models.EventType
errFunc func(err error) bool
}
tests := []struct {
name string
args args
res res
}{
{
name: "reactivate project grant",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
},
new: &model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
},
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
eventLen: 1,
eventTypes: []models.EventType{model.UserGrantReactivated},
},
},
{
name: "existing grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
{
name: "grant nil",
args: args{
ctx: auth.NewMockContext("orgID", "userID"),
existing: &model.UserGrant{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
new: nil,
aggCreator: models.NewAggregateCreator("Test"),
},
res: res{
errFunc: caos_errs.IsPreconditionFailed,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
agg, err := UserGrantReactivatedAggregate(tt.args.aggCreator, tt.args.existing, tt.args.new)(tt.args.ctx)
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
}
for i := 0; i < tt.res.eventLen; i++ {
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
}
}
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}

View File

@ -470,6 +470,11 @@ var ManagementService_AuthMethods = utils_auth.MethodMapping{
CheckParam: "",
},
"/caos.zitadel.management.api.v1.ManagementService/RemoveUserGrant": utils_auth.Option{
Permission: "user.grant.delete",
CheckParam: "",
},
"/caos.zitadel.management.api.v1.ManagementService/SearchProjectUserGrants": utils_auth.Option{
Permission: "project.user.grant.read",
CheckParam: "ProjectId",

File diff suppressed because it is too large Load Diff

View File

@ -3039,6 +3039,44 @@ func request_ManagementService_ReactivateUserGrant_0(ctx context.Context, marsha
}
func request_ManagementService_RemoveUserGrant_0(ctx context.Context, marshaler runtime.Marshaler, client ManagementServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq UserGrantID
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["user_id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user_id")
}
protoReq.UserId, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user_id", err)
}
val, ok = pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
msg, err := client.RemoveUserGrant(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func request_ManagementService_SearchProjectUserGrants_0(ctx context.Context, marshaler runtime.Marshaler, client ManagementServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ProjectUserGrantSearchRequest
var metadata runtime.ServerMetadata
@ -5576,6 +5614,26 @@ func RegisterManagementServiceHandlerClient(ctx context.Context, mux *runtime.Se
})
mux.Handle("DELETE", pattern_ManagementService_RemoveUserGrant_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_ManagementService_RemoveUserGrant_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_ManagementService_RemoveUserGrant_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_ManagementService_SearchProjectUserGrants_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
@ -6028,6 +6086,8 @@ var (
pattern_ManagementService_ReactivateUserGrant_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"users", "user_id", "grants", "id", "_reactivate"}, ""))
pattern_ManagementService_RemoveUserGrant_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"users", "user_id", "grants", "id"}, ""))
pattern_ManagementService_SearchProjectUserGrants_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 2, 3, 2, 4}, []string{"projects", "project_id", "users", "grants", "_search"}, ""))
pattern_ManagementService_ProjectUserGrantByID_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 1, 0, 4, 1, 5, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"projects", "project_id", "users", "user_id", "grants", "id"}, ""))
@ -6244,6 +6304,8 @@ var (
forward_ManagementService_ReactivateUserGrant_0 = runtime.ForwardResponseMessage
forward_ManagementService_RemoveUserGrant_0 = runtime.ForwardResponseMessage
forward_ManagementService_SearchProjectUserGrants_0 = runtime.ForwardResponseMessage
forward_ManagementService_ProjectUserGrantByID_0 = runtime.ForwardResponseMessage

View File

@ -3185,6 +3185,34 @@
"ManagementService"
]
},
"delete": {
"operationId": "RemoveUserGrant",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"properties": {}
}
}
},
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "id",
"in": "path",
"required": true,
"type": "string"
}
],
"tags": [
"ManagementService"
]
},
"put": {
"operationId": "UpdateUserGrant",
"responses": {
@ -5477,6 +5505,14 @@
"sequence": {
"type": "string",
"format": "uint64"
},
"creation_date": {
"type": "string",
"format": "date-time"
},
"change_date": {
"type": "string",
"format": "date-time"
}
}
},
@ -5496,6 +5532,14 @@
"sequence": {
"type": "string",
"format": "uint64"
},
"creation_date": {
"type": "string",
"format": "date-time"
},
"change_date": {
"type": "string",
"format": "date-time"
}
}
},
@ -5564,9 +5608,6 @@
"user_id": {
"type": "string"
},
"org_id": {
"type": "string"
},
"project_id": {
"type": "string"
},
@ -5705,6 +5746,14 @@
"sequence": {
"type": "string",
"format": "uint64"
},
"creation_date": {
"type": "string",
"format": "date-time"
},
"change_date": {
"type": "string",
"format": "date-time"
}
}
},
@ -5714,9 +5763,6 @@
"id": {
"type": "string"
},
"state": {
"$ref": "#/definitions/v1UserState"
},
"first_name": {
"type": "string"
},
@ -5741,6 +5787,14 @@
"sequence": {
"type": "string",
"format": "uint64"
},
"creation_date": {
"type": "string",
"format": "date-time"
},
"change_date": {
"type": "string",
"format": "date-time"
}
}
},

View File

@ -1537,6 +1537,26 @@ func (mr *MockManagementServiceClientMockRecorder) RemoveProjectRole(arg0, arg1
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveProjectRole", reflect.TypeOf((*MockManagementServiceClient)(nil).RemoveProjectRole), varargs...)
}
// RemoveUserGrant mocks base method
func (m *MockManagementServiceClient) RemoveUserGrant(arg0 context.Context, arg1 *grpc.UserGrantID, arg2 ...grpc0.CallOption) (*emptypb.Empty, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "RemoveUserGrant", varargs...)
ret0, _ := ret[0].(*emptypb.Empty)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RemoveUserGrant indicates an expected call of RemoveUserGrant
func (mr *MockManagementServiceClientMockRecorder) RemoveUserGrant(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUserGrant", reflect.TypeOf((*MockManagementServiceClient)(nil).RemoveUserGrant), varargs...)
}
// ResendEmailVerificationMail mocks base method
func (m *MockManagementServiceClient) ResendEmailVerificationMail(arg0 context.Context, arg1 *grpc.UserID, arg2 ...grpc0.CallOption) (*emptypb.Empty, error) {
m.ctrl.T.Helper()

View File

@ -13,18 +13,22 @@ import (
var _ ManagementServiceServer = (*Server)(nil)
type Server struct {
port string
project repository.ProjectRepository
verifier *mgmt_auth.TokenVerifier
authZ auth.Config
port string
project repository.ProjectRepository
user repository.UserRepository
usergrant repository.UserGrantRepository
verifier *mgmt_auth.TokenVerifier
authZ auth.Config
}
func StartServer(conf grpc_util.ServerConfig, authZ auth.Config, repo repository.Repository) *Server {
return &Server{
port: conf.Port,
project: repo,
authZ: authZ,
verifier: mgmt_auth.Start(),
port: conf.Port,
project: repo,
user: repo,
usergrant: repo,
authZ: authZ,
verifier: mgmt_auth.Start(),
}
}

View File

@ -6,8 +6,12 @@ import (
"github.com/golang/protobuf/ptypes/empty"
)
func (s *Server) GetUserByID(ctx context.Context, userID *UserID) (*User, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-0oVbs", "Not implemented")
func (s *Server) GetUserByID(ctx context.Context, id *UserID) (*User, error) {
user, err := s.user.UserByID(ctx, id.Id)
if err != nil {
return nil, err
}
return userFromModel(user), nil
}
func (s *Server) GetUserByEmailGlobal(ctx context.Context, email *UserEmailID) (*User, error) {
@ -26,76 +30,132 @@ func (s *Server) IsUserUnique(ctx context.Context, request *UniqueUserRequest) (
return nil, errors.ThrowUnimplemented(nil, "GRPC-olF56", "Not implemented")
}
func (s *Server) CreateUser(ctx context.Context, request *CreateUserRequest) (*User, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-sd4fs", "Not implemented")
func (s *Server) CreateUser(ctx context.Context, in *CreateUserRequest) (*User, error) {
user, err := s.user.CreateUser(ctx, userCreateToModel(in))
if err != nil {
return nil, err
}
return userFromModel(user), nil
}
func (s *Server) DeactivateUser(ctx context.Context, ID *UserID) (*User, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-Vgh64", "Not implemented")
func (s *Server) DeactivateUser(ctx context.Context, in *UserID) (*User, error) {
user, err := s.user.DeactivateUser(ctx, in.Id)
if err != nil {
return nil, err
}
return userFromModel(user), nil
}
func (s *Server) ReactivateUser(ctx context.Context, ID *UserID) (*User, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-mCx4f", "Not implemented")
func (s *Server) ReactivateUser(ctx context.Context, in *UserID) (*User, error) {
user, err := s.user.ReactivateUser(ctx, in.Id)
if err != nil {
return nil, err
}
return userFromModel(user), nil
}
func (s *Server) LockUser(ctx context.Context, ID *UserID) (*User, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-ds4fd", "Not implemented")
func (s *Server) LockUser(ctx context.Context, in *UserID) (*User, error) {
user, err := s.user.LockUser(ctx, in.Id)
if err != nil {
return nil, err
}
return userFromModel(user), nil
}
func (s *Server) UnlockUser(ctx context.Context, ID *UserID) (*User, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-MV7dd", "Not implemented")
func (s *Server) UnlockUser(ctx context.Context, in *UserID) (*User, error) {
user, err := s.user.UnlockUser(ctx, in.Id)
if err != nil {
return nil, err
}
return userFromModel(user), nil
}
func (s *Server) DeleteUser(ctx context.Context, ID *UserID) (*empty.Empty, error) {
func (s *Server) DeleteUser(ctx context.Context, in *UserID) (*empty.Empty, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-as4fg", "Not implemented")
}
func (s *Server) GetUserProfile(ctx context.Context, ID *UserID) (*UserProfile, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-mT67d", "Not implemented")
func (s *Server) GetUserProfile(ctx context.Context, in *UserID) (*UserProfile, error) {
profile, err := s.user.ProfileByID(ctx, in.Id)
if err != nil {
return nil, err
}
return profileFromModel(profile), nil
}
func (s *Server) UpdateUserProfile(ctx context.Context, request *UpdateUserProfileRequest) (*UserProfile, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-asje3", "Not implemented")
profile, err := s.user.ChangeProfile(ctx, updateProfileToModel(request))
if err != nil {
return nil, err
}
return profileFromModel(profile), nil
}
func (s *Server) GetUserEmail(ctx context.Context, ID *UserID) (*UserEmail, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-peo9d", "Not implemented")
func (s *Server) GetUserEmail(ctx context.Context, in *UserID) (*UserEmail, error) {
email, err := s.user.EmailByID(ctx, in.Id)
if err != nil {
return nil, err
}
return emailFromModel(email), nil
}
func (s *Server) ChangeUserEmail(ctx context.Context, request *UpdateUserEmailRequest) (*UserEmail, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-cloeS", "Not implemented")
email, err := s.user.ChangeEmail(ctx, updateEmailToModel(request))
if err != nil {
return nil, err
}
return emailFromModel(email), nil
}
func (s *Server) ResendEmailVerificationMail(ctx context.Context, ID *UserID) (*empty.Empty, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-dwsP9", "Not implemented")
func (s *Server) ResendEmailVerificationMail(ctx context.Context, in *UserID) (*empty.Empty, error) {
err := s.user.CreateEmailVerificationCode(ctx, in.Id)
return &empty.Empty{}, err
}
func (s *Server) GetUserPhone(ctx context.Context, ID *UserID) (*UserPhone, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-wlf7f", "Not implemented")
func (s *Server) GetUserPhone(ctx context.Context, in *UserID) (*UserPhone, error) {
phone, err := s.user.PhoneByID(ctx, in.Id)
if err != nil {
return nil, err
}
return phoneFromModel(phone), nil
}
func (s *Server) ChangeUserPhone(ctx context.Context, request *UpdateUserPhoneRequest) (*UserPhone, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-pld5g", "Not implemented")
phone, err := s.user.ChangePhone(ctx, updatePhoneToModel(request))
if err != nil {
return nil, err
}
return phoneFromModel(phone), nil
}
func (s *Server) ResendPhoneVerificationCode(ctx context.Context, ID *UserID) (*empty.Empty, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-98hdE", "Not implemented")
func (s *Server) ResendPhoneVerificationCode(ctx context.Context, in *UserID) (*empty.Empty, error) {
err := s.user.CreatePhoneVerificationCode(ctx, in.Id)
return &empty.Empty{}, err
}
func (s *Server) GetUserAddress(ctx context.Context, ID *UserID) (*UserAddress, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-plt67", "Not implemented")
func (s *Server) GetUserAddress(ctx context.Context, in *UserID) (*UserAddress, error) {
address, err := s.user.AddressByID(ctx, in.Id)
if err != nil {
return nil, err
}
return addressFromModel(address), nil
}
func (s *Server) UpdateUserAddress(ctx context.Context, request *UpdateUserAddressRequest) (*UserAddress, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-dleo3", "Not implemented")
address, err := s.user.ChangeAddress(ctx, updateAddressToModel(request))
if err != nil {
return nil, err
}
return addressFromModel(address), nil
}
func (s *Server) SendSetPasswordNotification(ctx context.Context, request *SetPasswordNotificationRequest) (*empty.Empty, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-LSe7s", "Not implemented")
err := s.user.RequestSetPassword(ctx, request.Id, notifyTypeToModel(request.Type))
return &empty.Empty{}, err
}
func (s *Server) SetInitialPassword(ctx context.Context, request *PasswordRequest) (*empty.Empty, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-ldo3s", "Not implemented")
_, err := s.user.SetOneTimePassword(ctx, passwordRequestToModel(request))
return &empty.Empty{}, err
}
func (s *Server) GetUserMfas(ctx context.Context, userID *UserID) (*MultiFactors, error) {

View File

@ -0,0 +1,258 @@
package grpc
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/golang/protobuf/ptypes"
"golang.org/x/text/language"
)
func userFromModel(user *usr_model.User) *User {
creationDate, err := ptypes.TimestampProto(user.CreationDate)
logging.Log("GRPC-8duwe").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(user.ChangeDate)
logging.Log("GRPC-ckoe3d").OnError(err).Debug("unable to parse timestamp")
converted := &User{
Id: user.AggregateID,
State: userStateFromModel(user.State),
CreationDate: creationDate,
ChangeDate: changeDate,
Sequence: user.Sequence,
UserName: user.UserName,
FirstName: user.FirstName,
LastName: user.LastName,
DisplayName: user.DisplayName,
NickName: user.NickName,
PreferredLanguage: user.PreferredLanguage.String(),
Gender: genderFromModel(user.Gender),
}
if user.Email != nil {
converted.Email = user.EmailAddress
converted.IsEmailVerified = user.IsEmailVerified
}
if user.Phone != nil {
converted.Phone = user.PhoneNumber
converted.IsPhoneVerified = user.IsPhoneVerified
}
if user.Address != nil {
converted.Country = user.Country
converted.Locality = user.Locality
converted.PostalCode = user.PostalCode
converted.Region = user.Region
converted.StreetAddress = user.StreetAddress
}
return converted
}
func userCreateToModel(u *CreateUserRequest) *usr_model.User {
preferredLanguage, err := language.Parse(u.PreferredLanguage)
logging.Log("GRPC-cK5k2").OnError(err).Debug("language malformed")
user := &usr_model.User{
Profile: &usr_model.Profile{
UserName: u.UserName,
FirstName: u.FirstName,
LastName: u.LastName,
NickName: u.NickName,
DisplayName: u.DisplayName,
PreferredLanguage: preferredLanguage,
Gender: genderToModel(u.Gender),
},
Email: &usr_model.Email{
EmailAddress: u.Email,
IsEmailVerified: u.IsEmailVerified,
},
Address: &usr_model.Address{
Country: u.Country,
Locality: u.Locality,
PostalCode: u.PostalCode,
Region: u.Region,
StreetAddress: u.StreetAddress,
},
}
if u.Password != "" {
user.Password = &usr_model.Password{SecretString: u.Password}
}
if u.Phone != "" {
user.Phone = &usr_model.Phone{PhoneNumber: u.Phone, IsPhoneVerified: u.IsPhoneVerified}
}
return user
}
func passwordRequestToModel(r *PasswordRequest) *usr_model.Password {
return &usr_model.Password{
ObjectRoot: models.ObjectRoot{AggregateID: r.Id},
SecretString: r.Password,
}
}
func profileFromModel(profile *usr_model.Profile) *UserProfile {
creationDate, err := ptypes.TimestampProto(profile.CreationDate)
logging.Log("GRPC-dkso3").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(profile.ChangeDate)
logging.Log("GRPC-ski8d").OnError(err).Debug("unable to parse timestamp")
return &UserProfile{
Id: profile.AggregateID,
CreationDate: creationDate,
ChangeDate: changeDate,
Sequence: profile.Sequence,
UserName: profile.UserName,
FirstName: profile.FirstName,
LastName: profile.LastName,
DisplayName: profile.DisplayName,
NickName: profile.NickName,
PreferredLanguage: profile.PreferredLanguage.String(),
Gender: genderFromModel(profile.Gender),
}
}
func updateProfileToModel(u *UpdateUserProfileRequest) *usr_model.Profile {
preferredLanguage, err := language.Parse(u.PreferredLanguage)
logging.Log("GRPC-d8k2s").OnError(err).Debug("language malformed")
return &usr_model.Profile{
ObjectRoot: models.ObjectRoot{AggregateID: u.Id},
FirstName: u.FirstName,
LastName: u.LastName,
NickName: u.NickName,
DisplayName: u.DisplayName,
PreferredLanguage: preferredLanguage,
Gender: genderToModel(u.Gender),
}
}
func emailFromModel(email *usr_model.Email) *UserEmail {
creationDate, err := ptypes.TimestampProto(email.CreationDate)
logging.Log("GRPC-d9ow2").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(email.ChangeDate)
logging.Log("GRPC-s0dkw").OnError(err).Debug("unable to parse timestamp")
return &UserEmail{
Id: email.AggregateID,
CreationDate: creationDate,
ChangeDate: changeDate,
Sequence: email.Sequence,
Email: email.EmailAddress,
IsEmailVerified: email.IsEmailVerified,
}
}
func updateEmailToModel(e *UpdateUserEmailRequest) *usr_model.Email {
return &usr_model.Email{
ObjectRoot: models.ObjectRoot{AggregateID: e.Id},
EmailAddress: e.Email,
IsEmailVerified: e.IsEmailVerified,
}
}
func phoneFromModel(phone *usr_model.Phone) *UserPhone {
creationDate, err := ptypes.TimestampProto(phone.CreationDate)
logging.Log("GRPC-ps9ws").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(phone.ChangeDate)
logging.Log("GRPC-09ewq").OnError(err).Debug("unable to parse timestamp")
return &UserPhone{
Id: phone.AggregateID,
CreationDate: creationDate,
ChangeDate: changeDate,
Sequence: phone.Sequence,
Phone: phone.PhoneNumber,
IsPhoneVerified: phone.IsPhoneVerified,
}
}
func updatePhoneToModel(e *UpdateUserPhoneRequest) *usr_model.Phone {
return &usr_model.Phone{
ObjectRoot: models.ObjectRoot{AggregateID: e.Id},
PhoneNumber: e.Phone,
IsPhoneVerified: e.IsPhoneVerified,
}
}
func addressFromModel(address *usr_model.Address) *UserAddress {
creationDate, err := ptypes.TimestampProto(address.CreationDate)
logging.Log("GRPC-ud8w7").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(address.ChangeDate)
logging.Log("GRPC-si9ws").OnError(err).Debug("unable to parse timestamp")
return &UserAddress{
Id: address.AggregateID,
CreationDate: creationDate,
ChangeDate: changeDate,
Sequence: address.Sequence,
Country: address.Country,
StreetAddress: address.StreetAddress,
Region: address.Region,
PostalCode: address.PostalCode,
Locality: address.Locality,
}
}
func updateAddressToModel(address *UpdateUserAddressRequest) *usr_model.Address {
return &usr_model.Address{
ObjectRoot: models.ObjectRoot{AggregateID: address.Id},
Country: address.Country,
StreetAddress: address.StreetAddress,
Region: address.Region,
PostalCode: address.PostalCode,
Locality: address.Locality,
}
}
func notifyTypeToModel(state NotificationType) usr_model.NotificationType {
switch state {
case NotificationType_NOTIFICATIONTYPE_EMAIL:
return usr_model.NOTIFICATIONTYPE_EMAIL
case NotificationType_NOTIFICATIONTYPE_SMS:
return usr_model.NOTIFICATIONTYPE_SMS
default:
return usr_model.NOTIFICATIONTYPE_EMAIL
}
}
func userStateFromModel(state usr_model.UserState) UserState {
switch state {
case usr_model.USERSTATE_ACTIVE:
return UserState_USERSTATE_ACTIVE
case usr_model.USERSTATE_INACTIVE:
return UserState_USERSTATE_INACTIVE
case usr_model.USERSTATE_LOCKED:
return UserState_USERSTATE_LOCKED
default:
return UserState_USERSTATE_UNSPECIFIED
}
}
func genderFromModel(gender usr_model.Gender) Gender {
switch gender {
case usr_model.GENDER_FEMALE:
return Gender_GENDER_FEMALE
case usr_model.GENDER_MALE:
return Gender_GENDER_MALE
case usr_model.GENDER_DIVERSE:
return Gender_GENDER_DIVERSE
default:
return Gender_GENDER_UNSPECIFIED
}
}
func genderToModel(gender Gender) usr_model.Gender {
switch gender {
case Gender_GENDER_FEMALE:
return usr_model.GENDER_FEMALE
case Gender_GENDER_MALE:
return usr_model.GENDER_MALE
case Gender_GENDER_DIVERSE:
return usr_model.GENDER_DIVERSE
default:
return usr_model.GENDER_UNDEFINED
}
}

View File

@ -3,6 +3,7 @@ package grpc
import (
"context"
"github.com/caos/zitadel/internal/errors"
"github.com/golang/protobuf/ptypes/empty"
)
func (s *Server) SearchUserGrants(ctx context.Context, request *UserGrantSearchRequest) (*UserGrantSearchResponse, error) {
@ -10,20 +11,45 @@ func (s *Server) SearchUserGrants(ctx context.Context, request *UserGrantSearchR
}
func (s *Server) UserGrantByID(ctx context.Context, request *UserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-9dksF", "Not implemented")
user, err := s.usergrant.UserGrantByID(ctx, request.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) CreateUserGrant(ctx context.Context, in *UserGrantCreate) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-2kdl2", "Not implemented")
user, err := s.usergrant.AddUserGrant(ctx, userGrantCreateToModel(in))
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) UpdateUserGrant(ctx context.Context, in *UserGrantUpdate) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-83jsF", "Not implemented")
user, err := s.usergrant.ChangeUserGrant(ctx, userGrantUpdateToModel(in))
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) DeactivateUserGrant(ctx context.Context, in *UserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-93dj3", "Not implemented")
user, err := s.usergrant.DeactivateUserGrant(ctx, in.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) ReactivateUserGrant(ctx context.Context, in *UserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-2kSfs", "Not implemented")
user, err := s.usergrant.ReactivateUserGrant(ctx, in.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) RemoveUserGrant(ctx context.Context, in *UserGrantID) (*empty.Empty, error) {
err := s.usergrant.RemoveUserGrant(ctx, in.Id)
return &empty.Empty{}, err
}
func (s *Server) SearchProjectUserGrants(ctx context.Context, request *ProjectUserGrantSearchRequest) (*UserGrantSearchResponse, error) {
@ -31,22 +57,42 @@ func (s *Server) SearchProjectUserGrants(ctx context.Context, request *ProjectUs
}
func (s *Server) ProjectUserGrantByID(ctx context.Context, request *ProjectUserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-dk32s", "Not implemented")
user, err := s.usergrant.UserGrantByID(ctx, request.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) CreateProjectUserGrant(ctx context.Context, in *UserGrantCreate) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-0or5G", "Not implemented")
user, err := s.usergrant.AddUserGrant(ctx, userGrantCreateToModel(in))
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) UpdateProjectUserGrant(ctx context.Context, in *ProjectUserGrantUpdate) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-asl4D", "Not implemented")
user, err := s.usergrant.ChangeUserGrant(ctx, projectUserGrantUpdateToModel(in))
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) DeactivateProjectUserGrant(ctx context.Context, in *ProjectUserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-2fG6h", "Not implemented")
user, err := s.usergrant.DeactivateUserGrant(ctx, in.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) ReactivateProjectUserGrant(ctx context.Context, in *ProjectUserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-03kSc", "Not implemented")
user, err := s.usergrant.ReactivateUserGrant(ctx, in.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) SearchProjectGrantUserGrants(ctx context.Context, request *ProjectGrantUserGrantSearchRequest) (*UserGrantSearchResponse, error) {
@ -54,20 +100,40 @@ func (s *Server) SearchProjectGrantUserGrants(ctx context.Context, request *Proj
}
func (s *Server) ProjectGrantUserGrantByID(ctx context.Context, request *ProjectGrantUserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-9kfSc", "Not implemented")
user, err := s.usergrant.UserGrantByID(ctx, request.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) CreateProjectGrantUserGrant(ctx context.Context, in *ProjectGrantUserGrantCreate) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-293md", "Not implemented")
user, err := s.usergrant.ChangeUserGrant(ctx, projectGrantUserGrantCreateToModel(in))
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) UpdateProjectGrantUserGrant(ctx context.Context, in *ProjectGrantUserGrantUpdate) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-76fGe", "Not implemented")
user, err := s.usergrant.ChangeUserGrant(ctx, projectGrantUserGrantUpdateToModel(in))
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) DeactivateProjectGrantUserGrant(ctx context.Context, in *ProjectGrantUserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-sFsi3", "Not implemented")
user, err := s.usergrant.DeactivateUserGrant(ctx, in.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}
func (s *Server) ReactivateProjectGrantUserGrant(ctx context.Context, in *ProjectGrantUserGrantID) (*UserGrant, error) {
return nil, errors.ThrowUnimplemented(nil, "GRPC-ckr56", "Not implemented")
user, err := s.usergrant.ReactivateUserGrant(ctx, in.Id)
if err != nil {
return nil, err
}
return usergrantFromModel(user), nil
}

View File

@ -0,0 +1,76 @@
package grpc
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
"github.com/golang/protobuf/ptypes"
)
func usergrantFromModel(grant *grant_model.UserGrant) *UserGrant {
creationDate, err := ptypes.TimestampProto(grant.CreationDate)
logging.Log("GRPC-ki9ds").OnError(err).Debug("unable to parse timestamp")
changeDate, err := ptypes.TimestampProto(grant.ChangeDate)
logging.Log("GRPC-sl9ew").OnError(err).Debug("unable to parse timestamp")
return &UserGrant{
Id: grant.AggregateID,
UserId: grant.UserID,
State: usergrantStateFromModel(grant.State),
CreationDate: creationDate,
ChangeDate: changeDate,
Sequence: grant.Sequence,
ProjectId: grant.ProjectID,
RoleKeys: grant.RoleKeys,
}
}
func userGrantCreateToModel(u *UserGrantCreate) *grant_model.UserGrant {
return &grant_model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: u.UserId},
UserID: u.UserId,
ProjectID: u.ProjectId,
RoleKeys: u.RoleKeys,
}
}
func userGrantUpdateToModel(u *UserGrantUpdate) *grant_model.UserGrant {
return &grant_model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: u.Id},
RoleKeys: u.RoleKeys,
}
}
func projectUserGrantUpdateToModel(u *ProjectUserGrantUpdate) *grant_model.UserGrant {
return &grant_model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: u.Id},
RoleKeys: u.RoleKeys,
}
}
func projectGrantUserGrantCreateToModel(u *ProjectGrantUserGrantCreate) *grant_model.UserGrant {
return &grant_model.UserGrant{
UserID: u.UserId,
ProjectID: u.ProjectId,
RoleKeys: u.RoleKeys,
}
}
func projectGrantUserGrantUpdateToModel(u *ProjectGrantUserGrantUpdate) *grant_model.UserGrant {
return &grant_model.UserGrant{
ObjectRoot: models.ObjectRoot{AggregateID: u.Id},
RoleKeys: u.RoleKeys,
}
}
func usergrantStateFromModel(state grant_model.UserGrantState) UserGrantState {
switch state {
case grant_model.USERGRANTSTATE_ACTIVE:
return UserGrantState_USERGRANTSTATE_ACTIVE
case grant_model.USERGRANTSTATE_INACTIVE:
return UserGrantState_USERGRANTSTATE_INACTIVE
default:
return UserGrantState_USERGRANTSTATE_UNSPECIFIED
}
}

View File

@ -994,7 +994,6 @@ service ManagementService {
};
}
//USER_GRANT
rpc SearchUserGrants(UserGrantSearchRequest) returns (UserGrantSearchResponse) {
option (google.api.http) = {
@ -1061,6 +1060,16 @@ service ManagementService {
};
}
rpc RemoveUserGrant(UserGrantID) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/users/{user_id}/grants/{id}"
};
option (caos.zitadel.utils.v1.auth_option) = {
permission: "user.grant.delete"
};
}
//PROJECT_USER_GRANT
rpc SearchProjectUserGrants(ProjectUserGrantSearchRequest) returns (UserGrantSearchResponse) {
option (google.api.http) = {
@ -1362,15 +1371,16 @@ enum SearchMethod {
message UserProfile {
string id = 1;
UserState state = 2;
string first_name = 3;
string last_name = 4;
string nick_name = 5;
string display_name = 6;
string preferred_language = 7;
Gender gender = 8;
string user_name = 9;
uint64 sequence = 10;
string first_name = 2;
string last_name = 3;
string nick_name = 4;
string display_name = 5;
string preferred_language = 6;
Gender gender = 7;
string user_name = 8;
uint64 sequence = 9;
google.protobuf.Timestamp creation_date = 10;
google.protobuf.Timestamp change_date = 11;
}
message UpdateUserProfileRequest {
@ -1388,6 +1398,8 @@ message UserEmail {
string email = 2;
bool is_email_verified = 3;
uint64 sequence = 4;
google.protobuf.Timestamp creation_date = 5;
google.protobuf.Timestamp change_date = 6;
}
message UpdateUserEmailRequest {
@ -1401,6 +1413,8 @@ message UserPhone {
string phone = 2;
bool is_phone_verified = 3;
uint64 sequence = 5;
google.protobuf.Timestamp creation_date = 6;
google.protobuf.Timestamp change_date = 7;
}
message UpdateUserPhoneRequest {
@ -1417,6 +1431,8 @@ message UserAddress {
string region = 5;
string street_address = 6;
uint64 sequence = 7;
google.protobuf.Timestamp creation_date = 8;
google.protobuf.Timestamp change_date = 9;
}
message UpdateUserAddressRequest {
@ -2089,9 +2105,8 @@ message UserGrant {
message UserGrantCreate {
string user_id = 1;
string org_id = 2;
string project_id = 3;
repeated string role_keys = 4;
string project_id = 2;
repeated string role_keys = 3;
}
message UserGrantUpdate {