diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index 8574121251..ebe4e80a66 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 +### UpdateInstance + +> **rpc** UpdateInstance([UpdateInstanceRequest](#updateinstancerequest)) +[UpdateInstanceResponse](#updateinstanceresponse) + +Updates name of an existing instance + + + + PUT: /instances/{instance_id} + + ### RemoveInstance > **rpc** RemoveInstance([RemoveInstanceRequest](#removeinstancerequest)) @@ -599,6 +611,29 @@ This is an empty response +### UpdateInstanceRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| instance_id | string | - | | +| instance_name | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### UpdateInstanceResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### View diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go index daeaf54cab..668d903f24 100644 --- a/internal/api/grpc/system/instance.go +++ b/internal/api/grpc/system/instance.go @@ -51,6 +51,17 @@ func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequ }, nil } +func (s *Server) UpdateInstance(ctx context.Context, req *system_pb.UpdateInstanceRequest) (*system_pb.UpdateInstanceResponse, error) { + ctx = authz.WithInstanceID(ctx, req.InstanceId) + details, err := s.command.UpdateInstance(ctx, req.InstanceName) + if err != nil { + return nil, err + } + return &system_pb.UpdateInstanceResponse{ + 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/command/instance.go b/internal/command/instance.go index f99e68d12b..c6761c0b0e 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -362,6 +362,24 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str }, nil } +func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) + validation := c.prepareUpdateInstance(instanceAgg, name) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) + if err != nil { + return nil, err + } + events, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, + }, nil +} + func (c *Commands) SetDefaultLanguage(ctx context.Context, defaultLanguage language.Tag) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) validation := c.prepareSetDefaultLanguage(instanceAgg, defaultLanguage) @@ -376,7 +394,7 @@ func (c *Commands) SetDefaultLanguage(ctx context.Context, defaultLanguage langu return &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), - ResourceOwner: events[len(events)-1].Aggregate().InstanceID, + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, }, nil } @@ -394,7 +412,7 @@ func (c *Commands) SetDefaultOrg(ctx context.Context, orgID string) (*domain.Obj return &domain.ObjectDetails{ Sequence: events[len(events)-1].Sequence(), EventDate: events[len(events)-1].CreationDate(), - ResourceOwner: events[len(events)-1].Aggregate().InstanceID, + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, }, nil } @@ -444,7 +462,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 +473,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) { @@ -498,6 +516,27 @@ func (c *Commands) setIAMProject(ctx context.Context, iamAgg *eventstore.Aggrega return instance.NewIAMProjectSetEvent(ctx, iamAgg, projectID), nil } +func (c *Commands) prepareUpdateInstance(a *instance.Aggregate, name string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if name == "" { + return nil, errors.ThrowInvalidArgument(nil, "INST-092mid", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := getInstanceWriteModel(ctx, filter) + if err != nil { + return nil, err + } + if writeModel.State == domain.InstanceStateUnspecified { + return nil, errors.ThrowNotFound(nil, "INST-nuso2m", "Errors.Instance.NotFound") + } + if writeModel.Name == name { + return nil, errors.ThrowPreconditionFailed(nil, "INST-alpxism", "Errors.Instance.NotChanged") + } + return []eventstore.Command{instance.NewInstanceChangedEvent(ctx, &a.Aggregate, name)}, nil + }, nil + } +} + func (c *Commands) prepareSetDefaultLanguage(a *instance.Aggregate, defaultLanguage language.Tag) preparation.Validation { return func() (preparation.CreateCommands, error) { if defaultLanguage == language.Und { diff --git a/internal/command/instance_model.go b/internal/command/instance_model.go index 1098a3b092..17437ec1a6 100644 --- a/internal/command/instance_model.go +++ b/internal/command/instance_model.go @@ -23,6 +23,7 @@ type InstanceWriteModel struct { func NewInstanceWriteModel(instanceID string) *InstanceWriteModel { return &InstanceWriteModel{ WriteModel: eventstore.WriteModel{ + InstanceID: instanceID, AggregateID: instanceID, ResourceOwner: instanceID, }, diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go new file mode 100644 index 0000000000..5aa88231d7 --- /dev/null +++ b/internal/command/instance_test.go @@ -0,0 +1,180 @@ +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +func TestCommandSide_ChangeInstance(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + name string + instanceID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "empty name, invalid error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + instanceID: "INSTANCE", + name: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "instance not existing, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + instanceID: "INSTANCE", + name: "INSTANCE_CHANGED", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + /* instance removed is not yet implemented + { + name: "generator removed, not found error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewInstanceAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "INSTANCE", + ), + ), + eventFromEventPusher( + instance.NewInstanceRemovedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "INSTANCE", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + name: "INSTANCE_CHANGED", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + },*/ + { + name: "no changes, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewInstanceAddedEvent( + context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "INSTANCE", + ), + ), + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + instanceID: "INSTANCE", + name: "INSTANCE", + }, + res: res{ + err: caos_errs.IsPreconditionFailed, + }, + }, + { + name: "instance change, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewInstanceAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "INSTANCE", + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewInstanceChangedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "INSTANCE_CHANGED", + ), + ), + }, + ), + ), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), + name: "INSTANCE_CHANGED", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := r.UpdateInstance(tt.args.ctx, tt.args.name) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/user_domain_policy_test.go b/internal/command/user_domain_policy_test.go index d8f7e0cd35..ed4a775729 100644 --- a/internal/command/user_domain_policy_test.go +++ b/internal/command/user_domain_policy_test.go @@ -137,6 +137,7 @@ func Test_defaultDomainPolicy(t *testing.T) { AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", Events: []eventstore.Event{}, + InstanceID: "INSTANCE", }, UserLoginMustBeDomain: true, ValidateOrgDomains: true, @@ -248,6 +249,7 @@ func Test_DomainPolicy(t *testing.T) { AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", Events: []eventstore.Event{}, + InstanceID: "INSTANCE", }, UserLoginMustBeDomain: true, ValidateOrgDomains: true, diff --git a/internal/command/user_password_complexity_policy_test.go b/internal/command/user_password_complexity_policy_test.go index 4dba815ebf..e4d79ab746 100644 --- a/internal/command/user_password_complexity_policy_test.go +++ b/internal/command/user_password_complexity_policy_test.go @@ -143,6 +143,7 @@ func Test_defaultPasswordComplexityPolicy(t *testing.T) { AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", Events: []eventstore.Event{}, + InstanceID: "INSTANCE", }, MinLength: 8, HasLowercase: true, @@ -262,6 +263,7 @@ func Test_passwordComplexityPolicy(t *testing.T) { AggregateID: "INSTANCE", ResourceOwner: "INSTANCE", Events: []eventstore.Event{}, + InstanceID: "INSTANCE", }, MinLength: 8, HasLowercase: true, diff --git a/internal/query/projection/instance.go b/internal/query/projection/instance.go index df384917f9..4212513ab6 100644 --- a/internal/query/projection/instance.go +++ b/internal/query/projection/instance.go @@ -62,6 +62,10 @@ func (p *instanceProjection) reducers() []handler.AggregateReducer { Event: instance.InstanceAddedEventType, Reduce: p.reduceInstanceAdded, }, + { + Event: instance.InstanceChangedEventType, + Reduce: p.reduceInstanceChanged, + }, { Event: instance.DefaultOrgSetEventType, Reduce: p.reduceDefaultOrgSet, @@ -100,6 +104,24 @@ func (p *instanceProjection) reduceInstanceAdded(event eventstore.Event) (*handl ), nil } +func (p *instanceProjection) reduceInstanceChanged(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*instance.InstanceChangedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-so2am1", "reduce.wrong.event.type %s", instance.InstanceChangedEventType) + } + return crdb.NewUpdateStatement( + e, + []handler.Column{ + handler.NewCol(InstanceColumnName, e.Name), + handler.NewCol(InstanceColumnChangeDate, e.CreationDate()), + handler.NewCol(InstanceColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(InstanceColumnID, e.Aggregate().InstanceID), + }, + ), nil +} + func (p *instanceProjection) reduceDefaultOrgSet(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*instance.DefaultOrgSetEvent) if !ok { diff --git a/internal/repository/instance/aggregate.go b/internal/repository/instance/aggregate.go index 4b7dcd3774..4c320dbded 100644 --- a/internal/repository/instance/aggregate.go +++ b/internal/repository/instance/aggregate.go @@ -20,6 +20,7 @@ type Aggregate struct { func NewAggregate(instanceID string) *Aggregate { return &Aggregate{ Aggregate: eventstore.Aggregate{ + InstanceID: instanceID, Type: AggregateType, Version: AggregateVersion, ID: instanceID, diff --git a/internal/repository/instance/instance.go b/internal/repository/instance/instance.go index 8e11b02150..2e24684c59 100644 --- a/internal/repository/instance/instance.go +++ b/internal/repository/instance/instance.go @@ -66,7 +66,7 @@ func (e *InstanceChangedEvent) UniqueConstraints() []*eventstore.EventUniqueCons return nil } -func NewInstanceChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, oldName, newName string) *InstanceChangedEvent { +func NewInstanceChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, newName string) *InstanceChangedEvent { return &InstanceChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 09a812a784..1c6aa15cc6 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -142,6 +142,10 @@ Errors: RefreshToken: Invalid: Refresh Token ist ungültig NotFound: Refresh Token nicht gefunden + Instance: + NotFound: Instanz konnte nicht gefunden werden + AlreadyExists: Instanz exisitiert bereits + NotChanged: Instanz wurde nicht verändert Org: AlreadyExists: Organisationsname existiert bereits Invalid: Organisation ist ungültig diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index bca370719e..bcd225db78 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -142,6 +142,10 @@ Errors: RefreshToken: Invalid: Refresh Token is invalid NotFound: Refresh Token not found + Instance: + NotFound: Instance not found + AlreadyExists: Instance already exists + NotChanged: Instance not changed Org: AlreadyExists: Organisation's name already taken Invalid: Organisation is invalid diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index cdb0a61d03..aba621bea4 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -142,6 +142,10 @@ Errors: RefreshToken: Invalid: Le jeton de rafraîchissement n'est pas valide NotFound: Jeton de rafraîchissement non trouvé + Instance: + NotFound: Instance non trouvée + AlreadyExists: L'instance existe déjà + NotChanged: L'instance n'a pas changé Org: AlreadyExists: Le nom de l'organisation est déjà pris Invalid: L'organisation n'est pas valide diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index cfbddcd88c..09d010862b 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -142,6 +142,10 @@ Errors: RefreshToken: Invalid: Refresh Token non è valido NotFound: Refresh Token non trovato + Instance: + NotFound: Istanza non trovata + AlreadyExists: L'istanza esiste già + NotChanged: Istanza non modificata Org: AlreadyExists: Nome dell'organizzazione già preso Invalid: L'organizzazione non è valida diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 9d145da32e..3288e1cec1 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -142,6 +142,10 @@ Errors: RefreshToken: Invalid: Refresh Token 无效 NotFound: 未找到 Refresh Token + Instance: + NotFound: 没有找到实例 + AlreadyExists: 实例已经存在 + NotChanged: 实例没有改变 Org: AlreadyExists: 组织名称已被占用 Invalid: 组织无效 diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 2d9e47a279..f9b7582a69 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -134,6 +134,18 @@ service SystemService { }; } + // Updates name of an existing instance + rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) { + option (google.api.http) = { + put: "/instances/{instance_id}" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + // Removes a instances // This might take some time rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { @@ -397,6 +409,15 @@ message AddInstanceResponse { zitadel.v1.ObjectDetails details = 2; } +message UpdateInstanceRequest{ + string instance_id = 1; + string instance_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message UpdateInstanceResponse{ + zitadel.v1.ObjectDetails details = 1; +} + message RemoveInstanceRequest { string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; }