Skip to content

Commit

Permalink
feat(api): add user creation to user service (#5745)
Browse files Browse the repository at this point in the history
* 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 c89e90a.

* 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>
  • Loading branch information
3 people committed Apr 26, 2023
1 parent 19f2f83 commit e4a4b7c
Show file tree
Hide file tree
Showing 24 changed files with 1,168 additions and 219 deletions.
7 changes: 7 additions & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ module.exports = {
sidebarOptions: {
groupPathsBy: "tag",
},
},
user: {
specPath: ".artifacts/openapi/zitadel/user/v2alpha/user_service.swagger.json",
outputDir: "docs/apis/user_service",
sidebarOptions: {
groupPathsBy: "tag",
},
}
}
},
Expand Down
16 changes: 15 additions & 1 deletion docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,20 @@ module.exports = {
},
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",
label: "Assets",
Expand Down Expand Up @@ -508,4 +522,4 @@ module.exports = {
],
},
],
};
};
11 changes: 4 additions & 7 deletions internal/api/grpc/management/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
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 {
return nil, err
}
return &mgmt_pb.AddHumanUserResponse{
UserId: details.ID,
Details: obj_grpc.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
UserId: human.ID,
Details: obj_grpc.DomainToAddDetailsPb(human.Details),
}, nil
}

Expand Down
7 changes: 7 additions & 0 deletions internal/api/grpc/server/middleware/auth_interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
}

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)
if err != nil {
Expand All @@ -42,3 +45,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
span.End()
return handler(ctxSetter(ctx), req)
}

type AuthContext interface {
AuthContext() string
}
2 changes: 1 addition & 1 deletion internal/api/grpc/user/v2/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

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

switch v := req.GetVerification().(type) {
Expand Down
107 changes: 107 additions & 0 deletions internal/api/grpc/user/v2/user.go
Original file line number Diff line number Diff line change
@@ -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 changes: 80 additions & 0 deletions internal/api/grpc/user/v2/user_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
2 changes: 2 additions & 0 deletions internal/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Commands struct {
httpClient *http.Client

checkPermission permissionCheck
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)

eventstore *eventstore.Eventstore
static static.Storage
Expand Down Expand Up @@ -109,6 +110,7 @@ func StartCommands(
checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) {
return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf)
},
newEmailCode: newEmailCode,
}

instance_repo.RegisterEventMappers(repo.eventstore)
Expand Down
23 changes: 16 additions & 7 deletions internal/command/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,33 @@ import (
"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)
if err != nil {
return nil, -1, err
return nil, err
}

code := new(CryptoCodeWithExpiry)
switch a := alg.(type) {
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:
value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
default:
return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
}
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) {
Expand Down
9 changes: 7 additions & 2 deletions internal/command/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package command

import (
"context"
"time"

"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
Expand All @@ -12,12 +11,18 @@ import (
type Email struct {
Address domain.EmailAddress
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 {
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)
}
3 changes: 2 additions & 1 deletion internal/command/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,8 +333,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str
validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize))
}
} else if setup.Org.Human != nil {
setup.Org.Human.ID = userAgg.ID
validations = append(validations,
AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption),
c.AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption, true),
)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/command/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user
var pat *PersonalAccessToken
var machineKey *MachineKey
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 {
validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine))
if o.Machine.Pat != nil {
Expand Down

1 comment on commit e4a4b7c

@vercel
Copy link

@vercel vercel bot commented on e4a4b7c Apr 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./

zitadel-docs.vercel.app
docs-git-main-zitadel.vercel.app
docs-zitadel.vercel.app

Please sign in to comment.