feat(instance): implement create instance with direct machine user and credentials

This commit is contained in:
Stefan Benz 2022-10-05 16:17:03 +02:00
parent 2bc19f55b5
commit f0f0cad231
No known key found for this signature in database
GPG Key ID: 9D2FE4EA50BEFE68
11 changed files with 577 additions and 101 deletions

View File

@ -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
}

View File

@ -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<br /> string.max_len: 200<br /> |
| first_org_name | string | - | string.max_len: 200<br /> |
| custom_domain | string | - | string.max_len: 200<br /> |
| [**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<br /> |
### CreateInstanceRequest.Email
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| email | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| is_email_verified | bool | - | |
### CreateInstanceRequest.Human
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| email | CreateInstanceRequest.Email | - | message.required: true<br /> |
| profile | CreateInstanceRequest.Profile | - | message.required: false<br /> |
| password | CreateInstanceRequest.Password | - | message.required: false<br /> |
| user_name | string | - | string.max_len: 200<br /> |
### CreateInstanceRequest.Machine
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| user_name | string | - | string.max_len: 200<br /> |
| name | string | - | string.max_len: 200<br /> |
| personal_access_token | CreateInstanceRequest.PersonalAccessToken | - | |
| machine_key | CreateInstanceRequest.MachineKey | - | |
### CreateInstanceRequest.MachineKey
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| type | zitadel.authn.v1.KeyType | - | enum.defined_only: true<br /> enum.not_in: [0]<br /> |
| expiration_date | google.protobuf.Timestamp | - | |
### CreateInstanceRequest.Password
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| password | string | - | string.max_len: 200<br /> |
| 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<br /> |
| last_name | string | - | string.max_len: 200<br /> |
| preferred_language | string | - | string.max_len: 10<br /> |
### CreateInstanceResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| instance_id | string | - | |
| details | zitadel.v1.ObjectDetails | - | |
| pat | string | - | |
| machine_key | bytes | - | |
### ExistsDomainRequest

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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,

View File

@ -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) {

View File

@ -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) {

View File

@ -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
}

View File

@ -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,
},
},
},

View File

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