diff --git a/cmd/setup/03.go b/cmd/setup/03.go
index 65aebf0481..2f0a09b807 100644
--- a/cmd/setup/03.go
+++ b/cmd/setup/03.go
@@ -88,7 +88,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
mig.instanceSetup.Org.Human.Email.Address = "admin@" + mig.instanceSetup.CustomDomain
}
- _, _, err = cmd.SetUpInstance(ctx, &mig.instanceSetup)
+ _, _, _, _, err = cmd.SetUpInstance(ctx, &mig.instanceSetup)
return err
}
diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md
index 8574121251..4fe7f0b4b7 100644
--- a/docs/docs/apis/proto/system.md
+++ b/docs/docs/apis/proto/system.md
@@ -57,6 +57,18 @@ This might take some time
POST: /instances
+### CreateInstance
+
+> **rpc** CreateInstance([CreateInstanceRequest](#createinstancerequest))
+[CreateInstanceResponse](#createinstanceresponse)
+
+
+
+
+
+ POST: /instances/_create
+
+
### RemoveInstance
> **rpc** RemoveInstance([RemoveInstanceRequest](#removeinstancerequest))
@@ -330,6 +342,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) user.human | CreateInstanceRequest.Human | oneof field for the user managing the instance | |
+| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) user.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
|
+| is_email_verified | bool | - | |
+
+
+
+
+### CreateInstanceRequest.Human
+
+
+
+| Field | Type | Description | Validation |
+| ----- | ---- | ----------- | ----------- |
+| email | CreateInstanceRequest.Email | - | message.required: true
|
+| profile | CreateInstanceRequest.Profile | - | message.required: false
|
+| password | CreateInstanceRequest.Password | - | message.required: false
|
+| user_name | string | - | string.max_len: 200
|
+
+
+
+
+### 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/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/system/instance.go b/internal/api/grpc/system/instance.go
index 135d41c6c0..5c13def162 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))
+ id, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.DefaultInstance))
if err != nil {
return nil, err
}
@@ -51,6 +51,19 @@ func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequ
}, 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))
+ if err != nil {
+ return nil, err
+ }
+ return &system_pb.CreateInstanceResponse{
+ Pat: pat,
+ MachineKey: key,
+ InstanceId: id,
+ Details: object.AddToDetailsPb(details.Sequence, details.EventDate, details.ResourceOwner),
+ }, nil
+}
+
func (s *Server) ExistsDomain(ctx context.Context, req *system_pb.ExistsDomainRequest) (*system_pb.ExistsDomainResponse, error) {
domainQuery, err := query.NewInstanceDomainDomainSearchQuery(query.TextEqualsIgnoreCase, req.Domain)
if err != nil {
diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go
index fb812b62fe..57c300920a 100644
--- a/internal/api/grpc/system/instance_converter.go
+++ b/internal/api/grpc/system/instance_converter.go
@@ -1,16 +1,94 @@
package system
import (
+ "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"
instance_pb "github.com/zitadel/zitadel/pkg/grpc/instance"
system_pb "github.com/zitadel/zitadel/pkg/grpc/system"
)
+func CreateInstancePbToSetupInstance(req *system_pb.CreateInstanceRequest, defaultInstance command.InstanceSetup) *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 {
+ defaultInstance.Org.Machine = &command.AddMachine{
+ Machine: &domain.Machine{},
+ }
+ if user.UserName != "" {
+ defaultInstance.Org.Machine.Machine.Username = user.UserName
+ }
+ if user.Name != "" {
+ defaultInstance.Org.Machine.Machine.Name = user.Name
+ }
+ if user.PersonalAccessToken != nil {
+ defaultInstance.Org.Machine.Pat = true
+ defaultInstance.Org.Machine.PatScopes = []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner}
+ if user.PersonalAccessToken.ExpirationDate != nil {
+ defaultInstance.Org.Machine.PatExpirationDate = user.PersonalAccessToken.ExpirationDate.AsTime()
+ }
+ }
+ if user.MachineKey != nil {
+ defaultInstance.Org.Machine.MachineKey = true
+ defaultInstance.Org.Machine.MachineKeyType = authn.KeyTypeToDomain(user.MachineKey.Type)
+ if user.MachineKey.ExpirationDate != nil {
+ defaultInstance.Org.Machine.MachineKeyExpirationDate = user.MachineKey.ExpirationDate.AsTime()
+ }
+ }
+ defaultInstance.Org.Human = nil
+ }
+ if user := req.GetHuman(); user != nil {
+ if user.Email != nil {
+ defaultInstance.Org.Human.Email.Address = user.Email.Email
+ defaultInstance.Org.Human.Email.Verified = user.Email.IsEmailVerified
+ }
+ if user.Profile != nil {
+ if user.Profile.FirstName != "" {
+ defaultInstance.Org.Human.FirstName = user.Profile.FirstName
+ }
+ if user.Profile.LastName != "" {
+ defaultInstance.Org.Human.LastName = user.Profile.LastName
+ }
+ if user.Profile.PreferredLanguage != "" {
+ lang, err := language.Parse(user.Profile.PreferredLanguage)
+ if err == nil {
+ defaultInstance.Org.Human.PreferredLanguage = lang
+ }
+ }
+ }
+ if user.UserName != "" {
+ defaultInstance.Org.Human.Username = user.UserName
+ }
+ if user.Password != nil {
+ defaultInstance.Org.Human.Password = user.Password.Password
+ defaultInstance.Org.Human.PasswordChangeRequired = user.Password.PasswordChangeRequired
+ }
+ defaultInstance.Org.Machine = nil
+ }
+
+ if lang := language.Make(req.DefaultLanguage); lang != language.Und {
+ defaultInstance.DefaultLanguage = lang
+ }
+
+ return &defaultInstance
+}
+
func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInstance command.InstanceSetup) *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 4338a86b97..159c9ace3f 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 f99e68d12b..2c9117fcae 100644
--- a/internal/command/instance.go
+++ b/internal/command/instance.go
@@ -13,6 +13,7 @@ import (
"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/id"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/repository/instance"
@@ -142,30 +143,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, []byte, *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)
@@ -273,10 +274,21 @@ 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),
+ )
+ //only a human or a machine user should be created as owner
+ if setup.Org.Human != nil {
+ validations = append(validations,
+ AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption),
+ )
+ } else if setup.Org.Machine != nil {
+ validations = append(validations,
+ AddMachineCommand(userAgg, setup.Org.Machine.Machine),
+ )
+ }
+
+ 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),
@@ -322,7 +334,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 != "" {
@@ -348,14 +360,40 @@ 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{
+
+ pat := ""
+ machineKey := make([]byte, 0)
+ if setup.Org.Machine != nil {
+ if setup.Org.Machine.Pat {
+ _, token, err := c.AddPersonalAccessToken(ctx, userID, orgID, setup.Org.Machine.PatExpirationDate, setup.Org.Machine.PatScopes, domain.UserTypeMachine)
+ if err != nil {
+ return "", "", nil, nil, err
+ }
+ pat = token
+ }
+ if setup.Org.Machine.MachineKey {
+ key, err := c.AddUserMachineKey(ctx, &domain.MachineKey{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: userID,
+ },
+ ExpirationDate: setup.Org.Machine.MachineKeyExpirationDate,
+ Type: setup.Org.Machine.MachineKeyType,
+ }, orgID)
+ if err != nil {
+ return "", "", nil, nil, err
+ }
+ machineKey = key.PrivateKey
+ }
+ }
+
+ return instanceID, pat, machineKey, &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: orgID,
@@ -444,7 +482,7 @@ func prepareAddInstance(a *instance.Aggregate, instanceName string, defaultLangu
}
}
-//SetIAMProject defines the command to set the id of the IAM project onto the instance
+// SetIAMProject defines the command to set the id of the IAM project onto the instance
func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
@@ -455,7 +493,7 @@ func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validati
}
}
-//SetIAMConsoleID defines the command to set the clientID of the Console App onto the instance
+// SetIAMConsoleID defines the command to set the clientID of the Console App onto the instance
func SetIAMConsoleID(a *instance.Aggregate, clientID, appID *string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
diff --git a/internal/command/org.go b/internal/command/org.go
index 76d60c1c4f..fd7230d831 100644
--- a/internal/command/org.go
+++ b/internal/command/org.go
@@ -10,6 +10,7 @@ import (
"github.com/zitadel/zitadel/internal/errors"
caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/org"
user_repo "github.com/zitadel/zitadel/internal/repository/user"
)
@@ -17,7 +18,8 @@ import (
type OrgSetup struct {
Name string
CustomDomain string
- Human AddHuman
+ Human *AddHuman
+ Machine *AddMachine
Roles []string
}
@@ -30,10 +32,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, []byte, *domain.ObjectDetails, error) {
orgAgg := org.NewAggregate(orgID)
userAgg := user_repo.NewAggregate(userID, orgID)
@@ -44,23 +47,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...),
}
+
+ 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))
+ }
+ 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{
+
+ pat := ""
+ machineKey := make([]byte, 0)
+ if o.Machine != nil {
+ if o.Machine.Pat {
+ _, token, err := c.AddPersonalAccessToken(ctx, userID, orgID, o.Machine.PatExpirationDate, o.Machine.PatScopes, domain.UserTypeMachine)
+ if err != nil {
+ return "", "", nil, nil, err
+ }
+ pat = token
+ }
+ if o.Machine.MachineKey {
+ key, err := c.AddUserMachineKey(ctx, &domain.MachineKey{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: userID,
+ },
+ ExpirationDate: o.Machine.MachineKeyExpirationDate,
+ Type: o.Machine.MachineKeyType,
+ }, orgID)
+ if err != nil {
+ return "", "", nil, nil, err
+ }
+ machineKey = key.PrivateKey
+ }
+ }
+
+ return userID, pat, machineKey, &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: orgID,
@@ -78,10 +113,11 @@ 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,
+// AddOrgCommand defines the commands to create a new org,
// this includes the verified default domain
func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs ...string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go
index 714f238eab..1dd4a4dea8 100644
--- a/internal/command/user_machine.go
+++ b/internal/command/user_machine.go
@@ -2,108 +2,166 @@ package command
import (
"context"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/command/preparation"
"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/v1/models"
"github.com/zitadel/zitadel/internal/repository/user"
- "github.com/zitadel/zitadel/internal/telemetry/tracing"
)
+type AddMachine struct {
+ Machine *domain.Machine
+ Pat bool
+ PatExpirationDate time.Time
+ PatScopes []string
+ MachineKey bool
+ MachineKeyType domain.AuthNKeyType
+ MachineKeyExpirationDate time.Time
+}
+
+func AddMachineCommand(a *user.Aggregate, machine *domain.Machine) preparation.Validation {
+ return func() (_ preparation.CreateCommands, err error) {
+ if !machine.IsValid() {
+ 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")
+ }
+ if !domainPolicy.UserLoginMustBeDomain {
+ return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0dd", "Errors.User.Invalid")
+ }
+
+ return []eventstore.Command{
+ user.NewMachineAddedEvent(ctx, &a.Aggregate, machine.Username, machine.Name, machine.Description, domainPolicy.UserLoginMustBeDomain),
+ }, nil
+ }, nil
+ }
+}
+
func (c *Commands) AddMachine(ctx context.Context, orgID string, machine *domain.Machine) (*domain.Machine, error) {
- if !machine.IsValid() {
- return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-bm9Ds", "Errors.User.Invalid")
- }
- domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
- if err != nil {
- return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotFound")
- }
userID, err := c.idGenerator.Next()
if err != nil {
return nil, err
}
- return c.addMachineWithID(ctx, orgID, userID, machine, domainPolicy)
+ return c.addMachineWithID(ctx, orgID, userID, machine)
}
func (c *Commands) AddMachineWithID(ctx context.Context, orgID string, userID string, machine *domain.Machine) (*domain.Machine, error) {
- existingMachine, err := c.machineWriteModelByID(ctx, userID, orgID)
- if err != nil {
- return nil, err
- }
- if isUserStateExists(existingMachine.UserState) {
- return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2una", "Errors.User.AlreadyExisting")
- }
- domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID)
- if err != nil {
- return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotFound")
- }
- if !domainPolicy.UserLoginMustBeDomain {
- return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0dd", "Errors.User.Invalid")
- }
- return c.addMachineWithID(ctx, orgID, userID, machine, domainPolicy)
+ return c.addMachineWithID(ctx, orgID, userID, machine)
}
-func (c *Commands) addMachineWithID(ctx context.Context, orgID string, userID string, machine *domain.Machine, domainPolicy *domain.DomainPolicy) (*domain.Machine, error) {
+func (c *Commands) addMachineWithID(ctx context.Context, orgID string, userID string, machine *domain.Machine) (*domain.Machine, error) {
+ agg := user.NewAggregate(userID, orgID)
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddMachineCommand(agg, machine))
+ if err != nil {
+ return nil, err
+ }
- machine.AggregateID = userID
- addedMachine := NewMachineWriteModel(machine.AggregateID, orgID)
- userAgg := UserAggregateFromWriteModel(&addedMachine.WriteModel)
- events, err := c.eventstore.Push(ctx, user.NewMachineAddedEvent(
- ctx,
- userAgg,
- machine.Username,
- machine.Name,
- machine.Description,
- domainPolicy.UserLoginMustBeDomain,
- ))
+ events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
- err = AppendAndReduce(addedMachine, events...)
- if err != nil {
- return nil, err
- }
- return writeModelToMachine(addedMachine), nil
+
+ return &domain.Machine{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: agg.ID,
+ Sequence: events[len(events)-1].Sequence(),
+ CreationDate: events[len(events)-1].CreationDate(),
+ ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
+ InstanceID: events[len(events)-1].Aggregate().InstanceID,
+ },
+ Username: machine.Username,
+ Name: machine.Name,
+ Description: machine.Description,
+ State: machine.State,
+ }, nil
}
func (c *Commands) ChangeMachine(ctx context.Context, machine *domain.Machine) (*domain.Machine, error) {
- existingMachine, err := c.machineWriteModelByID(ctx, machine.AggregateID, machine.ResourceOwner)
+ agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner)
+ cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, changeMachineCommand(agg, machine))
if err != nil {
return nil, err
}
- if !isUserStateExists(existingMachine.UserState) {
- return nil, caos_errs.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound")
- }
- userAgg := UserAggregateFromWriteModel(&existingMachine.WriteModel)
- changedEvent, hasChanged, err := existingMachine.NewChangedEvent(ctx, userAgg, machine.Name, machine.Description)
+ events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
- if !hasChanged {
- return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged")
- }
- events, err := c.eventstore.Push(ctx, changedEvent)
- if err != nil {
- return nil, err
- }
- err = AppendAndReduce(existingMachine, events...)
- if err != nil {
- return nil, err
- }
- return writeModelToMachine(existingMachine), nil
+ return &domain.Machine{
+ ObjectRoot: models.ObjectRoot{
+ AggregateID: agg.ID,
+ Sequence: events[len(events)-1].Sequence(),
+ CreationDate: events[len(events)-1].CreationDate(),
+ ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
+ InstanceID: events[len(events)-1].Aggregate().InstanceID,
+ },
+ Username: machine.Username,
+ Name: machine.Name,
+ Description: machine.Description,
+ }, nil
}
-func (c *Commands) machineWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *MachineWriteModel, err error) {
- if userID == "" {
- return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Plof", "Errors.User.UserIDMissing")
- }
- ctx, span := tracing.NewSpan(ctx)
- defer func() { span.EndWithError(err) }()
+func changeMachineCommand(a *user.Aggregate, machine *domain.Machine) preparation.Validation {
+ return func() (_ preparation.CreateCommands, err error) {
+ if !machine.IsValid() {
+ 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.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound")
+ }
- writeModel = NewMachineWriteModel(userID, resourceOwner)
- err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
+ domainPolicy, err := domainPolicyWriteModel(ctx, filter)
+ if err != nil {
+ return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotFound")
+ }
+ if !domainPolicy.UserLoginMustBeDomain {
+ return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0dd", "Errors.User.Invalid")
+ }
+
+ changedEvent, hasChanged, err := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description)
+ if err != nil {
+ return nil, err
+ }
+ if !hasChanged {
+ return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged")
+ }
+
+ return []eventstore.Command{
+ changedEvent,
+ }, nil
+ }, nil
+ }
+}
+
+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
}
- return writeModel, nil
+ if len(events) == 0 {
+ return writeModel, nil
+ }
+ writeModel.AppendEvents(events...)
+ err = writeModel.Reduce()
+ return writeModel, err
}
diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go
index e08ce98e10..75a641ab93 100644
--- a/internal/command/user_machine_test.go
+++ b/internal/command/user_machine_test.go
@@ -43,6 +43,7 @@ func TestCommandSide_AddMachine(t *testing.T) {
eventstore: eventstoreExpect(
t,
),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "username"),
},
args: args{
ctx: context.Background(),
@@ -62,7 +63,9 @@ func TestCommandSide_AddMachine(t *testing.T) {
t,
expectFilter(),
expectFilter(),
+ expectFilter(),
),
+ idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "username"),
},
args: args{
ctx: context.Background(),
@@ -81,6 +84,7 @@ func TestCommandSide_AddMachine(t *testing.T) {
fields: fields{
eventstore: eventstoreExpect(
t,
+ expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
@@ -126,7 +130,7 @@ func TestCommandSide_AddMachine(t *testing.T) {
Username: "username",
Name: "name",
Description: "description",
- State: domain.UserStateActive,
+ State: domain.UserStateUnspecified,
},
},
},
@@ -171,7 +175,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,
@@ -188,6 +192,24 @@ 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(),
+ orgID: "org1",
+ machine: &domain.Machine{
+ Name: "name",
+ },
+ },
+ res: res{
+ err: caos_errs.IsErrorInvalidArgument,
+ },
+ },
{
name: "user not existing, precondition error",
fields: fields{
@@ -227,6 +249,16 @@ func TestCommandSide_ChangeMachine(t *testing.T) {
),
),
),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewDomainPolicyAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ true,
+ true,
+ true,
+ ),
+ ),
+ ),
),
},
args: args{
@@ -236,6 +268,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) {
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
},
+ Username: "username",
Name: "name",
Description: "description",
},
@@ -260,6 +293,16 @@ func TestCommandSide_ChangeMachine(t *testing.T) {
),
),
),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewDomainPolicyAddedEvent(context.Background(),
+ &user.NewAggregate("user1", "org1").Aggregate,
+ true,
+ true,
+ true,
+ ),
+ ),
+ ),
expectPush(
[]*repository.Event{
eventFromEventPusher(
@@ -274,8 +317,10 @@ func TestCommandSide_ChangeMachine(t *testing.T) {
orgID: "org1",
machine: &domain.Machine{
ObjectRoot: models.ObjectRoot{
- AggregateID: "user1",
+ AggregateID: "user1",
+ ResourceOwner: "org1",
},
+ Username: "username",
Description: "description1",
Name: "name1",
},
@@ -289,7 +334,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) {
Username: "username",
Name: "name1",
Description: "description1",
- State: domain.UserStateActive,
+ State: domain.UserStateUnspecified,
},
},
},
diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto
index 2d9e47a279..8693ca3712 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";
@@ -134,6 +135,17 @@ service SystemService {
};
}
+ 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) {
@@ -397,6 +409,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 = 5 [(validate.rules).string = {max_len: 10}];
+ }
+ message Email {
+ string email = 1[(validate.rules).string = {min_len: 1, max_len: 200}];
+ bool is_email_verified = 2;
+ }
+ message Password {
+ string password = 1 [(validate.rules).string = {max_len: 200}];
+ bool password_change_required = 2;
+ }
+ message Human {
+ Email email = 1 [(validate.rules).message.required = true];
+ Profile profile = 2 [(validate.rules).message.required = false];
+ Password password = 3 [(validate.rules).message.required = false];
+ string user_name = 4 [(validate.rules).string = {max_len: 200}];
+ }
+ 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 user {
+ option (validate.required) = true;
+
+ // oneof field for the user managing the instance
+ Human human = 4;
+ Machine machine = 5;
+ }
+
+ string default_language = 8 [(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 RemoveInstanceRequest {
string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
}