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