diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 9278d5c85a..c93aea65b6 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -343,6 +343,15 @@ DefaultInstance: Number: Verified: Password: + Machine: + Machine: + Username: + Name: + MachineKey: + ExpirationDate: + Type: + Pat: + ExpirationDate: SecretGenerators: PasswordSaltCost: 14 ClientSecret: diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 3da04cc0fa..7a84d4275e 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "os" "strings" "golang.org/x/text/language" @@ -21,6 +22,7 @@ type FirstInstance struct { InstanceName string DefaultLanguage language.Tag Org command.OrgSetup + MachineKeyPath string instanceSetup command.InstanceSetup userEncryptionKey *crypto.KeyConfig @@ -97,7 +99,25 @@ func (mig *FirstInstance) Execute(ctx context.Context) error { } } - _, _, err = cmd.SetUpInstance(ctx, &mig.instanceSetup) + _, _, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup) + if key == nil { + return err + } + + f := os.Stdout + if mig.MachineKeyPath != "" { + f, err = os.OpenFile(mig.MachineKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + } + defer f.Close() + + keyDetails, err := key.Detail() + if err != nil { + return err + } + _, err = fmt.Fprintln(f, string(keyDetails)) return err } diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 84be4310a0..d4d7da0308 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -3,6 +3,7 @@ package setup import ( "bytes" "strings" + "time" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" @@ -39,6 +40,7 @@ func MustNewConfig(v *viper.Viper) *Config { hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), database.DecodeHook, )), @@ -87,6 +89,7 @@ func MustNewSteps(v *viper.Viper) *Steps { hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), )), ) diff --git a/cmd/setup/steps.yaml b/cmd/setup/steps.yaml index 75335ba71a..c362f80e84 100644 --- a/cmd/setup/steps.yaml +++ b/cmd/setup/steps.yaml @@ -1,4 +1,5 @@ FirstInstance: + MachineKeyPath: InstanceName: ZITADEL DefaultLanguage: en Org: @@ -22,3 +23,10 @@ FirstInstance: Verified: Password: Password1! PasswordChangeRequired: true + Machine: + Machine: + Username: + Name: + MachineKey: + ExpirationDate: + Type: diff --git a/cmd/start/config.go b/cmd/start/config.go index 078e3ce1de..b8f7a92e0d 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -6,13 +6,13 @@ import ( "github.com/mitchellh/mapstructure" "github.com/spf13/viper" "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/actions" admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" internal_authz "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/api/saml" "github.com/zitadel/zitadel/internal/api/ui/console" "github.com/zitadel/zitadel/internal/api/ui/login" auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing" @@ -70,6 +70,7 @@ func MustNewConfig(v *viper.Viper) *Config { hook.Base64ToBytesHookFunc(), hook.TagToLanguageHookFunc(), mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToTimeHookFunc(time.RFC3339), mapstructure.StringToSliceHookFunc(","), database.DecodeHook, actions.HTTPConfigDecodeHook, diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index d5af548c40..6b0124016c 100644 --- a/docs/docs/apis/proto/system.md +++ b/docs/docs/apis/proto/system.md @@ -49,6 +49,7 @@ Returns the detail of an instance > **rpc** AddInstance([AddInstanceRequest](#addinstancerequest)) [AddInstanceResponse](#addinstanceresponse) +Deprecated: Use CreateInstance instead Creates a new instance with all needed setup data This might take some time @@ -69,6 +70,19 @@ Updates name of an existing instance PUT: /instances/{instance_id} +### CreateInstance + +> **rpc** CreateInstance([CreateInstanceRequest](#createinstancerequest)) +[CreateInstanceResponse](#createinstanceresponse) + +Creates a new instance with all needed setup data +This might take some time + + + + POST: /instances/_create + + ### RemoveInstance > **rpc** RemoveInstance([RemoveInstanceRequest](#removeinstancerequest)) @@ -342,6 +356,124 @@ This is an empty response +### CreateInstanceRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| instance_name | string | - | string.min_len: 1
string.max_len: 200
| +| first_org_name | string | - | string.max_len: 200
| +| custom_domain | string | - | string.max_len: 200
| +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) owner.human | CreateInstanceRequest.Human | oneof field for the user managing the instance | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) owner.machine | CreateInstanceRequest.Machine | - | | +| default_language | string | - | string.max_len: 10
| + + + + +### CreateInstanceRequest.Email + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| email | string | - | string.min_len: 1
string.max_len: 200
string.email: true
| +| is_email_verified | bool | - | | + + + + +### CreateInstanceRequest.Human + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_name | string | - | string.max_len: 200
| +| email | CreateInstanceRequest.Email | - | message.required: true
| +| profile | CreateInstanceRequest.Profile | - | message.required: false
| +| password | CreateInstanceRequest.Password | - | message.required: false
| + + + + +### CreateInstanceRequest.Machine + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_name | string | - | string.max_len: 200
| +| name | string | - | string.max_len: 200
| +| personal_access_token | CreateInstanceRequest.PersonalAccessToken | - | | +| machine_key | CreateInstanceRequest.MachineKey | - | | + + + + +### CreateInstanceRequest.MachineKey + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| type | zitadel.authn.v1.KeyType | - | enum.defined_only: true
enum.not_in: [0]
| +| expiration_date | google.protobuf.Timestamp | - | | + + + + +### CreateInstanceRequest.Password + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| password | string | - | string.max_len: 200
| +| password_change_required | bool | - | | + + + + +### CreateInstanceRequest.PersonalAccessToken + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| expiration_date | google.protobuf.Timestamp | - | | + + + + +### CreateInstanceRequest.Profile + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| first_name | string | - | string.max_len: 200
| +| last_name | string | - | string.max_len: 200
| +| preferred_language | string | - | string.max_len: 10
| + + + + +### CreateInstanceResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| instance_id | string | - | | +| details | zitadel.v1.ObjectDetails | - | | +| pat | string | - | | +| machine_key | bytes | - | | + + + + ### ExistsDomainRequest diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index ec84475b0f..4967cc4fe2 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -22,6 +22,7 @@ import ( action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/management" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -597,7 +598,7 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm if org.MachineKeys != nil { for _, key := range org.GetMachineKeys() { logging.Debugf("import machine_user_key: %s", key.KeyId) - _, err := s.command.AddUserMachineKeyWithID(ctx, &domain.MachineKey{ + _, err := s.command.AddUserMachineKey(ctx, &command.MachineKey{ ObjectRoot: models.ObjectRoot{ AggregateID: key.UserId, ResourceOwner: org.GetOrgId(), @@ -606,7 +607,7 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm Type: authn.KeyTypeToDomain(key.Type), ExpirationDate: key.ExpirationDate.AsTime(), PublicKey: key.PublicKey, - }, org.GetOrgId()) + }) if err != nil { errors = append(errors, &admin_pb.ImportDataError{Type: "machine_user_key", Id: key.KeyId, Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/admin/user_converter.go b/internal/api/grpc/admin/user_converter.go index e7ec098931..3be84e2f23 100644 --- a/internal/api/grpc/admin/user_converter.go +++ b/internal/api/grpc/admin/user_converter.go @@ -9,11 +9,11 @@ import ( admin_grpc "github.com/zitadel/zitadel/pkg/grpc/admin" ) -func setUpOrgHumanToCommand(human *admin_grpc.SetUpOrgRequest_Human) command.AddHuman { +func setUpOrgHumanToCommand(human *admin_grpc.SetUpOrgRequest_Human) *command.AddHuman { var lang language.Tag lang, err := language.Parse(human.Profile.PreferredLanguage) logging.OnError(err).Debug("unable to parse language") - return command.AddHuman{ + return &command.AddHuman{ Username: human.UserName, FirstName: human.Profile.FirstName, LastName: human.Profile.LastName, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 8e0ecbe2aa..46d12fc848 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -2,7 +2,6 @@ package management import ( "context" - "time" "github.com/zitadel/logging" "github.com/zitadel/oidc/v2/pkg/oidc" @@ -731,27 +730,24 @@ func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKe } func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRequest) (*mgmt_pb.AddMachineKeyResponse, error) { - key, err := s.command.AddUserMachineKey(ctx, AddMachineKeyRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + machineKey := AddMachineKeyRequestToCommand(req, authz.GetCtxData(ctx).OrgID) + details, err := s.command.AddUserMachineKey(ctx, machineKey) if err != nil { return nil, err } - keyDetails, err := key.Detail() + keyDetails, err := machineKey.Detail() if err != nil { return nil, err } return &mgmt_pb.AddMachineKeyResponse{ - KeyId: key.KeyID, + KeyId: machineKey.KeyID, KeyDetails: keyDetails, - Details: obj_grpc.AddToDetailsPb( - key.Sequence, - key.ChangeDate, - key.ResourceOwner, - ), + Details: obj_grpc.DomainToAddDetailsPb(details), }, nil } func (s *Server) RemoveMachineKey(ctx context.Context, req *mgmt_pb.RemoveMachineKeyRequest) (*mgmt_pb.RemoveMachineKeyResponse, error) { - objectDetails, err := s.command.RemoveUserMachineKey(ctx, req.UserId, req.KeyId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveUserMachineKey(ctx, RemoveMachineKeyRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } @@ -794,28 +790,21 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List } func (s *Server) AddPersonalAccessToken(ctx context.Context, req *mgmt_pb.AddPersonalAccessTokenRequest) (*mgmt_pb.AddPersonalAccessTokenResponse, error) { - expDate := time.Time{} - if req.ExpirationDate != nil { - expDate = req.ExpirationDate.AsTime() - } scopes := []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner} - pat, token, err := s.command.AddPersonalAccessToken(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, expDate, scopes, domain.UserTypeMachine) + pat := AddPersonalAccessTokenRequestToCommand(req, authz.GetCtxData(ctx).OrgID, scopes, domain.UserTypeMachine) + details, err := s.command.AddPersonalAccessToken(ctx, pat) if err != nil { return nil, err } return &mgmt_pb.AddPersonalAccessTokenResponse{ TokenId: pat.TokenID, - Token: token, - Details: obj_grpc.AddToDetailsPb( - pat.Sequence, - pat.ChangeDate, - pat.ResourceOwner, - ), + Token: pat.Token, + Details: obj_grpc.DomainToAddDetailsPb(details), }, nil } func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *mgmt_pb.RemovePersonalAccessTokenRequest) (*mgmt_pb.RemovePersonalAccessTokenResponse, error) { - objectDetails, err := s.command.RemovePersonalAccessToken(ctx, req.UserId, req.TokenId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemovePersonalAccessToken(ctx, RemovePersonalAccessTokenRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 33d85bba93..96a26f6181 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -255,21 +255,59 @@ func ListMachineKeysRequestToQuery(ctx context.Context, req *mgmt_pb.ListMachine } -func AddMachineKeyRequestToDomain(req *mgmt_pb.AddMachineKeyRequest) *domain.MachineKey { +func AddMachineKeyRequestToCommand(req *mgmt_pb.AddMachineKeyRequest, resourceOwner string) *command.MachineKey { expDate := time.Time{} if req.ExpirationDate != nil { expDate = req.ExpirationDate.AsTime() } - return &domain.MachineKey{ + return &command.MachineKey{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.UserId, + AggregateID: req.UserId, + ResourceOwner: resourceOwner, }, ExpirationDate: expDate, Type: authn.KeyTypeToDomain(req.Type), } } +func RemoveMachineKeyRequestToCommand(req *mgmt_pb.RemoveMachineKeyRequest, resourceOwner string) *command.MachineKey { + return &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + ResourceOwner: resourceOwner, + }, + KeyID: req.KeyId, + } +} + +func AddPersonalAccessTokenRequestToCommand(req *mgmt_pb.AddPersonalAccessTokenRequest, resourceOwner string, scopes []string, allowedUserType domain.UserType) *command.PersonalAccessToken { + expDate := time.Time{} + if req.ExpirationDate != nil { + expDate = req.ExpirationDate.AsTime() + } + + return &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + ResourceOwner: resourceOwner, + }, + ExpirationDate: expDate, + Scopes: scopes, + AllowedUserType: allowedUserType, + } +} + +func RemovePersonalAccessTokenRequestToCommand(req *mgmt_pb.RemovePersonalAccessTokenRequest, resourceOwner string) *command.PersonalAccessToken { + return &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.UserId, + ResourceOwner: resourceOwner, + }, + TokenID: req.TokenId, + } +} + func ListPersonalAccessTokensRequestToQuery(ctx context.Context, req *mgmt_pb.ListPersonalAccessTokensRequest) (*query.PersonalAccessTokenSearchQueries, error) { resourceOwner, err := query.NewPersonalAccessTokenResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID) if err != nil { diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go index 029c8b7624..8d62424b15 100644 --- a/internal/api/grpc/system/instance.go +++ b/internal/api/grpc/system/instance.go @@ -41,7 +41,7 @@ func (s *Server) GetInstance(ctx context.Context, req *system_pb.GetInstanceRequ } func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequest) (*system_pb.AddInstanceResponse, error) { - id, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) + id, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) if err != nil { return nil, err } @@ -62,6 +62,28 @@ func (s *Server) UpdateInstance(ctx context.Context, req *system_pb.UpdateInstan }, nil } +func (s *Server) CreateInstance(ctx context.Context, req *system_pb.CreateInstanceRequest) (*system_pb.CreateInstanceResponse, error) { + id, pat, key, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) + if err != nil { + return nil, err + } + + var machineKey []byte + if key != nil { + machineKey, err = key.Detail() + if err != nil { + return nil, err + } + } + + return &system_pb.CreateInstanceResponse{ + Pat: pat, + MachineKey: machineKey, + InstanceId: id, + Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner), + }, nil +} + func (s *Server) RemoveInstance(ctx context.Context, req *system_pb.RemoveInstanceRequest) (*system_pb.RemoveInstanceResponse, error) { ctx = authz.WithInstanceID(ctx, req.InstanceId) details, err := s.command.RemoveInstance(ctx, req.InstanceId) diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 7c452c789b..d5a663f08d 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -3,10 +3,13 @@ package system import ( "strings" + "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/grpc/authn" instance_grpc "github.com/zitadel/zitadel/internal/api/grpc/instance" "github.com/zitadel/zitadel/internal/api/grpc/object" + z_oidc "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -14,6 +17,123 @@ import ( system_pb "github.com/zitadel/zitadel/pkg/grpc/system" ) +func CreateInstancePbToSetupInstance(req *system_pb.CreateInstanceRequest, defaultInstance command.InstanceSetup, externalDomain string) *command.InstanceSetup { + if req.InstanceName != "" { + defaultInstance.InstanceName = req.InstanceName + defaultInstance.Org.Name = req.InstanceName + } + if req.CustomDomain != "" { + defaultInstance.CustomDomain = req.CustomDomain + } + if req.FirstOrgName != "" { + defaultInstance.Org.Name = req.FirstOrgName + } + + if user := req.GetMachine(); user != nil { + defaultMachine := command.AddMachine{} + if defaultInstance.Org.Machine != nil { + defaultMachine = *defaultInstance.Org.Machine + } + + defaultInstance.Org.Machine = createInstancePbToAddMachine(user, defaultMachine) + defaultInstance.Org.Human = nil + } + if user := req.GetHuman(); user != nil { + defaultHuman := command.AddHuman{} + if defaultInstance.Org.Human != nil { + defaultHuman = *defaultInstance.Org.Human + } + + defaultInstance.Org.Human = createInstancePbToAddHuman(user, defaultHuman, defaultInstance.DomainPolicy.UserLoginMustBeDomain, defaultInstance.Org.Name, externalDomain) + defaultInstance.Org.Machine = nil + } + + if lang := language.Make(req.DefaultLanguage); lang != language.Und { + defaultInstance.DefaultLanguage = lang + } + + return &defaultInstance +} + +func createInstancePbToAddHuman(user *system_pb.CreateInstanceRequest_Human, defaultHuman command.AddHuman, userLoginMustBeDomain bool, org, externalDomain string) *command.AddHuman { + if user.Email != nil { + defaultHuman.Email.Address = user.Email.Email + defaultHuman.Email.Verified = user.Email.IsEmailVerified + } + if user.Profile != nil { + if user.Profile.FirstName != "" { + defaultHuman.FirstName = user.Profile.FirstName + } + if user.Profile.LastName != "" { + defaultHuman.LastName = user.Profile.LastName + } + if user.Profile.PreferredLanguage != "" { + lang, err := language.Parse(user.Profile.PreferredLanguage) + if err == nil { + defaultHuman.PreferredLanguage = lang + } + } + } + // check if default username is email style or else append @. + // this way we have the same value as before changing `UserLoginMustBeDomain` to false + if !userLoginMustBeDomain && !strings.Contains(defaultHuman.Username, "@") { + defaultHuman.Username = defaultHuman.Username + "@" + domain.NewIAMDomainName(org, externalDomain) + } + if user.UserName != "" { + defaultHuman.Username = user.UserName + } + + if user.Password != nil { + defaultHuman.Password = user.Password.Password + defaultHuman.PasswordChangeRequired = user.Password.PasswordChangeRequired + } + return &defaultHuman +} + +func createInstancePbToAddMachine(user *system_pb.CreateInstanceRequest_Machine, defaultMachine command.AddMachine) *command.AddMachine { + machine := command.Machine{} + if defaultMachine.Machine != nil { + machine = *defaultMachine.Machine + } + if user.UserName != "" { + machine.Username = user.UserName + } + if user.Name != "" { + machine.Name = user.Name + } + defaultMachine.Machine = &machine + + if defaultMachine.Pat != nil || user.PersonalAccessToken != nil { + pat := command.AddPat{} + if defaultMachine.Pat != nil { + pat = *defaultMachine.Pat + } + // scopes are currently static and can not be overwritten + pat.Scopes = []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner} + if user.PersonalAccessToken.ExpirationDate != nil { + pat.ExpirationDate = user.PersonalAccessToken.ExpirationDate.AsTime() + } + defaultMachine.Pat = &pat + } + + if defaultMachine.MachineKey != nil || user.MachineKey != nil { + machineKey := command.AddMachineKey{} + if defaultMachine.MachineKey != nil { + machineKey = *defaultMachine.MachineKey + } + if user.MachineKey != nil { + if user.MachineKey.Type != 0 { + machineKey.Type = authn.KeyTypeToDomain(user.MachineKey.Type) + } + if user.MachineKey.ExpirationDate != nil { + machineKey.ExpirationDate = user.MachineKey.ExpirationDate.AsTime() + } + } + defaultMachine.MachineKey = &machineKey + } + return &defaultMachine +} + func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInstance command.InstanceSetup, externalDomain string) *command.InstanceSetup { if req.InstanceName != "" { defaultInstance.InstanceName = req.InstanceName diff --git a/internal/api/ui/login/register_org_handler.go b/internal/api/ui/login/register_org_handler.go index f56f429a27..4dd332298a 100644 --- a/internal/api/ui/login/register_org_handler.go +++ b/internal/api/ui/login/register_org_handler.go @@ -144,7 +144,7 @@ func (d registerOrgFormData) toCommandOrg() *command.OrgSetup { } return &command.OrgSetup{ Name: d.RegisterOrgName, - Human: command.AddHuman{ + Human: &command.AddHuman{ Username: d.Username, FirstName: d.Firstname, LastName: d.Lastname, diff --git a/internal/command/instance.go b/internal/command/instance.go index b856fd3495..bb6077f05e 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -151,30 +151,30 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) { return nil } -func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, *domain.ObjectDetails, error) { +func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, *domain.ObjectDetails, error) { instanceID, err := c.idGenerator.Next() if err != nil { - return "", nil, err + return "", "", nil, nil, err } if err = c.eventstore.NewInstance(ctx, instanceID); err != nil { - return "", nil, err + return "", "", nil, nil, err } ctx = authz.SetCtxData(authz.WithRequestedDomain(authz.WithInstanceID(ctx, instanceID), c.externalDomain), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID}) orgID, err := c.idGenerator.Next() if err != nil { - return "", nil, err + return "", "", nil, nil, err } userID, err := c.idGenerator.Next() if err != nil { - return "", nil, err + return "", "", nil, nil, err } if err = setup.generateIDs(c.idGenerator); err != nil { - return "", nil, err + return "", "", nil, nil, err } ctx = authz.WithConsole(ctx, setup.zitadel.projectID, setup.zitadel.consoleAppID) @@ -285,10 +285,40 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str validations = append(validations, AddOrgCommand(ctx, orgAgg, setup.Org.Name), c.prepareSetDefaultOrg(instanceAgg, orgAgg.ID), - AddHumanCommand(userAgg, &setup.Org.Human, c.userPasswordAlg, c.userEncryption), + ) + + var pat *PersonalAccessToken + var machineKey *MachineKey + // only a human or a machine user should be created as owner + if setup.Org.Machine != nil && setup.Org.Machine.Machine != nil && !setup.Org.Machine.Machine.IsZero() { + validations = append(validations, + AddMachineCommand(userAgg, setup.Org.Machine.Machine), + ) + if setup.Org.Machine.Pat != nil { + pat = NewPersonalAccessToken(orgID, userID, setup.Org.Machine.Pat.ExpirationDate, setup.Org.Machine.Pat.Scopes, domain.UserTypeMachine) + pat.TokenID, err = c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm)) + } + if setup.Org.Machine.MachineKey != nil { + machineKey = NewMachineKey(orgID, userID, setup.Org.Machine.MachineKey.ExpirationDate, setup.Org.Machine.MachineKey.Type) + machineKey.KeyID, err = c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize)) + } + } else if setup.Org.Human != nil { + validations = append(validations, + AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption), + ) + } + + validations = append(validations, c.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), c.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), - AddProjectCommand(projectAgg, zitadelProjectName, userID, false, false, false, domain.PrivateLabelingSettingUnspecified), SetIAMProject(instanceAgg, projectAgg.ID), @@ -334,7 +364,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str addGeneratedDomain, err := c.addGeneratedInstanceDomain(ctx, instanceAgg, setup.InstanceName) if err != nil { - return "", nil, err + return "", "", nil, nil, err } validations = append(validations, addGeneratedDomain...) if setup.CustomDomain != "" { @@ -372,14 +402,20 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { - return "", nil, err + return "", "", nil, nil, err } events, err := c.eventstore.Push(ctx, cmds...) if err != nil { - return "", nil, err + return "", "", nil, nil, err } - return instanceID, &domain.ObjectDetails{ + + var token string + if pat != nil { + token = pat.Token + } + + return instanceID, token, machineKey, &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), ResourceOwner: orgID, diff --git a/internal/command/org.go b/internal/command/org.go index 3f8f99a807..25aaf4315d 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -17,7 +17,8 @@ import ( type OrgSetup struct { Name string CustomDomain string - Human AddHuman + Human *AddHuman + Machine *AddMachine Roles []string } @@ -30,10 +31,11 @@ func (c *Commands) SetUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user return "", nil, errors.ThrowPreconditionFailed(nil, "COMMAND-poaj2", "Errors.Org.AlreadyExisting") } - return c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) + userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) + return userID, details, err } -func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, *domain.ObjectDetails, error) { +func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, string, *MachineKey, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(orgID) userAgg := user_repo.NewAggregate(userID, orgID) @@ -44,23 +46,55 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user validations := []preparation.Validation{ AddOrgCommand(ctx, orgAgg, o.Name, userIDs...), - AddHumanCommand(userAgg, &o.Human, c.userPasswordAlg, c.userEncryption), - c.AddOrgMemberCommand(orgAgg, userID, roles...), } + + var pat *PersonalAccessToken + var machineKey *MachineKey + if o.Human != nil { + validations = append(validations, AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption)) + } else if o.Machine != nil { + validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine)) + if o.Machine.Pat != nil { + pat = NewPersonalAccessToken(orgID, userID, o.Machine.Pat.ExpirationDate, o.Machine.Pat.Scopes, domain.UserTypeMachine) + tokenID, err := c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + pat.TokenID = tokenID + validations = append(validations, prepareAddPersonalAccessToken(pat, c.keyAlgorithm)) + } + if o.Machine.MachineKey != nil { + machineKey = NewMachineKey(orgID, userID, o.Machine.MachineKey.ExpirationDate, o.Machine.MachineKey.Type) + keyID, err := c.idGenerator.Next() + if err != nil { + return "", "", nil, nil, err + } + machineKey.KeyID = keyID + validations = append(validations, prepareAddUserMachineKey(machineKey, c.keySize)) + } + } + validations = append(validations, c.AddOrgMemberCommand(orgAgg, userID, roles...)) + if o.CustomDomain != "" { validations = append(validations, c.prepareAddOrgDomain(orgAgg, o.CustomDomain, userIDs)) } cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { - return "", nil, err + return "", "", nil, nil, err } events, err := c.eventstore.Push(ctx, cmds...) if err != nil { - return "", nil, err + return "", "", nil, nil, err } - return userID, &domain.ObjectDetails{ + + var token string + if pat != nil { + token = pat.Token + } + + return userID, token, machineKey, &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), ResourceOwner: orgID, @@ -78,7 +112,8 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string) return "", nil, err } - return c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) + userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) + return userID, details, err } // AddOrgCommand defines the commands to create a new org, diff --git a/internal/command/user.go b/internal/command/user.go index 4d04a18e76..bb04cd2727 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -449,9 +449,6 @@ func userWriteModelByID(ctx context.Context, filter preparation.FilterToQueryRed if err != nil { return nil, err } - if len(events) == 0 { - return nil, nil - } user.AppendEvents(events...) err = user.Reduce() return user, err diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index fed5c4b736..f78798529a 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -11,6 +11,12 @@ import ( "github.com/zitadel/zitadel/internal/repository/user" ) +type AddMachine struct { + Machine *Machine + Pat *AddPat + MachineKey *AddMachineKey +} + type Machine struct { models.ObjectRoot @@ -19,14 +25,41 @@ type Machine struct { Description string } -func (m *Machine) content() error { - if m.ResourceOwner == "" { - return caos_errs.ThrowInvalidArgument(nil, "COMMAND-xiown2", "Errors.ResourceOwnerMissing") +func (m *Machine) IsZero() bool { + return m.Username == "" && m.Name == "" +} + +func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if a.ResourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-xiown2", "Errors.ResourceOwnerMissing") + } + if a.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-p0p2mi", "Errors.User.UserIDMissing") + } + if machine.Name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bs9Ds", "Errors.User.Invalid") + } + if machine.Username == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bm9Ds", "Errors.User.Invalid") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + if err != nil { + return nil, err + } + if isUserStateExists(writeModel.UserState) { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2una", "Errors.User.AlreadyExisting") + } + domainPolicy, err := domainPolicyWriteModel(ctx, filter) + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotFound") + } + return []eventstore.Command{ + user.NewMachineAddedEvent(ctx, &a.Aggregate, machine.Username, machine.Name, machine.Description, domainPolicy.UserLoginMustBeDomain), + }, nil + }, nil } - if m.AggregateID == "" { - return caos_errs.ThrowInvalidArgument(nil, "COMMAND-p0p2mi", "Errors.User.UserIDMissing") - } - return nil } func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (*domain.ObjectDetails, error) { @@ -37,20 +70,18 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (*domain.Ob } machine.AggregateID = userID } - domainPolicy, err := c.getOrgDomainPolicy(ctx, machine.ResourceOwner) - if err != nil { - return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotFound") - } - validation := prepareAddUserMachine(machine, domainPolicy) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddMachineCommand(agg, machine)) if err != nil { return nil, err } + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } + return &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), @@ -58,49 +89,18 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (*domain.Ob }, nil } -func prepareAddUserMachine(machine *Machine, domainPolicy *domain.DomainPolicy) preparation.Validation { - return func() (_ preparation.CreateCommands, err error) { - if err := machine.content(); err != nil { - return nil, err - } - if machine.Name == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bs9Ds", "Errors.User.Invalid") - } - if machine.Username == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bm9Ds", "Errors.User.Invalid") - } - return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModelByID(ctx, filter, machine.AggregateID, machine.ResourceOwner) - if err != nil { - return nil, err - } - if isUserStateExists(writeModel.UserState) { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2una", "Errors.User.AlreadyExisting") - } - return []eventstore.Command{ - user.NewMachineAddedEvent( - ctx, - UserAggregateFromWriteModel(&writeModel.WriteModel), - machine.Username, - machine.Name, - machine.Description, - domainPolicy.UserLoginMustBeDomain, - ), - }, nil - }, nil - } -} - func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain.ObjectDetails, error) { - validation := prepareChangeUserMachine(machine) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, changeMachineCommand(agg, machine)) if err != nil { return nil, err } + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } + return &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), @@ -108,21 +108,23 @@ func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain }, nil } -func prepareChangeUserMachine(machine *Machine) preparation.Validation { +func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if err := machine.content(); err != nil { - return nil, err + if a.ResourceOwner == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") + } + if a.ID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-p0p3mi", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModelByID(ctx, filter, machine.AggregateID, machine.ResourceOwner) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) if err != nil { return nil, err } if !isUserStateExists(writeModel.UserState) { return nil, caos_errs.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound") } - - event, hasChanged, err := writeModel.NewChangedEvent(ctx, UserAggregateFromWriteModel(&writeModel.WriteModel), machine.Name, machine.Description) + changedEvent, hasChanged, err := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description) if err != nil { return nil, err } @@ -131,18 +133,21 @@ func prepareChangeUserMachine(machine *Machine) preparation.Validation { } return []eventstore.Command{ - event, + changedEvent, }, nil }, nil } } -func getMachineWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceOwner string) (_ *MachineWriteModel, err error) { +func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer) (*MachineWriteModel, error) { writeModel := NewMachineWriteModel(userID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } + if len(events) == 0 { + return writeModel, nil + } writeModel.AppendEvents(events...) err = writeModel.Reduce() return writeModel, err diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index a10cdc0da1..abf8cd01e5 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -2,108 +2,204 @@ package command import ( "context" + "time" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/user" - "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddUserMachineKeyWithID(ctx context.Context, machineKey *domain.MachineKey, resourceOwner string) (*domain.MachineKey, error) { - writeModel, err := c.machineKeyWriteModelByID(ctx, machineKey.AggregateID, machineKey.KeyID, resourceOwner) - if err != nil { - return nil, err - } - if writeModel.State != domain.MachineKeyStateUnspecified { - return nil, errors.ThrowNotFound(nil, "COMMAND-p22101", "Errors.User.Machine.Key.AlreadyExisting") - } - return c.addUserMachineKey(ctx, machineKey, resourceOwner) +type AddMachineKey struct { + Type domain.AuthNKeyType + ExpirationDate time.Time } -func (c *Commands) AddUserMachineKey(ctx context.Context, machineKey *domain.MachineKey, resourceOwner string) (*domain.MachineKey, error) { - keyID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - machineKey.KeyID = keyID - return c.addUserMachineKey(ctx, machineKey, resourceOwner) +type MachineKey struct { + models.ObjectRoot + + KeyID string + Type domain.AuthNKeyType + ExpirationDate time.Time + PrivateKey []byte + PublicKey []byte } -func (c *Commands) addUserMachineKey(ctx context.Context, machineKey *domain.MachineKey, resourceOwner string) (*domain.MachineKey, error) { - err := c.checkUserExists(ctx, machineKey.AggregateID, resourceOwner) - if err != nil { - return nil, err - } - keyWriteModel := NewMachineKeyWriteModel(machineKey.AggregateID, machineKey.KeyID, resourceOwner) - if err := c.eventstore.FilterToQueryReducer(ctx, keyWriteModel); err != nil { - return nil, err +func NewMachineKey(resourceOwner string, userID string, expirationDate time.Time, keyType domain.AuthNKeyType) *MachineKey { + return &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + ExpirationDate: expirationDate, + Type: keyType, } +} - if err := domain.EnsureValidExpirationDate(machineKey); err != nil { - return nil, err - } +func (key *MachineKey) SetPublicKey(publicKey []byte) { + key.PublicKey = publicKey +} +func (key *MachineKey) SetPrivateKey(privateKey []byte) { + key.PrivateKey = privateKey +} +func (key *MachineKey) GetExpirationDate() time.Time { + return key.ExpirationDate +} +func (key *MachineKey) SetExpirationDate(t time.Time) { + key.ExpirationDate = t +} - if len(machineKey.PublicKey) == 0 { - if err := domain.SetNewAuthNKeyPair(machineKey, c.machineKeySize); err != nil { +func (key *MachineKey) Detail() ([]byte, error) { + if len(key.PrivateKey) == 0 { + return nil, errors.ThrowPreconditionFailed(nil, "KEY-sp2l2m", "Errors.Internal") + } + if key.Type == domain.AuthNKeyTypeJSON { + return domain.MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.AggregateID) + } + return nil, errors.ThrowPreconditionFailed(nil, "KEY-dsg52", "Errors.Internal") +} + +func (key *MachineKey) content() error { + if key.ResourceOwner == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-kqpoix", "Errors.ResourceOwnerMissing") + } + if key.AggregateID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-xuiwk2", "Errors.User.UserIDMissing") + } + if key.KeyID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-0p2m1h", "Errors.IDMissing") + } + return nil +} + +func (key *MachineKey) valid() (err error) { + if err := key.content(); err != nil { + return err + } + key.ExpirationDate, err = domain.ValidateExpirationDate(key.ExpirationDate) + return err +} + +func (key *MachineKey) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { + if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner); err != nil || !exists { + return errors.ThrowPreconditionFailed(err, "COMMAND-bnipwm1", "Errors.User.NotFound") + } + return nil +} + +func (c *Commands) AddUserMachineKey(ctx context.Context, machineKey *MachineKey) (*domain.ObjectDetails, error) { + if machineKey.KeyID == "" { + keyID, err := c.idGenerator.Next() + if err != nil { return nil, err } + machineKey.KeyID = keyID } - events, err := c.eventstore.Push(ctx, - user.NewMachineKeyAddedEvent( - ctx, - UserAggregateFromWriteModel(&keyWriteModel.WriteModel), - machineKey.KeyID, - machineKey.Type, - machineKey.ExpirationDate, - machineKey.PublicKey)) + validation := prepareAddUserMachineKey(machineKey, c.machineKeySize) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err } - err = AppendAndReduce(keyWriteModel, events...) + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } - - key := keyWriteModelToMachineKey(keyWriteModel) - if len(machineKey.PrivateKey) > 0 { - key.PrivateKey = machineKey.PrivateKey - } - return key, nil + return &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) RemoveUserMachineKey(ctx context.Context, userID, keyID, resourceOwner string) (*domain.ObjectDetails, error) { - keyWriteModel, err := c.machineKeyWriteModelByID(ctx, userID, keyID, resourceOwner) - if err != nil { - return nil, err +func prepareAddUserMachineKey(machineKey *MachineKey, keySize int) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := machineKey.valid(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if err := machineKey.checkAggregate(ctx, filter); err != nil { + return nil, err + } + if len(machineKey.PublicKey) == 0 { + if err = domain.SetNewAuthNKeyPair(machineKey, keySize); err != nil { + return nil, err + } + } + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + if err != nil { + return nil, err + } + if writeModel.Exists() { + return nil, errors.ThrowAlreadyExists(nil, "COMMAND-091mops", "Errors.User.Machine.Key.AlreadyExists") + } + return []eventstore.Command{ + user.NewMachineKeyAddedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + machineKey.KeyID, + machineKey.Type, + machineKey.ExpirationDate, + machineKey.PublicKey, + ), + }, nil + }, nil } - if !keyWriteModel.Exists() { - return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.Machine.Key.NotFound") - } - - pushedEvents, err := c.eventstore.Push(ctx, - user.NewMachineKeyRemovedEvent(ctx, UserAggregateFromWriteModel(&keyWriteModel.WriteModel), keyID)) - if err != nil { - return nil, err - } - err = AppendAndReduce(keyWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&keyWriteModel.WriteModel), nil } -func (c *Commands) machineKeyWriteModelByID(ctx context.Context, userID, keyID, resourceOwner string) (writeModel *MachineKeyWriteModel, err error) { - if userID == "" { - return nil, errors.ThrowInvalidArgument(nil, "COMMAND-4n8vs", "Errors.User.UserIDMissing") - } - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - writeModel = NewMachineKeyWriteModel(userID, keyID, resourceOwner) - err = c.eventstore.FilterToQueryReducer(ctx, writeModel) +func (c *Commands) RemoveUserMachineKey(ctx context.Context, machineKey *MachineKey) (*domain.ObjectDetails, error) { + validation := prepareRemoveUserMachineKey(machineKey) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err } - return writeModel, nil + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := machineKey.content(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + if err != nil { + return nil, err + } + if !writeModel.Exists() { + return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.Machine.Key.NotFound") + } + return []eventstore.Command{ + user.NewMachineKeyRemovedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + machineKey.KeyID, + ), + }, nil + }, nil + } +} + +func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string) (_ *MachineKeyWriteModel, err error) { + writeModel := NewMachineKeyWriteModel(userID, keyID, resourceOwner) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return writeModel, nil + } + writeModel.AppendEvents(events...) + err = writeModel.Reduce() + return writeModel, err } diff --git a/internal/command/user_machine_key_test.go b/internal/command/user_machine_key_test.go new file mode 100644 index 0000000000..b2def53fa3 --- /dev/null +++ b/internal/command/user_machine_key_test.go @@ -0,0 +1,253 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/id" + id_mock "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/user" +) + +func TestCommands_AddMachineKey(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + keyAlgorithm crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + key *MachineKey + } + type res struct { + want *domain.ObjectDetails + key bool + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "user does not exist, error", + fields{ + eventstore: eventstoreExpect(t, + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), + }, + args{ + ctx: context.Background(), + key: &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Time{}, + }, + }, + res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + "invalid expiration date, error", + fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), + }, + args{ + ctx: context.Background(), + key: &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Now().Add(-24 * time.Hour), + }, + }, + res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + "no userID, error", + fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), + }, + args{ + ctx: context.Background(), + key: &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "", + ResourceOwner: "org1", + }, + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Time{}, + }, + }, + res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + "no resourceowner, error", + fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), + }, + args{ + ctx: context.Background(), + key: &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "", + }, + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Time{}, + }, + }, + res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + "key added with public key", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "machine", + "Machine", + "", + true, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewMachineKeyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key1", + domain.AuthNKeyTypeJSON, + time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + []byte("public"), + ), + ), + }, + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: context.Background(), + key: &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + PublicKey: []byte("public"), + }, + }, + res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + key: true, + }, + }, + { + "key added with ID and public key", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "machine", + "Machine", + "", + true, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewMachineKeyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key1", + domain.AuthNKeyTypeJSON, + time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + []byte("public"), + ), + ), + }, + ), + ), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: context.Background(), + key: &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + KeyID: "key1", + Type: domain.AuthNKeyTypeJSON, + ExpirationDate: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + PublicKey: []byte("public"), + }, + }, + res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + key: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + keyAlgorithm: tt.fields.keyAlgorithm, + } + got, err := c.AddUserMachineKey(tt.args.ctx, tt.args.key) + if tt.res.err == nil { + assert.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) + if tt.res.key { + assert.NotEqual(t, "", tt.args.key.PrivateKey) + } + } + }) + } +} diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index b6248dc445..90dc648f46 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -41,16 +41,6 @@ func TestCommandSide_AddMachine(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, - ), - ), - ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), }, @@ -72,16 +62,6 @@ func TestCommandSide_AddMachine(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, - expectFilter( - eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - true, - true, - true, - ), - ), - ), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), }, @@ -105,6 +85,7 @@ func TestCommandSide_AddMachine(t *testing.T) { t, expectFilter(), expectFilter(), + expectFilter(), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), }, @@ -127,6 +108,7 @@ func TestCommandSide_AddMachine(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -137,7 +119,6 @@ func TestCommandSide_AddMachine(t *testing.T) { ), ), ), - expectFilter(), expectPush( []*repository.Event{ eventFromEventPusher( @@ -212,7 +193,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) { res res }{ { - name: "user invalid, invalid argument error", + name: "user invalid, invalid argument error name", fields: fields{ eventstore: eventstoreExpect( t, @@ -231,6 +212,26 @@ func TestCommandSide_ChangeMachine(t *testing.T) { err: caos_errs.IsErrorInvalidArgument, }, }, + { + name: "user invalid, invalid argument error username", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "username", + }, + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, { name: "user not existing, precondition error", fields: fields{ @@ -279,6 +280,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) { ResourceOwner: "org1", AggregateID: "user1", }, + Username: "username", Name: "name", Description: "description", }, diff --git a/internal/command/user_personal_access_token.go b/internal/command/user_personal_access_token.go index 56722c9c08..4f252c08c8 100644 --- a/internal/command/user_personal_access_token.go +++ b/internal/command/user_personal_access_token.go @@ -2,91 +2,195 @@ package command import ( "context" + "encoding/base64" "time" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/user" - "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func (c *Commands) AddPersonalAccessToken(ctx context.Context, userID, resourceOwner string, expirationDate time.Time, scopes []string, allowedUserType domain.UserType) (*domain.Token, string, error) { - userWriteModel, err := c.userWriteModelByID(ctx, userID, resourceOwner) +type AddPat struct { + ExpirationDate time.Time + Scopes []string +} + +type PersonalAccessToken struct { + models.ObjectRoot + + ExpirationDate time.Time + Scopes []string + AllowedUserType domain.UserType + + TokenID string + Token string +} + +func NewPersonalAccessToken(resourceOwner string, userID string, expirationDate time.Time, scopes []string, allowedUserType domain.UserType) *PersonalAccessToken { + return &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + ExpirationDate: expirationDate, + Scopes: scopes, + AllowedUserType: allowedUserType, + } +} + +func (pat *PersonalAccessToken) content() error { + if pat.ResourceOwner == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-xs0k2n", "Errors.ResourceOwnerMissing") + } + if pat.AggregateID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-0pzb1", "Errors.User.UserIDMissing") + } + if pat.TokenID == "" { + return errors.ThrowInvalidArgument(nil, "COMMAND-68xm2o", "Errors.IDMissing") + } + return nil +} + +func (pat *PersonalAccessToken) valid() (err error) { + if err := pat.content(); err != nil { + return err + } + pat.ExpirationDate, err = domain.ValidateExpirationDate(pat.ExpirationDate) + return err +} + +func (pat *PersonalAccessToken) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { + userWriteModel, err := userWriteModelByID(ctx, filter, pat.AggregateID, pat.ResourceOwner) if err != nil { - return nil, "", err + return err } if !isUserStateExists(userWriteModel.UserState) { - return nil, "", errors.ThrowPreconditionFailed(nil, "COMMAND-Dggw2", "Errors.User.NotFound") + return errors.ThrowPreconditionFailed(nil, "COMMAND-Dggw2", "Errors.User.NotFound") } - if allowedUserType != domain.UserTypeUnspecified && userWriteModel.UserType != allowedUserType { - return nil, "", errors.ThrowPreconditionFailed(nil, "COMMAND-Df2f1", "Errors.User.WrongType") + if pat.AllowedUserType != domain.UserTypeUnspecified && userWriteModel.UserType != pat.AllowedUserType { + return errors.ThrowPreconditionFailed(nil, "COMMAND-Df2f1", "Errors.User.WrongType") } - tokenID, err := c.idGenerator.Next() - if err != nil { - return nil, "", err - } - tokenWriteModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) - err = c.eventstore.FilterToQueryReducer(ctx, tokenWriteModel) - if err != nil { - return nil, "", err - } - - expirationDate, err = domain.ValidateExpirationDate(expirationDate) - if err != nil { - return nil, "", err - } - - events, err := c.eventstore.Push(ctx, - user.NewPersonalAccessTokenAddedEvent( - ctx, - UserAggregateFromWriteModel(&tokenWriteModel.WriteModel), - tokenID, - expirationDate, - scopes, - ), - ) - if err != nil { - return nil, "", err - } - err = AppendAndReduce(tokenWriteModel, events...) - if err != nil { - return nil, "", err - } - return personalTokenWriteModelToToken(tokenWriteModel, c.keyAlgorithm) + return nil } -func (c *Commands) RemovePersonalAccessToken(ctx context.Context, userID, tokenID, resourceOwner string) (*domain.ObjectDetails, error) { - tokenWriteModel, err := c.personalAccessTokenWriteModelByID(ctx, userID, tokenID, resourceOwner) +func (c *Commands) AddPersonalAccessToken(ctx context.Context, pat *PersonalAccessToken) (_ *domain.ObjectDetails, err error) { + if pat.TokenID == "" { + pat.TokenID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + validation := prepareAddPersonalAccessToken(pat, c.keyAlgorithm) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err } - if !tokenWriteModel.Exists() { - return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.PAT.NotFound") - } - - pushedEvents, err := c.eventstore.Push(ctx, - user.NewPersonalAccessTokenRemovedEvent(ctx, UserAggregateFromWriteModel(&tokenWriteModel.WriteModel), tokenID)) + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err } - err = AppendAndReduce(tokenWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&tokenWriteModel.WriteModel), nil + return &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) personalAccessTokenWriteModelByID(ctx context.Context, userID, tokenID, resourceOwner string) (writeModel *PersonalAccessTokenWriteModel, err error) { - if userID == "" { - return nil, errors.ThrowInvalidArgument(nil, "COMMAND-4n8vs", "Errors.User.UserIDMissing") - } - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() +func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.EncryptionAlgorithm) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := pat.valid(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { + if err := pat.checkAggregate(ctx, filter); err != nil { + return nil, err + } + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + if err != nil { + return nil, err + } - writeModel = NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) - err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID) + if err != nil { + return nil, err + } + + return []eventstore.Command{ + user.NewPersonalAccessTokenAddedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + pat.TokenID, + pat.ExpirationDate, + pat.Scopes, + ), + }, nil + }, nil + } +} + +func (c *Commands) RemovePersonalAccessToken(ctx context.Context, pat *PersonalAccessToken) (*domain.ObjectDetails, error) { + validation := prepareRemovePersonalAccessToken(pat) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err } - return writeModel, nil + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + +func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Validation { + return func() (_ preparation.CreateCommands, err error) { + if err := pat.content(); err != nil { + return nil, err + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + if err != nil { + return nil, err + } + if !writeModel.Exists() { + return nil, errors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.User.PAT.NotFound") + } + return []eventstore.Command{ + user.NewPersonalAccessTokenRemovedEvent( + ctx, + UserAggregateFromWriteModel(&writeModel.WriteModel), + pat.TokenID, + ), + }, nil + }, nil + } +} + +func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) (string, error) { + encrypted, err := algorithm.Encrypt([]byte(tokenID + ":" + userID)) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(encrypted), nil +} + +func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string) (_ *PersonalAccessTokenWriteModel, err error) { + writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return writeModel, nil + } + writeModel.AppendEvents(events...) + err = writeModel.Reduce() + return writeModel, err } diff --git a/internal/command/user_personal_access_token_test.go b/internal/command/user_personal_access_token_test.go index bf50445557..53a73e4dc8 100644 --- a/internal/command/user_personal_access_token_test.go +++ b/internal/command/user_personal_access_token_test.go @@ -32,15 +32,11 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { keyAlgorithm crypto.EncryptionAlgorithm } type args struct { - ctx context.Context - userID string - resourceOwner string - expirationDate time.Time - scopes []string - allowedUserType domain.UserType + ctx context.Context + pat *PersonalAccessToken } type res struct { - want *domain.Token + want *domain.ObjectDetails token string err func(error) bool } @@ -56,13 +52,18 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { eventstore: eventstoreExpect(t, expectFilter(), ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), }, args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - scopes: []string{"openid"}, - expirationDate: time.Time{}, + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Scopes: []string{"openid"}, + ExpirationDate: time.Time{}, + }, }, res{ err: caos_errs.IsPreconditionFailed, @@ -84,14 +85,19 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { ), ), ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), }, args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - expirationDate: time.Time{}, - scopes: []string{"openid"}, - allowedUserType: domain.UserTypeHuman, + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Scopes: []string{"openid"}, + ExpirationDate: time.Time{}, + AllowedUserType: domain.UserTypeHuman, + }, }, res{ err: caos_errs.IsPreconditionFailed, @@ -100,28 +106,58 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { { "invalid expiration date, error", fields{ - eventstore: eventstoreExpect(t, - expectFilter( - eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), - &user.NewAggregate("user1", "org1").Aggregate, - "machine", - "Machine", - "", - true, - ), - ), - ), - expectFilter(), - ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), }, args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - expirationDate: time.Now().Add(-24 * time.Hour), - scopes: []string{"openid"}, + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Scopes: []string{"openid"}, + ExpirationDate: time.Now().Add(-24 * time.Hour), + }, + }, + res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + "no userID, error", + fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), + }, + args{ + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "", + ResourceOwner: "org1", + }, + Scopes: []string{"openid"}, + ExpirationDate: time.Time{}, + }, + }, + res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + "no resourceowner, error", + fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "token1"), + }, + args{ + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "", + }, + Scopes: []string{"openid"}, + ExpirationDate: time.Time{}, + }, }, res{ err: caos_errs.IsErrorInvalidArgument, @@ -160,20 +196,71 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args{ - ctx: context.Background(), - userID: "user1", - resourceOwner: "org1", - expirationDate: time.Time{}, - scopes: []string{"openid"}, - }, - res{ - want: &domain.Token{ + ctx: context.Background(), + pat: &PersonalAccessToken{ ObjectRoot: models.ObjectRoot{ AggregateID: "user1", ResourceOwner: "org1", }, - TokenID: "token1", - Expiration: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + Scopes: []string{"openid"}, + ExpirationDate: time.Time{}, + AllowedUserType: domain.UserTypeMachine, + }, + }, + res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + token: base64.RawURLEncoding.EncodeToString([]byte("token1:user1")), + }, + }, + { + "token added with ID", + fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusher( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "machine", + "Machine", + "", + true, + ), + ), + ), + expectFilter(), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewPersonalAccessTokenAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "token1", + time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), + []string{"openid"}, + ), + ), + }, + ), + ), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args{ + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + TokenID: "token1", + Scopes: []string{"openid"}, + ExpirationDate: time.Time{}, + AllowedUserType: domain.UserTypeMachine, + }, + }, + res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, token: base64.RawURLEncoding.EncodeToString([]byte("token1:user1")), }, @@ -186,7 +273,7 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { idGenerator: tt.fields.idGenerator, keyAlgorithm: tt.fields.keyAlgorithm, } - got, token, err := c.AddPersonalAccessToken(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.expirationDate, tt.args.scopes, tt.args.allowedUserType) + got, err := c.AddPersonalAccessToken(tt.args.ctx, tt.args.pat) if tt.res.err == nil { assert.NoError(t, err) } @@ -195,7 +282,7 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.want, got) - assert.Equal(t, tt.res.token, token) + assert.Equal(t, tt.res.token, tt.args.pat.Token) } }) } @@ -206,10 +293,8 @@ func TestCommands_RemovePersonalAccessToken(t *testing.T) { eventstore *eventstore.Eventstore } type args struct { - ctx context.Context - userID string - tokenID string - resourceOwner string + ctx context.Context + pat *PersonalAccessToken } type res struct { want *domain.ObjectDetails @@ -229,10 +314,14 @@ func TestCommands_RemovePersonalAccessToken(t *testing.T) { ), }, args{ - ctx: context.Background(), - userID: "user1", - tokenID: "token1", - resourceOwner: "org1", + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + TokenID: "token1", + }, }, res{ err: caos_errs.IsNotFound, @@ -265,10 +354,14 @@ func TestCommands_RemovePersonalAccessToken(t *testing.T) { ), }, args{ - ctx: context.Background(), - userID: "user1", - tokenID: "token1", - resourceOwner: "org1", + ctx: context.Background(), + pat: &PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + TokenID: "token1", + }, }, res{ want: &domain.ObjectDetails{ @@ -282,7 +375,7 @@ func TestCommands_RemovePersonalAccessToken(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore, } - got, err := c.RemovePersonalAccessToken(tt.args.ctx, tt.args.userID, tt.args.tokenID, tt.args.resourceOwner) + got, err := c.RemovePersonalAccessToken(tt.args.ctx, tt.args.pat) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/domain/application_key.go b/internal/domain/application_key.go index 01453cb69f..7d6407ee5f 100644 --- a/internal/domain/application_key.go +++ b/internal/domain/application_key.go @@ -20,19 +20,19 @@ type ApplicationKey struct { PublicKey []byte } -func (k *ApplicationKey) setPublicKey(publicKey []byte) { +func (k *ApplicationKey) SetPublicKey(publicKey []byte) { k.PublicKey = publicKey } -func (k *ApplicationKey) setPrivateKey(privateKey []byte) { +func (k *ApplicationKey) SetPrivateKey(privateKey []byte) { k.PrivateKey = privateKey } -func (k *ApplicationKey) expirationDate() time.Time { +func (k *ApplicationKey) GetExpirationDate() time.Time { return k.ExpirationDate } -func (k *ApplicationKey) setExpirationDate(expiration time.Time) { +func (k *ApplicationKey) SetExpirationDate(expiration time.Time) { k.ExpirationDate = expiration } diff --git a/internal/domain/authn_key.go b/internal/domain/authn_key.go index 213aa4c427..eb45e359ef 100644 --- a/internal/domain/authn_key.go +++ b/internal/domain/authn_key.go @@ -8,8 +8,8 @@ import ( ) type authNKey interface { - setPublicKey([]byte) - setPrivateKey([]byte) + SetPublicKey([]byte) + SetPrivateKey([]byte) expiration } @@ -44,8 +44,8 @@ func SetNewAuthNKeyPair(key authNKey, keySize int) error { if err != nil { return err } - key.setPrivateKey(privateKey) - key.setPublicKey(publicKey) + key.SetPrivateKey(privateKey) + key.SetPublicKey(publicKey) return nil } diff --git a/internal/domain/expiration.go b/internal/domain/expiration.go index d622924152..9b74702d16 100644 --- a/internal/domain/expiration.go +++ b/internal/domain/expiration.go @@ -12,16 +12,16 @@ var ( ) type expiration interface { - expirationDate() time.Time - setExpirationDate(time.Time) + GetExpirationDate() time.Time + SetExpirationDate(time.Time) } func EnsureValidExpirationDate(key expiration) error { - date, err := ValidateExpirationDate(key.expirationDate()) + date, err := ValidateExpirationDate(key.GetExpirationDate()) if err != nil { return err } - key.setExpirationDate(date) + key.SetExpirationDate(date) return nil } diff --git a/internal/domain/machine_key.go b/internal/domain/machine_key.go index 9f821fbe48..43abfd83b9 100644 --- a/internal/domain/machine_key.go +++ b/internal/domain/machine_key.go @@ -42,17 +42,7 @@ func (key *MachineKey) Detail() ([]byte, error) { } func (key *MachineKey) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - Type string `json:"type"` - KeyID string `json:"keyId"` - Key string `json:"key"` - UserID string `json:"userId"` - }{ - Type: "serviceaccount", - KeyID: key.KeyID, - Key: string(key.PrivateKey), - UserID: key.AggregateID, - }) + return MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.AggregateID) } type MachineKeyState int32 @@ -68,3 +58,17 @@ const ( func (f MachineKeyState) Valid() bool { return f >= 0 && f < machineKeyStateCount } + +func MachineKeyMarshalJSON(keyID string, privateKey []byte, userID string) ([]byte, error) { + return json.Marshal(struct { + Type string `json:"type"` + KeyID string `json:"keyId"` + Key string `json:"key"` + UserID string `json:"userId"` + }{ + Type: "serviceaccount", + KeyID: keyID, + Key: string(privateKey), + UserID: userID, + }) +} diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 7e5a49810e..b3837ec2db 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -3,6 +3,7 @@ syntax = "proto3"; import "zitadel/object.proto"; import "zitadel/options.proto"; import "zitadel/instance.proto"; +import "zitadel/auth_n_key.proto"; import "google/api/annotations.proto"; import "google/protobuf/timestamp.proto"; @@ -121,6 +122,7 @@ service SystemService { }; } + // Deprecated: Use CreateInstance instead // Creates a new instance with all needed setup data // This might take some time rpc AddInstance(AddInstanceRequest) returns (AddInstanceResponse) { @@ -146,6 +148,19 @@ service SystemService { }; } + // Creates a new instance with all needed setup data + // This might take some time + rpc CreateInstance(CreateInstanceRequest) returns (CreateInstanceResponse) { + option (google.api.http) = { + post: "/instances/_create" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + // Removes a instances // This might take some time rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { @@ -409,6 +424,72 @@ message AddInstanceResponse { zitadel.v1.ObjectDetails details = 2; } +message CreateInstanceRequest { + message Profile { + string first_name = 1 [(validate.rules).string = {max_len: 200}]; + string last_name = 2 [(validate.rules).string = {max_len: 200}]; + string preferred_language = 3 [(validate.rules).string = {max_len: 10}]; + } + message Email { + string email = 1[(validate.rules).string = {min_len: 1, max_len: 200, email: true}]; + bool is_email_verified = 2; + } + message Password { + string password = 1 [(validate.rules).string = {max_len: 200}]; + bool password_change_required = 2; + } + message Human { + string user_name = 1 [(validate.rules).string = {max_len: 200}]; + Email email = 2 [(validate.rules).message.required = true]; + Profile profile = 3 [(validate.rules).message.required = false]; + Password password = 4 [(validate.rules).message.required = false]; + } + message PersonalAccessToken { + google.protobuf.Timestamp expiration_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + description: "The date the token will expire and no logins will be possible"; + } + ]; + } + message MachineKey { + zitadel.authn.v1.KeyType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; + google.protobuf.Timestamp expiration_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + description: "The date the key will expire and no logins will be possible"; + } + ]; + } + message Machine { + string user_name = 1 [(validate.rules).string = {max_len: 200}]; + string name = 2 [(validate.rules).string = {max_len: 200}]; + PersonalAccessToken personal_access_token = 3; + MachineKey machine_key = 4; + } + + string instance_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string first_org_name = 2 [(validate.rules).string = {max_len: 200}]; + string custom_domain = 3 [(validate.rules).string = {max_len: 200}]; + + oneof owner { + option (validate.required) = true; + + // oneof field for the user managing the instance + Human human = 4; + Machine machine = 5; + } + + string default_language = 6 [(validate.rules).string = {max_len: 10}]; +} + +message CreateInstanceResponse { + string instance_id = 1; + zitadel.v1.ObjectDetails details = 2; + string pat = 3; + bytes machine_key = 4; +} + message UpdateInstanceRequest{ string instance_id = 1; string instance_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}];