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}];