mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-30 09:53:59 +00:00
feat(api): add user creation to user service (#5745)
* chore(proto): update versions * change protoc plugin * some cleanups * define api for setting emails in new api * implement user.SetEmail * move SetEmail buisiness logic into command * resuse newCryptoCode * command: add ChangeEmail unit tests Not complete, was not able to mock the generator. * Revert "resuse newCryptoCode" This reverts commit c89e90ae35ae924a3f706a0a7394f933910c2e65. * undo change to crypto code generators * command: use a generator so we can test properly * command: reorganise ChangeEmail improve test coverage * implement VerifyEmail including unit tests * add URL template tests * begin user creation * change protos * implement metadata and move context * merge commands * proto: change context to object * remove old auth option * remove old auth option * fix linting errors run gci on modified files * add permission checks and fix some errors * comments * comments * update email requests * rename proto requests * cleanup and docs * simplify * simplify * fix setup * remove unused proto messages / fields --------- Co-authored-by: adlerhurst <silvan.reusser@gmail.com> Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
This commit is contained in:
parent
19f2f83b61
commit
e4a4b7cfbe
@ -259,6 +259,13 @@ module.exports = {
|
|||||||
sidebarOptions: {
|
sidebarOptions: {
|
||||||
groupPathsBy: "tag",
|
groupPathsBy: "tag",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
specPath: ".artifacts/openapi/zitadel/user/v2alpha/user_service.swagger.json",
|
||||||
|
outputDir: "docs/apis/user_service",
|
||||||
|
sidebarOptions: {
|
||||||
|
groupPathsBy: "tag",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -374,6 +374,20 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
items: require("./docs/apis/system/sidebar.js"),
|
items: require("./docs/apis/system/sidebar.js"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "User Lifecycle (Alpha)",
|
||||||
|
link: {
|
||||||
|
type: "generated-index",
|
||||||
|
title: "User Service API (Alpha)",
|
||||||
|
slug: "/apis/user_service",
|
||||||
|
description:
|
||||||
|
"This API is intended to manage users in a ZITADEL instance.\n"+
|
||||||
|
"\n"+
|
||||||
|
"This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.",
|
||||||
|
},
|
||||||
|
items: require("./docs/apis/user_service/sidebar.js"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "Assets",
|
label: "Assets",
|
||||||
@ -508,4 +522,4 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -210,17 +210,14 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
|
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
|
||||||
details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToAddHuman(req))
|
human := AddHumanUserRequestToAddHuman(req)
|
||||||
|
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &mgmt_pb.AddHumanUserResponse{
|
return &mgmt_pb.AddHumanUserResponse{
|
||||||
UserId: details.ID,
|
UserId: human.ID,
|
||||||
Details: obj_grpc.AddToDetailsPb(
|
Details: obj_grpc.DomainToAddDetailsPb(human.Details),
|
||||||
details.Sequence,
|
|
||||||
details.EventDate,
|
|
||||||
details.ResourceOwner,
|
|
||||||
),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,9 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
|||||||
}
|
}
|
||||||
|
|
||||||
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
|
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
|
||||||
|
if o, ok := req.(AuthContext); ok {
|
||||||
|
orgID = o.AuthContext()
|
||||||
|
}
|
||||||
|
|
||||||
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
|
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -42,3 +45,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
|
|||||||
span.End()
|
span.End()
|
||||||
return handler(ctxSetter(ctx), req)
|
return handler(ctxSetter(ctx), req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthContext interface {
|
||||||
|
AuthContext() string
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||||
resourceOwner := "" // TODO: check if still needed
|
var resourceOwner string // TODO: check if still needed
|
||||||
var email *domain.Email
|
var email *domain.Email
|
||||||
|
|
||||||
switch v := req.GetVerification().(type) {
|
switch v := req.GetVerification().(type) {
|
||||||
|
107
internal/api/grpc/user/v2/user.go
Normal file
107
internal/api/grpc/user/v2/user.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
|
"github.com/zitadel/zitadel/internal/command"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
|
||||||
|
human, err := addUserRequestToAddHuman(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = s.command.AddHuman(ctx, req.GetOrganisation().GetOrgId(), human, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.AddHumanUserResponse{
|
||||||
|
UserId: human.ID,
|
||||||
|
Details: object.DomainToDetailsPb(human.Details),
|
||||||
|
EmailCode: human.EmailCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
|
||||||
|
username := req.GetUsername()
|
||||||
|
if username == "" {
|
||||||
|
username = req.GetEmail().GetEmail()
|
||||||
|
}
|
||||||
|
var urlTemplate string
|
||||||
|
if req.GetEmail().GetSendCode() != nil {
|
||||||
|
urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate()
|
||||||
|
// test the template execution so the async notification will not fail because of it and the user won't realize
|
||||||
|
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bcryptedPassword, err := hashedPasswordToCommand(req.GetHashedPassword())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
|
||||||
|
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
|
||||||
|
for i, metadataEntry := range req.Metadata {
|
||||||
|
metadata[i] = &command.AddMetadataEntry{
|
||||||
|
Key: metadataEntry.GetKey(),
|
||||||
|
Value: metadataEntry.GetValue(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &command.AddHuman{
|
||||||
|
ID: req.GetUserId(),
|
||||||
|
Username: username,
|
||||||
|
FirstName: req.GetProfile().GetFirstName(),
|
||||||
|
LastName: req.GetProfile().GetLastName(),
|
||||||
|
NickName: req.GetProfile().GetNickName(),
|
||||||
|
DisplayName: req.GetProfile().GetDisplayName(),
|
||||||
|
Email: command.Email{
|
||||||
|
Address: domain.EmailAddress(req.GetEmail().GetEmail()),
|
||||||
|
Verified: req.GetEmail().GetIsVerified(),
|
||||||
|
ReturnCode: req.GetEmail().GetReturnCode() != nil,
|
||||||
|
URLTemplate: urlTemplate,
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
|
||||||
|
Gender: genderToDomain(req.GetProfile().GetGender()),
|
||||||
|
Phone: command.Phone{}, // TODO: add as soon as possible
|
||||||
|
Password: req.GetPassword().GetPassword(),
|
||||||
|
BcryptedPassword: bcryptedPassword,
|
||||||
|
PasswordChangeRequired: passwordChangeRequired,
|
||||||
|
Passwordless: false,
|
||||||
|
ExternalIDP: false,
|
||||||
|
Register: false,
|
||||||
|
Metadata: metadata,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genderToDomain(gender user.Gender) domain.Gender {
|
||||||
|
switch gender {
|
||||||
|
case user.Gender_GENDER_UNSPECIFIED:
|
||||||
|
return domain.GenderUnspecified
|
||||||
|
case user.Gender_GENDER_FEMALE:
|
||||||
|
return domain.GenderFemale
|
||||||
|
case user.Gender_GENDER_MALE:
|
||||||
|
return domain.GenderMale
|
||||||
|
case user.Gender_GENDER_DIVERSE:
|
||||||
|
return domain.GenderDiverse
|
||||||
|
default:
|
||||||
|
return domain.GenderUnspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) {
|
||||||
|
if hashed == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
// we currently only handle bcrypt
|
||||||
|
if hashed.GetAlgorithm() != "bcrypt" {
|
||||||
|
return "", errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument")
|
||||||
|
}
|
||||||
|
return hashed.GetHash(), nil
|
||||||
|
}
|
80
internal/api/grpc/user/v2/user_test.go
Normal file
80
internal/api/grpc/user/v2/user_test.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_hashedPasswordToCommand(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
hashed *user.HashedPassword
|
||||||
|
}
|
||||||
|
type res struct {
|
||||||
|
want string
|
||||||
|
err func(error) bool
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
res res
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"not hashed",
|
||||||
|
args{
|
||||||
|
hashed: nil,
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
"",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hashed, not bcrypt",
|
||||||
|
args{
|
||||||
|
hashed: &user.HashedPassword{
|
||||||
|
Hash: "hash",
|
||||||
|
Algorithm: "custom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
"",
|
||||||
|
func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hashed, bcrypt",
|
||||||
|
args{
|
||||||
|
hashed: &user.HashedPassword{
|
||||||
|
Hash: "hash",
|
||||||
|
Algorithm: "bcrypt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
res{
|
||||||
|
"hash",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := hashedPasswordToCommand(tt.args.hashed)
|
||||||
|
if tt.res.err == nil {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
if tt.res.err != nil && !tt.res.err(err) {
|
||||||
|
t.Errorf("got wrong err: %v ", err)
|
||||||
|
}
|
||||||
|
if tt.res.err == nil {
|
||||||
|
assert.Equal(t, tt.res.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,7 @@ type Commands struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|
||||||
checkPermission permissionCheck
|
checkPermission permissionCheck
|
||||||
|
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||||
|
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
static static.Storage
|
static static.Storage
|
||||||
@ -109,6 +110,7 @@ func StartCommands(
|
|||||||
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
|
||||||
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
|
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
|
||||||
},
|
},
|
||||||
|
newEmailCode: newEmailCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
instance_repo.RegisterEventMappers(repo.eventstore)
|
instance_repo.RegisterEventMappers(repo.eventstore)
|
||||||
|
@ -10,24 +10,33 @@ import (
|
|||||||
"github.com/zitadel/zitadel/internal/errors"
|
"github.com/zitadel/zitadel/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
type CryptoCodeWithExpiry struct {
|
||||||
|
Crypted *crypto.CryptoValue
|
||||||
|
Plain string
|
||||||
|
Expiry time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
|
||||||
config, err := secretGeneratorConfig(ctx, filter, typ)
|
config, err := secretGeneratorConfig(ctx, filter, typ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, -1, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
code := new(CryptoCodeWithExpiry)
|
||||||
switch a := alg.(type) {
|
switch a := alg.(type) {
|
||||||
case crypto.HashAlgorithm:
|
case crypto.HashAlgorithm:
|
||||||
value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
||||||
case crypto.EncryptionAlgorithm:
|
case crypto.EncryptionAlgorithm:
|
||||||
value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
||||||
default:
|
default:
|
||||||
return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
|
return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, -1, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return value, config.Expiry, nil
|
|
||||||
|
code.Expiry = config.Expiry
|
||||||
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
|
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
|
||||||
|
@ -2,7 +2,6 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
@ -12,12 +11,18 @@ import (
|
|||||||
type Email struct {
|
type Email struct {
|
||||||
Address domain.EmailAddress
|
Address domain.EmailAddress
|
||||||
Verified bool
|
Verified bool
|
||||||
|
|
||||||
|
// ReturnCode is used if the Verified field is false
|
||||||
|
ReturnCode bool
|
||||||
|
|
||||||
|
// URLTemplate can be used to specify a custom link to be sent in the mail verification
|
||||||
|
URLTemplate string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Email) Validate() error {
|
func (e *Email) Validate() error {
|
||||||
return e.Address.Validate()
|
return e.Address.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
|
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
|
||||||
}
|
}
|
||||||
|
@ -333,8 +333,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
|
|||||||
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
|
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
|
||||||
}
|
}
|
||||||
} else if setup.Org.Human != nil {
|
} else if setup.Org.Human != nil {
|
||||||
|
setup.Org.Human.ID = userAgg.ID
|
||||||
validations = append(validations,
|
validations = append(validations,
|
||||||
AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption),
|
c.AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption, true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user
|
|||||||
var pat *PersonalAccessToken
|
var pat *PersonalAccessToken
|
||||||
var machineKey *MachineKey
|
var machineKey *MachineKey
|
||||||
if o.Human != nil {
|
if o.Human != nil {
|
||||||
validations = append(validations, AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption))
|
validations = append(validations, c.AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption, true))
|
||||||
} else if o.Machine != nil {
|
} else if o.Machine != nil {
|
||||||
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
|
||||||
if o.Machine.Pat != nil {
|
if o.Machine.Pat != nil {
|
||||||
|
@ -2,7 +2,6 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
@ -14,6 +13,6 @@ type Phone struct {
|
|||||||
Verified bool
|
Verified bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
|
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg)
|
||||||
}
|
}
|
||||||
|
@ -439,7 +439,7 @@ func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id
|
|||||||
return exists, nil
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) {
|
func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
|
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddHuman struct {
|
type AddHuman struct {
|
||||||
|
// ID is optional
|
||||||
|
ID string
|
||||||
// Username is required
|
// Username is required
|
||||||
Username string
|
Username string
|
||||||
// FirstName is required
|
// FirstName is required
|
||||||
@ -43,63 +45,99 @@ type AddHuman struct {
|
|||||||
PreferredLanguage language.Tag
|
PreferredLanguage language.Tag
|
||||||
// Gender is required
|
// Gender is required
|
||||||
Gender domain.Gender
|
Gender domain.Gender
|
||||||
//Phone represents an international phone number
|
// Phone represents an international phone number
|
||||||
Phone Phone
|
Phone Phone
|
||||||
//Password is optional
|
// Password is optional
|
||||||
Password string
|
Password string
|
||||||
//BcryptedPassword is optional
|
// BcryptedPassword is optional
|
||||||
BcryptedPassword string
|
BcryptedPassword string
|
||||||
//PasswordChangeRequired is used if the `Password`-field is set
|
// PasswordChangeRequired is used if the `Password`-field is set
|
||||||
PasswordChangeRequired bool
|
PasswordChangeRequired bool
|
||||||
Passwordless bool
|
Passwordless bool
|
||||||
ExternalIDP bool
|
ExternalIDP bool
|
||||||
Register bool
|
Register bool
|
||||||
|
Metadata []*AddMetadataEntry
|
||||||
|
|
||||||
|
// Details are set after a successful execution of the command
|
||||||
|
Details *domain.ObjectDetails
|
||||||
|
|
||||||
|
// EmailCode is set by the command
|
||||||
|
EmailCode *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) AddHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
|
func (h *AddHuman) Validate() (err error) {
|
||||||
existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceOwner)
|
if err := h.Email.Validate(); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if isUserStateExists(existingHuman.UserState) {
|
if h.Username = strings.TrimSpace(h.Username); h.Username == "" {
|
||||||
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
|
return errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.addHumanWithID(ctx, resourceOwner, userID, human)
|
if h.FirstName = strings.TrimSpace(h.FirstName); h.FirstName == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
|
||||||
|
}
|
||||||
|
if h.LastName = strings.TrimSpace(h.LastName); h.LastName == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
|
||||||
|
}
|
||||||
|
h.ensureDisplayName()
|
||||||
|
|
||||||
|
if h.Phone.Number != "" {
|
||||||
|
if h.Phone.Number, err = h.Phone.Number.Normalize(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, metadataEntry := range h.Metadata {
|
||||||
|
if err := metadataEntry.Valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commands) addHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) {
|
type AddMetadataEntry struct {
|
||||||
agg := user.NewAggregate(userID, resourceOwner)
|
Key string
|
||||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddHumanCommand(agg, human, c.userPasswordAlg, c.userEncryption))
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AddMetadataEntry) Valid() error {
|
||||||
|
if m.Key = strings.TrimSpace(m.Key); m.Key == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-Drght", "Errors.User.Metadata.KeyEmpty")
|
||||||
|
}
|
||||||
|
if len(m.Value) == 0 {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "USER-Dbgth", "Errors.User.Metadata.ValueEmpty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool) (err error) {
|
||||||
|
if resourceOwner == "" {
|
||||||
|
return errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
|
||||||
|
}
|
||||||
|
agg := user.NewAggregate(human.ID, resourceOwner)
|
||||||
|
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter,
|
||||||
|
c.AddHumanCommand(
|
||||||
|
agg,
|
||||||
|
human,
|
||||||
|
c.userPasswordAlg,
|
||||||
|
c.userEncryption,
|
||||||
|
allowInitMail,
|
||||||
|
))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
events, err := c.eventstore.Push(ctx, cmds...)
|
events, err := c.eventstore.Push(ctx, cmds...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
|
}
|
||||||
|
human.Details = &domain.ObjectDetails{
|
||||||
|
Sequence: events[len(events)-1].Sequence(),
|
||||||
|
EventDate: events[len(events)-1].CreationDate(),
|
||||||
|
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &domain.HumanDetails{
|
return nil
|
||||||
ID: userID,
|
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
Sequence: events[len(events)-1].Sequence(),
|
|
||||||
EventDate: events[len(events)-1].CreationDate(),
|
|
||||||
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) {
|
|
||||||
if resourceOwner == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")
|
|
||||||
}
|
|
||||||
userID, err := c.idGenerator.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.addHumanWithID(ctx, resourceOwner, userID, human)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type humanCreationCommand interface {
|
type humanCreationCommand interface {
|
||||||
@ -108,30 +146,17 @@ type humanCreationCommand interface {
|
|||||||
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
|
AddPasswordData(secret *crypto.CryptoValue, changeRequired bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation {
|
func (c *Commands) AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation {
|
||||||
return func() (_ preparation.CreateCommands, err error) {
|
return func() (_ preparation.CreateCommands, err error) {
|
||||||
if err := human.Email.Validate(); err != nil {
|
if err := human.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if human.Username = strings.TrimSpace(human.Username); human.Username == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument")
|
|
||||||
}
|
|
||||||
|
|
||||||
if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty")
|
|
||||||
}
|
|
||||||
if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" {
|
|
||||||
return nil, errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty")
|
|
||||||
}
|
|
||||||
human.ensureDisplayName()
|
|
||||||
|
|
||||||
if human.Phone.Number != "" {
|
|
||||||
if human.Phone.Number, err = human.Phone.Number.Normalize(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||||
|
if err := c.addHumanCommandCheckID(ctx, filter, a, human); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner)
|
domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -176,55 +201,30 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
|
|||||||
createCmd.AddPhoneData(human.Phone.Number)
|
createCmd.AddPhoneData(human.Phone.Number)
|
||||||
}
|
}
|
||||||
|
|
||||||
if human.Password != "" {
|
if err := addHumanCommandPassword(ctx, filter, createCmd, human, passwordAlg); err != nil {
|
||||||
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
if human.BcryptedPassword != "" {
|
|
||||||
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmds := make([]eventstore.Command, 0, 3)
|
cmds := make([]eventstore.Command, 0, 3)
|
||||||
cmds = append(cmds, createCmd)
|
cmds = append(cmds, createCmd)
|
||||||
|
|
||||||
if human.Email.Verified {
|
cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, a, human, codeAlg, allowInitMail)
|
||||||
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
//add init code if
|
|
||||||
// email not verified or
|
|
||||||
// user not registered and password set
|
|
||||||
if human.shouldAddInitCode() {
|
|
||||||
value, expiry, err := newUserInitCode(ctx, filter, codeAlg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
|
||||||
} else {
|
|
||||||
if !human.Email.Verified {
|
|
||||||
value, expiry, err := newEmailCode(ctx, filter, codeAlg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if human.Phone.Verified {
|
cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, a, human, codeAlg)
|
||||||
cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate))
|
if err != nil {
|
||||||
} else if human.Phone.Number != "" {
|
return nil, err
|
||||||
value, expiry, err := newPhoneCode(ctx, filter, codeAlg)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
for _, metadataEntry := range human.Metadata {
|
||||||
}
|
cmds = append(cmds, user.NewMetadataSetEvent(
|
||||||
cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry))
|
ctx,
|
||||||
|
&a.Aggregate,
|
||||||
|
metadataEntry.Key,
|
||||||
|
metadataEntry.Value,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmds, nil
|
return cmds, nil
|
||||||
@ -232,6 +232,87 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) ([]eventstore.Command, error) {
|
||||||
|
if human.Email.Verified {
|
||||||
|
cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate))
|
||||||
|
}
|
||||||
|
|
||||||
|
// if allowInitMail, used for v1 api (system, admin, mgmt, auth):
|
||||||
|
// add init code if
|
||||||
|
// email not verified or
|
||||||
|
// user not registered and password set
|
||||||
|
if allowInitMail && human.shouldAddInitCode() {
|
||||||
|
initCode, err := newUserInitCode(ctx, filter, codeAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry)), nil
|
||||||
|
}
|
||||||
|
if !human.Email.Verified {
|
||||||
|
emailCode, err := c.newEmailCode(ctx, filter, codeAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if human.Email.ReturnCode {
|
||||||
|
human.EmailCode = &emailCode.Plain
|
||||||
|
}
|
||||||
|
return append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode)), nil
|
||||||
|
}
|
||||||
|
return cmds, nil
|
||||||
|
}
|
||||||
|
func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
|
||||||
|
if human.Phone.Number == "" {
|
||||||
|
return cmds, nil
|
||||||
|
}
|
||||||
|
if human.Phone.Verified {
|
||||||
|
return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)), nil
|
||||||
|
}
|
||||||
|
phoneCode, err := newPhoneCode(ctx, filter, codeAlg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, a *user.Aggregate, human *AddHuman) (err error) {
|
||||||
|
if human.ID != "" {
|
||||||
|
existingHuman, err := humanWriteModelByID(ctx, filter, human.ID, a.ResourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isUserStateExists(existingHuman.UserState) {
|
||||||
|
return errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
human.ID, err = c.idGenerator.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.ID = human.ID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, passwordAlg crypto.HashAlgorithm) (err error) {
|
||||||
|
if human.Password != "" {
|
||||||
|
if err = humanValidatePassword(ctx, filter, human.Password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := crypto.Hash([]byte(human.Password), passwordAlg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
createCmd.AddPasswordData(secret, human.PasswordChangeRequired)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if human.BcryptedPassword != "" {
|
||||||
|
createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
|
func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error {
|
||||||
if mustBeDomain {
|
if mustBeDomain {
|
||||||
return nil
|
return nil
|
||||||
@ -651,3 +732,17 @@ func (c *Commands) getHumanWriteModelByID(ctx context.Context, userID, resourceo
|
|||||||
}
|
}
|
||||||
return humanWriteModel, nil
|
return humanWriteModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func humanWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceowner string) (*HumanWriteModel, error) {
|
||||||
|
humanWriteModel := NewHumanWriteModel(userID, resourceowner)
|
||||||
|
events, err := filter(ctx, humanWriteModel.Query())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(events) == 0 {
|
||||||
|
return humanWriteModel, nil
|
||||||
|
}
|
||||||
|
humanWriteModel.AppendEvents(events...)
|
||||||
|
err = humanWriteModel.Reduce()
|
||||||
|
return humanWriteModel, err
|
||||||
|
}
|
||||||
|
@ -2,17 +2,19 @@ package command
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/muhlemmer/gu"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||||
"github.com/zitadel/zitadel/internal/crypto"
|
"github.com/zitadel/zitadel/internal/crypto"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
"github.com/zitadel/zitadel/internal/errors"
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore"
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
@ -29,16 +31,20 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
idGenerator id.Generator
|
idGenerator id.Generator
|
||||||
userPasswordAlg crypto.HashAlgorithm
|
userPasswordAlg crypto.HashAlgorithm
|
||||||
codeAlg crypto.EncryptionAlgorithm
|
codeAlg crypto.EncryptionAlgorithm
|
||||||
|
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||||
}
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
orgID string
|
orgID string
|
||||||
human *AddHuman
|
human *AddHuman
|
||||||
secretGenerator crypto.Generator
|
secretGenerator crypto.Generator
|
||||||
|
allowInitMail bool
|
||||||
}
|
}
|
||||||
type res struct {
|
type res struct {
|
||||||
want *domain.HumanDetails
|
want *domain.ObjectDetails
|
||||||
err func(error) bool
|
wantID string
|
||||||
|
wantEmailCode string
|
||||||
|
err func(error) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
userAgg := user.NewAggregate("user1", "org1")
|
userAgg := user.NewAggregate("user1", "org1")
|
||||||
@ -68,9 +74,67 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
Address: "email@test.ch",
|
Address: "email@test.ch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user invalid, invalid argument error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
FirstName: "firstname",
|
||||||
|
},
|
||||||
|
allowInitMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with id, already exists, precondition error",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("password", true, ""),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
ID: "user1",
|
||||||
|
Username: "username",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
allowInitMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting"))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -95,9 +159,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsInternal,
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal"))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -134,30 +201,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsInternal,
|
err: func(err error) bool {
|
||||||
},
|
return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal"))
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "user invalid, invalid argument error",
|
|
||||||
fields: fields{
|
|
||||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
|
||||||
eventstore: eventstoreExpect(
|
|
||||||
t,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
orgID: "org1",
|
|
||||||
human: &AddHuman{
|
|
||||||
Username: "username",
|
|
||||||
FirstName: "firstname",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
|
||||||
err: errors.IsErrorInvalidArgument,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "add human (with initial code), ok",
|
name: "add human (with initial code), ok",
|
||||||
@ -237,16 +287,15 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
Sequence: 0,
|
||||||
ObjectDetails: domain.ObjectDetails{
|
EventDate: time.Time{},
|
||||||
Sequence: 0,
|
ResourceOwner: "org1",
|
||||||
EventDate: time.Time{},
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -330,14 +379,172 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add human (with password and email code custom template), ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("password", false, ""),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("emailCode"),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
|
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||||
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}",
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: false,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add human (with password and return email code), ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("password", false, ""),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanEmailCodeAddedEventV2(context.Background(),
|
||||||
|
&user.NewAggregate("user1", "org1").Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte("emailCode"),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
|
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||||
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
Password: "password",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
ReturnCode: true,
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
},
|
||||||
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: false,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantID: "user1",
|
||||||
|
wantEmailCode: "emailCode",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -400,14 +607,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -470,14 +676,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -540,14 +745,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -594,9 +798,12 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: func(err error) bool {
|
||||||
|
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername"))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -687,15 +894,14 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PasswordChangeRequired: true,
|
PasswordChangeRequired: true,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -787,14 +993,13 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
|
||||||
ResourceOwner: "org1",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -875,14 +1080,104 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
PreferredLanguage: language.English,
|
PreferredLanguage: language.English,
|
||||||
},
|
},
|
||||||
secretGenerator: GetMockSecretGenerator(t),
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
want: &domain.HumanDetails{
|
want: &domain.ObjectDetails{
|
||||||
ID: "user1",
|
ResourceOwner: "org1",
|
||||||
ObjectDetails: domain.ObjectDetails{
|
},
|
||||||
ResourceOwner: "org1",
|
wantID: "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add human with metadata, ok",
|
||||||
|
fields: fields{
|
||||||
|
eventstore: eventstoreExpect(
|
||||||
|
t,
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
org.NewDomainPolicyAddedEvent(context.Background(),
|
||||||
|
&userAgg.Aggregate,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
instance.NewSecretGeneratorAddedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&instanceAgg.Aggregate,
|
||||||
|
domain.SecretGeneratorTypeInitCode,
|
||||||
|
0,
|
||||||
|
1*time.Hour,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
expectPush(
|
||||||
|
[]*repository.Event{
|
||||||
|
eventFromEventPusher(
|
||||||
|
newAddHumanEvent("", false, ""),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanInitialCodeAddedEvent(
|
||||||
|
context.Background(),
|
||||||
|
&userAgg.Aggregate,
|
||||||
|
&crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte(""),
|
||||||
|
},
|
||||||
|
1*time.Hour,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewMetadataSetEvent(
|
||||||
|
context.Background(),
|
||||||
|
&userAgg.Aggregate,
|
||||||
|
"testKey",
|
||||||
|
[]byte("testValue"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||||
|
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
ctx: context.Background(),
|
||||||
|
orgID: "org1",
|
||||||
|
human: &AddHuman{
|
||||||
|
Username: "username",
|
||||||
|
FirstName: "firstname",
|
||||||
|
LastName: "lastname",
|
||||||
|
Email: Email{
|
||||||
|
Address: "email@test.ch",
|
||||||
|
},
|
||||||
|
PreferredLanguage: language.English,
|
||||||
|
Metadata: []*AddMetadataEntry{
|
||||||
|
{
|
||||||
|
Key: "testKey",
|
||||||
|
Value: []byte("testValue"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
secretGenerator: GetMockSecretGenerator(t),
|
||||||
|
allowInitMail: true,
|
||||||
|
},
|
||||||
|
res: res{
|
||||||
|
want: &domain.ObjectDetails{
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
wantID: "user1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -893,8 +1188,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||||
userEncryption: tt.fields.codeAlg,
|
userEncryption: tt.fields.codeAlg,
|
||||||
idGenerator: tt.fields.idGenerator,
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
newEmailCode: tt.fields.newEmailCode,
|
||||||
}
|
}
|
||||||
got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human)
|
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@ -904,7 +1200,9 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
|||||||
t.Errorf("got wrong err: %v ", err)
|
t.Errorf("got wrong err: %v ", err)
|
||||||
}
|
}
|
||||||
if tt.res.err == nil {
|
if tt.res.err == nil {
|
||||||
assert.Equal(t, tt.res.want, got)
|
assert.Equal(t, tt.res.want, tt.args.human.Details)
|
||||||
|
assert.Equal(t, tt.res.wantID, tt.args.human.ID)
|
||||||
|
assert.Equal(t, tt.res.wantEmailCode, gu.Value(tt.args.human.EmailCode))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -958,7 +1256,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -985,7 +1283,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1022,7 +1320,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1065,7 +1363,7 @@ func TestCommandSide_ImportHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1869,7 +2167,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1899,7 +2197,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1939,7 +2237,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1987,7 +2285,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2056,7 +2354,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsPreconditionFailed,
|
err: caos_errs.IsPreconditionFailed,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2125,7 +2423,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2211,7 +2509,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3147,7 +3445,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
|
|||||||
userID: "",
|
userID: "",
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3164,7 +3462,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) {
|
|||||||
userID: "user1",
|
userID: "user1",
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsNotFound,
|
err: caos_errs.IsNotFound,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3261,7 +3559,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
|||||||
userIDs: []string{"user1"},
|
userIDs: []string{"user1"},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3277,7 +3575,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) {
|
|||||||
userIDs: []string{},
|
userIDs: []string{},
|
||||||
},
|
},
|
||||||
res: res{
|
res: res{
|
||||||
err: errors.IsErrorInvalidArgument,
|
err: caos_errs.IsErrorInvalidArgument,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3479,18 +3777,23 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddHumanCommand(t *testing.T) {
|
func TestAddHumanCommand(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
idGenerator id.Generator
|
||||||
|
}
|
||||||
type args struct {
|
type args struct {
|
||||||
a *user.Aggregate
|
a *user.Aggregate
|
||||||
human *AddHuman
|
human *AddHuman
|
||||||
passwordAlg crypto.HashAlgorithm
|
passwordAlg crypto.HashAlgorithm
|
||||||
filter preparation.FilterToQueryReducer
|
filter preparation.FilterToQueryReducer
|
||||||
codeAlg crypto.EncryptionAlgorithm
|
codeAlg crypto.EncryptionAlgorithm
|
||||||
|
allowInitMail bool
|
||||||
}
|
}
|
||||||
agg := user.NewAggregate("id", "ro")
|
agg := user.NewAggregate("id", "ro")
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
fields fields
|
||||||
want Want
|
args args
|
||||||
|
want Want
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "invalid email",
|
name: "invalid email",
|
||||||
@ -3503,7 +3806,7 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
ValidationErr: errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
|
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3519,7 +3822,7 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
|
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3534,11 +3837,14 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
ValidationErr: errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
|
ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid password",
|
name: "invalid password",
|
||||||
|
fields: fields{
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||||
|
},
|
||||||
args: args{
|
args: args{
|
||||||
a: agg,
|
a: agg,
|
||||||
human: &AddHuman{
|
human: &AddHuman{
|
||||||
@ -3578,11 +3884,14 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
Filter(),
|
Filter(),
|
||||||
},
|
},
|
||||||
want: Want{
|
want: Want{
|
||||||
CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
|
CreateErr: caos_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "correct",
|
name: "correct",
|
||||||
|
fields: fields{
|
||||||
|
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"),
|
||||||
|
},
|
||||||
args: args{
|
args: args{
|
||||||
a: agg,
|
a: agg,
|
||||||
human: &AddHuman{
|
human: &AddHuman{
|
||||||
@ -3654,7 +3963,25 @@ func TestAddHumanCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
AssertValidation(t, context.Background(), AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.codeAlg), tt.args.filter, tt.want)
|
c := &Commands{
|
||||||
|
idGenerator: tt.fields.idGenerator,
|
||||||
|
}
|
||||||
|
AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mockEmailCode(code string, exp time.Duration) func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
|
return func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||||
|
return &CryptoCodeWithExpiry{
|
||||||
|
Crypted: &crypto.CryptoValue{
|
||||||
|
CryptoType: crypto.TypeEncryption,
|
||||||
|
Algorithm: "enc",
|
||||||
|
KeyID: "id",
|
||||||
|
Crypted: []byte(code),
|
||||||
|
},
|
||||||
|
Plain: code,
|
||||||
|
Expiry: exp,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,11 +10,6 @@ import (
|
|||||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HumanDetails struct {
|
|
||||||
ID string
|
|
||||||
ObjectDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
type Human struct {
|
type Human struct {
|
||||||
es_models.ObjectRoot
|
es_models.ObjectRoot
|
||||||
|
|
||||||
|
@ -185,7 +185,6 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St
|
|||||||
if e.CodeReturned {
|
if e.CodeReturned {
|
||||||
return crdb.NewNoOpStatement(e), nil
|
return crdb.NewNoOpStatement(e), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := HandlerContext(event.Aggregate())
|
ctx := HandlerContext(event.Aggregate())
|
||||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil,
|
||||||
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType,
|
||||||
|
5
pkg/grpc/user/v2alpha/user.go
Normal file
5
pkg/grpc/user/v2alpha/user.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
func (r *AddHumanUserRequest) AuthContext() string {
|
||||||
|
return r.GetOrganisation().GetOrgId()
|
||||||
|
}
|
@ -6,11 +6,11 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha;object";
|
|||||||
|
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
message OrgContext {
|
message Organisation {
|
||||||
oneof ctx {
|
oneof org {
|
||||||
string org_id = 1;
|
string org_id = 1;
|
||||||
string org_domain = 2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,23 @@ import "google/api/field_behavior.proto";
|
|||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
import "validate/validate.proto";
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
message SetHumanEmail {
|
||||||
|
string email = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200, email: true},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"mini@mouse.com\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// if no verification is specified, an email is sent with the default url
|
||||||
|
oneof verification {
|
||||||
|
SendEmailVerificationCode send_code = 2;
|
||||||
|
ReturnEmailVerificationCode return_code = 3;
|
||||||
|
bool is_verified = 4 [(validate.rules).bool.const = true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message SendEmailVerificationCode {
|
message SendEmailVerificationCode {
|
||||||
optional string url_template = 1 [
|
optional string url_template = 1 [
|
||||||
|
53
proto/zitadel/user/v2alpha/password.proto
Normal file
53
proto/zitadel/user/v2alpha/password.proto
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package zitadel.user.v2alpha;
|
||||||
|
|
||||||
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
||||||
|
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
|
message SetUserPassword {
|
||||||
|
oneof type {
|
||||||
|
Password password = 1;
|
||||||
|
HashedPassword hashed_password = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message Password {
|
||||||
|
string password = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"Secr3tP4ssw0rd!\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
bool change_required = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HashedPassword {
|
||||||
|
string hash = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"$2a$12$lJ08fqVr8bFJilRVnDT9QeULI7YW.nT3iwUv6dyg0aCrfm3UY8XR2\"";
|
||||||
|
description: "\"hashed password\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
string algorithm = 2 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200, const: "bcrypt"},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"bcrypt\"";
|
||||||
|
description: "\"algorithm used for the hash. currently only bcrypt is supported\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
bool change_required = 3;
|
||||||
|
}
|
@ -4,6 +4,87 @@ package zitadel.user.v2alpha;
|
|||||||
|
|
||||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
||||||
|
|
||||||
|
import "google/api/field_behavior.proto";
|
||||||
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
|
import "validate/validate.proto";
|
||||||
|
|
||||||
message User {
|
message User {
|
||||||
string id = 1;
|
string id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Gender {
|
||||||
|
GENDER_UNSPECIFIED = 0;
|
||||||
|
GENDER_FEMALE = 1;
|
||||||
|
GENDER_MALE = 2;
|
||||||
|
GENDER_DIVERSE = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetHumanProfile {
|
||||||
|
string first_name = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"Minnie\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
string last_name = 2 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"Mouse\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
optional string nick_name = 3 [
|
||||||
|
(validate.rules).string = {max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"Mini\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
optional string display_name = 4 [
|
||||||
|
(validate.rules).string = {max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"Minnie Mouse\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
optional string preferred_language = 5 [
|
||||||
|
(validate.rules).string = {max_len: 10},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
max_length: 10;
|
||||||
|
example: "\"en\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
optional zitadel.user.v2alpha.Gender gender = 6 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"GENDER_FEMALE\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message SetMetadataEntry {
|
||||||
|
string key = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
example: "\"my-key\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 200;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
bytes value = 2 [
|
||||||
|
(validate.rules).bytes = {min_len: 1, max_len: 500000},
|
||||||
|
(google.api.field_behavior) = REQUIRED,
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "The value has to be base64 encoded.";
|
||||||
|
example: "\"VGhpcyBpcyBteSB0ZXN0IHZhbHVl\"";
|
||||||
|
min_length: 1,
|
||||||
|
max_length: 500000;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
@ -5,6 +5,8 @@ package zitadel.user.v2alpha;
|
|||||||
import "zitadel/options.proto";
|
import "zitadel/options.proto";
|
||||||
import "zitadel/object/v2alpha/object.proto";
|
import "zitadel/object/v2alpha/object.proto";
|
||||||
import "zitadel/user/v2alpha/email.proto";
|
import "zitadel/user/v2alpha/email.proto";
|
||||||
|
import "zitadel/user/v2alpha/password.proto";
|
||||||
|
import "zitadel/user/v2alpha/user.proto";
|
||||||
import "google/api/annotations.proto";
|
import "google/api/annotations.proto";
|
||||||
import "google/api/field_behavior.proto";
|
import "google/api/field_behavior.proto";
|
||||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||||
@ -12,8 +14,91 @@ import "validate/validate.proto";
|
|||||||
|
|
||||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user";
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
|
||||||
|
info: {
|
||||||
|
title: "User Service";
|
||||||
|
version: "2.0-alpha";
|
||||||
|
description: "This API is intended to manage users in a ZITADEL instance. This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.";
|
||||||
|
contact:{
|
||||||
|
name: "ZITADEL"
|
||||||
|
url: "https://zitadel.com"
|
||||||
|
email: "hi@zitadel.com"
|
||||||
|
}
|
||||||
|
license: {
|
||||||
|
name: "Apache 2.0",
|
||||||
|
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
schemes: HTTPS;
|
||||||
|
schemes: HTTP;
|
||||||
|
|
||||||
|
consumes: "application/json";
|
||||||
|
consumes: "application/grpc";
|
||||||
|
|
||||||
|
produces: "application/json";
|
||||||
|
produces: "application/grpc";
|
||||||
|
|
||||||
|
consumes: "application/grpc-web+proto";
|
||||||
|
produces: "application/grpc-web+proto";
|
||||||
|
|
||||||
|
host: "$ZITADEL_DOMAIN";
|
||||||
|
base_path: "/";
|
||||||
|
|
||||||
|
external_docs: {
|
||||||
|
description: "Detailed information about ZITADEL",
|
||||||
|
url: "https://zitadel.com/docs"
|
||||||
|
}
|
||||||
|
|
||||||
|
responses: {
|
||||||
|
key: "403";
|
||||||
|
value: {
|
||||||
|
description: "Returned when the user does not have permission to access the resource.";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/rpcStatus";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses: {
|
||||||
|
key: "404";
|
||||||
|
value: {
|
||||||
|
description: "Returned when the resource does not exist.";
|
||||||
|
schema: {
|
||||||
|
json_schema: {
|
||||||
|
ref: "#/definitions/rpcStatus";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
service UserService {
|
service UserService {
|
||||||
|
|
||||||
|
// Create a new human user
|
||||||
|
rpc AddHumanUser (AddHumanUserRequest) returns (AddHumanUserResponse) {
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/v2alpha/users/human"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (zitadel.v1.auth_option) = {
|
||||||
|
permission: "user.write"
|
||||||
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Create a user (Human)";
|
||||||
|
description: "Create/import a new user with the type human. The newly created user will get a verification email if either the email address is not marked as verified and you did not request the verification to be returned."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the email of a user
|
||||||
rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) {
|
rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/v2alpha/users/{user_id}/email"
|
post: "/v2alpha/users/{user_id}/email"
|
||||||
@ -23,8 +108,20 @@ service UserService {
|
|||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "authenticated"
|
permission: "authenticated"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Change the user email";
|
||||||
|
description: "Change the email address of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by email."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify the email with the provided code
|
||||||
rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) {
|
rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
post: "/v2alpha/users/{user_id}/email/_verify"
|
post: "/v2alpha/users/{user_id}/email/_verify"
|
||||||
@ -34,9 +131,61 @@ service UserService {
|
|||||||
option (zitadel.v1.auth_option) = {
|
option (zitadel.v1.auth_option) = {
|
||||||
permission: "authenticated"
|
permission: "authenticated"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||||
|
summary: "Verify the email";
|
||||||
|
description: "Verify the email with the generated code."
|
||||||
|
responses: {
|
||||||
|
key: "200"
|
||||||
|
value: {
|
||||||
|
description: "OK";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AddHumanUserRequest{
|
||||||
|
// optionally set your own id unique for the user
|
||||||
|
optional string user_id = 1 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// optionally set a unique username, if none is provided the email will be used
|
||||||
|
optional string username = 2 [
|
||||||
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 1;
|
||||||
|
max_length: 200;
|
||||||
|
example: "\"minnie-mouse\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
zitadel.object.v2alpha.Organisation organisation = 3;
|
||||||
|
SetHumanProfile profile = 4 [
|
||||||
|
(validate.rules).message.required = true,
|
||||||
|
(google.api.field_behavior) = REQUIRED
|
||||||
|
];
|
||||||
|
SetHumanEmail email = 5 [
|
||||||
|
(validate.rules).message.required = true,
|
||||||
|
(google.api.field_behavior) = REQUIRED
|
||||||
|
];
|
||||||
|
repeated SetMetadataEntry metadata = 6;
|
||||||
|
oneof password_type {
|
||||||
|
Password password = 7;
|
||||||
|
HashedPassword hashed_password = 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddHumanUserResponse {
|
||||||
|
string user_id = 1;
|
||||||
|
zitadel.object.v2alpha.Details details = 2;
|
||||||
|
optional string email_code = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message SetEmailRequest{
|
message SetEmailRequest{
|
||||||
string user_id = 1 [
|
string user_id = 1 [
|
||||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user