From c25d853820a893024c6e52bedb050623962fd529 Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Thu, 14 Apr 2022 14:19:18 +0200 Subject: [PATCH] feat: Instance domains (#3444) * feat: add domain list * feat: domain tests * feat: add redirect url on adding instance domain * Update internal/command/instance_domain.go Co-authored-by: Livio Amstutz * feat: remove unused code * fix Co-authored-by: Livio Amstutz --- docs/docs/apis/proto/admin.md | 38 ++++ docs/docs/apis/proto/instance.md | 77 +++++++- docs/docs/apis/proto/system.md | 177 +++++++++++------- internal/api/authz/instance.go | 5 + internal/api/authz/instance_test.go | 4 + internal/api/grpc/admin/instance.go | 29 +++ internal/api/grpc/admin/instance_converter.go | 25 +++ internal/api/grpc/instance/converter.go | 54 ++++++ .../middleware/instance_interceptor_test.go | 4 + .../middleware/instance_interceptor_test.go | 4 + internal/command/instance.go | 6 +- internal/command/instance_domain.go | 60 +++++- internal/command/instance_domain_test.go | 165 +++++++++++++++- internal/command/main_test.go | 22 +++ internal/command/project_application_oidc.go | 2 +- internal/domain/instance_domain.go | 4 + internal/query/instance.go | 11 ++ internal/query/instance_domain.go | 15 +- internal/query/instance_domain_test.go | 12 ++ internal/query/instance_test.go | 6 + internal/query/projection/instance.go | 2 + internal/query/projection/instance_domain.go | 39 ++++ .../query/projection/instance_domain_test.go | 3 +- internal/repository/instance/domain.go | 46 ++++- .../instance/event_iam_project_set.go | 5 +- internal/repository/instance/eventstore.go | 1 + proto/zitadel/admin.proto | 22 +++ proto/zitadel/instance.proto | 99 ++++++---- proto/zitadel/system.proto | 66 ++++--- 29 files changed, 858 insertions(+), 145 deletions(-) create mode 100644 internal/api/grpc/admin/instance.go create mode 100644 internal/api/grpc/admin/instance_converter.go create mode 100644 internal/api/grpc/instance/converter.go diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 6b0640a027..89d07061b4 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -56,6 +56,18 @@ Set the default language GET: /languages/default +### ListInstanceDomains + +> **rpc** ListInstanceDomains([ListInstanceDomainsRequest](#listinstancedomainsrequest)) +[ListInstanceDomainsResponse](#listinstancedomainsresponse) + +Returns the domains of an instance + + + + GET: /domains + + ### ListSecretGenerators > **rpc** ListSecretGenerators([ListSecretGeneratorsRequest](#listsecretgeneratorsrequest)) @@ -2629,6 +2641,32 @@ This is an empty request +### ListInstanceDomainsRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| query | zitadel.v1.ListQuery | - | | +| sorting_column | zitadel.instance.v1.DomainFieldName | the field the result is sorted | | +| queries | repeated zitadel.instance.v1.DomainSearchQuery | criterias the client is looking for | | + + + + +### ListInstanceDomainsResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ListDetails | - | | +| sorting_column | zitadel.instance.v1.DomainFieldName | - | | +| result | repeated zitadel.instance.v1.Domain | - | | + + + + ### ListLoginPolicyIDPsRequest diff --git a/docs/docs/apis/proto/instance.md b/docs/docs/apis/proto/instance.md index 69d7cda721..dacfab4921 100644 --- a/docs/docs/apis/proto/instance.md +++ b/docs/docs/apis/proto/instance.md @@ -9,14 +9,63 @@ title: zitadel/instance.proto ## Messages -### DomainsQuery +### Domain | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| domains | repeated string | - | string.max_len: 200
| -| method | zitadel.v1.ListQueryMethod | - | enum.defined_only: true
| +| details | zitadel.v1.ObjectDetails | - | | +| domain | string | - | | +| primary | bool | - | | +| generated | bool | - | | + + + + +### DomainGeneratedQuery +DomainGeneratedQuery is always equals + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| generated | bool | - | | + + + + +### DomainPrimaryQuery +DomainPrimaryQuery is always equals + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| primary | bool | - | | + + + + +### DomainQuery + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| domain | string | - | string.max_len: 200
| +| method | zitadel.v1.TextQueryMethod | - | enum.defined_only: true
| + + + + +### DomainSearchQuery + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.domain_query | DomainQuery | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.generated_query | DomainGeneratedQuery | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.primary_query | DomainPrimaryQuery | - | | @@ -41,8 +90,6 @@ IdQuery is always equals | id | string | - | | | details | zitadel.v1.ObjectDetails | - | | | state | State | - | | -| generated_domain | string | - | | -| custom_domains | repeated string | - | | | name | string | - | | | version | string | - | | @@ -56,7 +103,6 @@ IdQuery is always equals | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | | [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.id_query | IdQuery | - | | -| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.domains_query | DomainsQuery | - | | | [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.state_query | StateQuery | - | | @@ -78,6 +124,20 @@ StateQuery is always equals ## Enums +### DomainFieldName {#domainfieldname} + + +| Name | Number | Description | +| ---- | ------ | ----------- | +| DOMAIN_FIELD_NAME_UNSPECIFIED | 0 | - | +| DOMAIN_FIELD_NAME_DOMAIN | 1 | - | +| DOMAIN_FIELD_NAME_PRIMARY | 2 | - | +| DOMAIN_FIELD_NAME_GENERATED | 3 | - | +| DOMAIN_FIELD_NAME_CREATION_DATE | 4 | - | + + + + ### FieldName {#fieldname} @@ -85,9 +145,8 @@ StateQuery is always equals | ---- | ------ | ----------- | | FIELD_NAME_UNSPECIFIED | 0 | - | | FIELD_NAME_ID | 1 | - | -| FIELD_NAME_GENERATED_DOMAIN | 2 | - | -| FIELD_NAME_NAME | 3 | - | -| FIELD_NAME_CREATION_DATE | 4 | - | +| FIELD_NAME_NAME | 2 | - | +| FIELD_NAME_CREATION_DATE | 3 | - | diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index 1b601750ef..aae6b40457 100644 --- a/docs/docs/apis/proto/system.md +++ b/docs/docs/apis/proto/system.md @@ -82,40 +82,52 @@ Returns the usage metrics of an instance GET: /instances/{id}/usage -### GetGeneratedDomain +### ListDomains -> **rpc** GetGeneratedDomain([GetGeneratedDomainRequest](#getgenerateddomainrequest)) -[GetGeneratedDomainResponse](#getgenerateddomainresponse) - -Returns the domain of an instance - - - - GET: /instances/{id}/domains/generated - - -### GetCustomDomains - -> **rpc** GetCustomDomains([GetCustomDomainsRequest](#getcustomdomainsrequest)) -[GetCustomDomainsResponse](#getcustomdomainsresponse) +> **rpc** ListDomains([ListDomainsRequest](#listdomainsrequest)) +[ListDomainsResponse](#listdomainsresponse) Returns the custom domains of an instance - GET: /instances/{id}/domains/custom + GET: /instances/{id}/domains -### AddCustomDomain +### AddDomain -> **rpc** AddCustomDomain([AddCustomDomainRequest](#addcustomdomainrequest)) -[AddCustomDomainResponse](#addcustomdomainresponse) +> **rpc** AddDomain([AddDomainRequest](#adddomainrequest)) +[AddDomainResponse](#adddomainresponse) Returns the domain of an instance - POST: /instances/{id}/domains/custom + POST: /instances/{id}/domains + + +### RemoveDomain + +> **rpc** RemoveDomain([RemoveDomainRequest](#removedomainrequest)) +[RemoveDomainResponse](#removedomainresponse) + +Returns the domain of an instance + + + + DELETE: /instances/{id}/domains/{domain} + + +### SetPrimaryDomain + +> **rpc** SetPrimaryDomain([SetPrimaryDomainRequest](#setprimarydomainrequest)) +[SetPrimaryDomainResponse](#setprimarydomainresponse) + +Returns the domain of an instance + + + + POST: /instances/{id}/domains/_set_primary ### ListViews @@ -185,19 +197,19 @@ failed event. You can find out if it worked on the `failure_count` ## Messages -### AddCustomDomainRequest +### AddDomainRequest | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | | id | string | - | string.min_len: 1
string.max_len: 200
| -| custom_domain | string | - | string.min_len: 1
string.max_len: 200
| +| domain | string | - | string.min_len: 1
string.max_len: 200
| -### AddCustomDomainResponse +### AddDomainResponse @@ -297,52 +309,6 @@ This is an empty response -### GetCustomDomainsRequest - - - -| Field | Type | Description | Validation | -| ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| - - - - -### GetCustomDomainsResponse - - - -| Field | Type | Description | Validation | -| ----- | ---- | ----------- | ----------- | -| details | zitadel.v1.ObjectDetails | - | | -| domains | repeated string | - | | - - - - -### GetGeneratedDomainRequest - - - -| Field | Type | Description | Validation | -| ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| - - - - -### GetGeneratedDomainResponse - - - -| Field | Type | Description | Validation | -| ----- | ---- | ----------- | ----------- | -| details | zitadel.v1.ObjectDetails | - | | -| domain | string | - | | - - - - ### GetInstanceRequest @@ -401,6 +367,33 @@ This is an empty response +### ListDomainsRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | list limitations and ordering | string.min_len: 1
string.max_len: 200
| +| query | zitadel.v1.ListQuery | - | | +| sorting_column | zitadel.instance.v1.DomainFieldName | the field the result is sorted | | +| queries | repeated zitadel.instance.v1.DomainSearchQuery | criterias the client is looking for | | + + + + +### ListDomainsResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ListDetails | - | | +| sorting_column | zitadel.instance.v1.DomainFieldName | - | | +| result | repeated zitadel.instance.v1.Domain | - | | + + + + ### ListFailedEventsRequest This is an empty request @@ -461,6 +454,29 @@ This is an empty request +### RemoveDomainRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | string.min_len: 1
string.max_len: 200
| +| domain | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### RemoveDomainResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### RemoveFailedEventRequest @@ -502,6 +518,29 @@ This is an empty response +### SetPrimaryDomainRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | string.min_len: 1
string.max_len: 200
| +| domain | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### SetPrimaryDomainResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| details | zitadel.v1.ObjectDetails | - | | + + + + ### View diff --git a/internal/api/authz/instance.go b/internal/api/authz/instance.go index 6501793bb6..6f55c20438 100644 --- a/internal/api/authz/instance.go +++ b/internal/api/authz/instance.go @@ -12,6 +12,7 @@ type Instance interface { InstanceID() string ProjectID() string ConsoleClientID() string + ConsoleApplicationID() string RequestedDomain() string } @@ -36,6 +37,10 @@ func (i *instance) ConsoleClientID() string { return "" } +func (i *instance) ConsoleApplicationID() string { + return "" +} + func (i *instance) RequestedDomain() string { return i.Domain } diff --git a/internal/api/authz/instance_test.go b/internal/api/authz/instance_test.go index c14f5fdf3a..db714cf118 100644 --- a/internal/api/authz/instance_test.go +++ b/internal/api/authz/instance_test.go @@ -79,6 +79,10 @@ func (m *mockInstance) ConsoleClientID() string { return "consoleID" } +func (m *mockInstance) ConsoleApplicationID() string { + return "appID" +} + func (m *mockInstance) RequestedDomain() string { return "zitadel.cloud" } diff --git a/internal/api/grpc/admin/instance.go b/internal/api/grpc/admin/instance.go new file mode 100644 index 0000000000..6778c3f034 --- /dev/null +++ b/internal/api/grpc/admin/instance.go @@ -0,0 +1,29 @@ +package admin + +import ( + "context" + + instance_grpc "github.com/caos/zitadel/internal/api/grpc/instance" + "github.com/caos/zitadel/internal/api/grpc/object" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" +) + +func (s *Server) GetInstanceDomains(ctx context.Context, req *admin_pb.ListInstanceDomainsRequest) (*admin_pb.ListInstanceDomainsResponse, error) { + queries, err := ListInstanceDomainsRequestToModel(req) + if err != nil { + return nil, err + } + + domains, err := s.query.SearchInstanceDomains(ctx, queries) + if err != nil { + return nil, err + } + return &admin_pb.ListInstanceDomainsResponse{ + Result: instance_grpc.DomainsToPb(domains.Domains), + Details: object.ToListDetails( + domains.Count, + domains.Sequence, + domains.Timestamp, + ), + }, nil +} diff --git a/internal/api/grpc/admin/instance_converter.go b/internal/api/grpc/admin/instance_converter.go new file mode 100644 index 0000000000..b83cbb90be --- /dev/null +++ b/internal/api/grpc/admin/instance_converter.go @@ -0,0 +1,25 @@ +package admin + +import ( + instance_grpc "github.com/caos/zitadel/internal/api/grpc/instance" + "github.com/caos/zitadel/internal/api/grpc/object" + "github.com/caos/zitadel/internal/query" + admin_pb "github.com/caos/zitadel/pkg/grpc/admin" +) + +func ListInstanceDomainsRequestToModel(req *admin_pb.ListInstanceDomainsRequest) (*query.InstanceDomainSearchQueries, error) { + offset, limit, asc := object.ListQueryToModel(req.Query) + queries, err := instance_grpc.DomainQueriesToModel(req.Queries) + if err != nil { + return nil, err + } + return &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + //SortingColumn: //TODO: sorting + Queries: queries, + }, nil +} diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go new file mode 100644 index 0000000000..f944a1f18e --- /dev/null +++ b/internal/api/grpc/instance/converter.go @@ -0,0 +1,54 @@ +package org + +import ( + "github.com/caos/zitadel/internal/api/grpc/object" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/query" + instance_pb "github.com/caos/zitadel/pkg/grpc/instance" +) + +func DomainQueriesToModel(queries []*instance_pb.DomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = DomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func DomainQueryToModel(searchQuery *instance_pb.DomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.Query.(type) { + case *instance_pb.DomainSearchQuery_DomainQuery: + return query.NewInstanceDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.Method), q.DomainQuery.Domain) + case *instance_pb.DomainSearchQuery_GeneratedQuery: + return query.NewInstanceDomainGeneratedSearchQuery(q.GeneratedQuery.Generated) + case *instance_pb.DomainSearchQuery_PrimaryQuery: + return query.NewInstanceDomainPrimarySearchQuery(q.PrimaryQuery.Primary) + default: + return nil, errors.ThrowInvalidArgument(nil, "ORG-Ags42", "List.Query.Invalid") + } +} + +func DomainsToPb(domains []*query.InstanceDomain) []*instance_pb.Domain { + d := make([]*instance_pb.Domain, len(domains)) + for i, domain := range domains { + d[i] = DomainToPb(domain) + } + return d +} + +func DomainToPb(d *query.InstanceDomain) *instance_pb.Domain { + return &instance_pb.Domain{ + Domain: d.Domain, + Primary: d.IsPrimary, + Generated: d.IsGenerated, + Details: object.ToViewDetailsPb( + d.Sequence, + d.CreationDate, + d.ChangeDate, + d.InstanceID, + ), + } +} diff --git a/internal/api/grpc/server/middleware/instance_interceptor_test.go b/internal/api/grpc/server/middleware/instance_interceptor_test.go index dab78be056..8bfc274cc6 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor_test.go +++ b/internal/api/grpc/server/middleware/instance_interceptor_test.go @@ -173,6 +173,10 @@ func (m *mockInstance) ConsoleClientID() string { return "consoleClientID" } +func (m *mockInstance) ConsoleApplicationID() string { + return "consoleApplicationID" +} + func (m *mockInstance) RequestedDomain() string { return "localhost" } diff --git a/internal/api/http/middleware/instance_interceptor_test.go b/internal/api/http/middleware/instance_interceptor_test.go index 625d3c748c..10052fd2c7 100644 --- a/internal/api/http/middleware/instance_interceptor_test.go +++ b/internal/api/http/middleware/instance_interceptor_test.go @@ -257,6 +257,10 @@ func (m *mockInstance) ConsoleClientID() string { return "consoleClientID" } +func (m *mockInstance) ConsoleApplicationID() string { + return "consoleApplicationID" +} + func (m *mockInstance) RequestedDomain() string { return "zitadel.cloud" } diff --git a/internal/command/instance.go b/internal/command/instance.go index 1dfc721180..e0c68b39b3 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -356,7 +356,7 @@ func (c *commandNew) SetUpInstance(ctx context.Context, setup *InstanceSetup) (* ), AddOIDCAppCommand(console, nil), - SetIAMConsoleID(instanceAgg, &console.ClientID), + SetIAMConsoleID(instanceAgg, &console.ClientID, &setup.Zitadel.consoleAppID), ) cmds, err := preparation.PrepareCommands(ctx, c.es.Filter, validations...) @@ -387,11 +387,11 @@ func SetIAMProject(a *instance.Aggregate, projectID string) preparation.Validati } //SetIAMConsoleID defines the command to set the clientID of the Console App onto the instance -func SetIAMConsoleID(a *instance.Aggregate, clientID *string) preparation.Validation { +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) { return []eventstore.Command{ - instance.NewIAMConsoleSetEvent(ctx, &a.Aggregate, clientID), + instance.NewIAMConsoleSetEvent(ctx, &a.Aggregate, clientID, appID), }, nil }, nil } diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go index c0db780047..757df7abb7 100644 --- a/internal/command/instance_domain.go +++ b/internal/command/instance_domain.go @@ -10,6 +10,7 @@ import ( "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/repository/instance" + "github.com/caos/zitadel/internal/repository/project" ) func (c *Commands) AddInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) { @@ -30,6 +31,24 @@ func (c *Commands) AddInstanceDomain(ctx context.Context, instanceDomain string) }, nil } +func (c *Commands) SetPrimaryInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) + validation := c.setPrimaryInstanceDomain(instanceAgg, instanceDomain) + 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().InstanceID, + }, nil +} + func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) validation := c.removeInstanceDomain(instanceAgg, instanceDomain) @@ -61,7 +80,46 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin if domainWriteModel.State == domain.InstanceDomainStateActive { return nil, errors.ThrowAlreadyExists(nil, "INST-i2nl", "Errors.Instance.Domain.AlreadyExists") } - return []eventstore.Command{instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated)}, nil + appWriteModel, err := c.getOIDCAppWriteModel(ctx, authz.GetInstance(ctx).ProjectID(), authz.GetInstance(ctx).ConsoleApplicationID(), "") + if err != nil { + return nil, err + } + redirectUrls := append(appWriteModel.RedirectUris, instanceDomain+consoleRedirectPath) + logoutUrls := append(appWriteModel.PostLogoutRedirectUris, instanceDomain+consolePostLogoutPath) + consoleChangeEvent, err := project.NewOIDCConfigChangedEvent( + ctx, + ProjectAggregateFromWriteModel(&appWriteModel.WriteModel), + appWriteModel.AppID, + []project.OIDCConfigChanges{ + project.ChangeRedirectURIs(redirectUrls), + project.ChangePostLogoutRedirectURIs(logoutUrls), + }, + ) + if err != nil { + return nil, err + } + return []eventstore.Command{ + instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated), + consoleChangeEvent, + }, nil + }, nil + } +} + +func (c *Commands) setPrimaryInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" { + return nil, errors.ThrowInvalidArgument(nil, "INST-9mWjf", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain) + if err != nil { + return nil, err + } + if !domainWriteModel.State.Exists() { + return nil, errors.ThrowNotFound(nil, "INSTANCE-9nkWf", "Errors.Instance.Domain.NotFound") + } + return []eventstore.Command{instance.NewDomainPrimarySetEvent(ctx, &a.Aggregate, instanceDomain)}, nil }, nil } } diff --git a/internal/command/instance_domain_test.go b/internal/command/instance_domain_test.go index 075e0dcc85..19aff8ebda 100644 --- a/internal/command/instance_domain_test.go +++ b/internal/command/instance_domain_test.go @@ -3,8 +3,11 @@ package command import ( "context" "testing" + "time" "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/repository/project" "github.com/stretchr/testify/assert" "github.com/caos/zitadel/internal/domain" @@ -77,6 +80,43 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) { eventstore: eventstoreExpect( t, expectFilter(), + expectFilter( + eventFromEventPusherWithInstanceID( + "INSTANCE", + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "consoleApplicationID", + "app", + ), + ), + eventFromEventPusherWithInstanceID( + "INSTANCE", + project.NewOIDCConfigAddedEvent(context.Background(), + &project.NewAggregate("projectID", "org1").Aggregate, + domain.OIDCVersionV1, + "consoleApplicationID", + "client1@project", + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + []string{"https://test.ch"}, + []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + domain.OIDCApplicationTypeWeb, + domain.OIDCAuthMethodTypePost, + []string{"https://test.ch/logout"}, + true, + domain.OIDCTokenTypeBearer, + true, + true, + true, + time.Second*1, + []string{"https://sub.test.ch"}), + ), + ), expectPush( []*repository.Event{ eventFromEventPusherWithInstanceID( @@ -86,11 +126,121 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) { "domain.ch", false, )), + eventFromEventPusherWithInstanceID( + "INSTANCE", + newOIDCAppChangedEventInstanceDomain(context.Background(), "consoleApplicationID", "projectID", "org1"), + ), }, uniqueConstraintsFromEventConstraintWithInstanceID("INSTANCE", instance.NewAddInstanceDomainUniqueConstraint("domain.ch")), ), ), }, + args: args{ + ctx: authz.WithInstance(context.Background(), new(mockInstance)), + domain: "domain.ch", + }, + 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.AddInstanceDomain(tt.args.ctx, tt.args.domain) + 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) + } + }) + } +} + +func TestCommandSide_SetPrimaryInstanceDomain(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + domain string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid domain, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + domain: "", + }, + res: res{ + err: caos_errs.IsErrorInvalidArgument, + }, + }, + { + name: "domain not exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + ), + }, + args: args{ + ctx: context.Background(), + domain: "domain.ch", + }, + res: res{ + err: caos_errs.IsNotFound, + }, + }, + { + name: "set primary domain, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewDomainAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "domain.ch", + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID( + "INSTANCE", + instance.NewDomainPrimarySetEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "domain.ch", + )), + }, + ), + ), + }, args: args{ ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), domain: "domain.ch", @@ -107,7 +257,7 @@ func TestCommandSide_AddInstanceDomain(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } - got, err := r.AddInstanceDomain(tt.args.ctx, tt.args.domain) + got, err := r.SetPrimaryInstanceDomain(tt.args.ctx, tt.args.domain) if tt.res.err == nil { assert.NoError(t, err) } @@ -251,3 +401,16 @@ func TestCommandSide_RemoveInstanceDomain(t *testing.T) { }) } } + +func newOIDCAppChangedEventInstanceDomain(ctx context.Context, appID, projectID, resourceOwner string) *project.OIDCConfigChangedEvent { + changes := []project.OIDCConfigChanges{ + project.ChangeRedirectURIs([]string{"https://test.ch", "domain.ch/ui/console/auth/callback"}), + project.ChangePostLogoutRedirectURIs([]string{"https://test.ch/logout", "domain.ch/ui/console/signedout"}), + } + event, _ := project.NewOIDCConfigChangedEvent(ctx, + &project.NewAggregate(projectID, resourceOwner).Aggregate, + appID, + changes, + ) + return event +} diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 4cae45781b..17f0fc9950 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -256,3 +256,25 @@ func (v *testVerifier) CheckOrgFeatures(ctx context.Context, orgID string, requi } return nil } + +type mockInstance struct{} + +func (m *mockInstance) InstanceID() string { + return "INSTANCE" +} + +func (m *mockInstance) ProjectID() string { + return "projectID" +} + +func (m *mockInstance) ConsoleClientID() string { + return "consoleID" +} + +func (m *mockInstance) ConsoleApplicationID() string { + return "consoleApplicationID" +} + +func (m *mockInstance) RequestedDomain() string { + return "zitadel.cloud" +} diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index 2a826ece66..6d987fb6c3 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -309,7 +309,7 @@ func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID, return err } _, err = c.eventstore.Push(ctx, project_repo.NewOIDCConfigSecretCheckFailedEvent(ctx, projectAgg, app.AppID)) - logging.Log("COMMAND-ADfhz").OnError(err).Error("could not push event OIDCClientSecretCheckFailed") + logging.New().OnError(err).Error("could not push event OIDCClientSecretCheckFailed") return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid") } diff --git a/internal/domain/instance_domain.go b/internal/domain/instance_domain.go index 5e3a7c1dde..be1284ed6a 100644 --- a/internal/domain/instance_domain.go +++ b/internal/domain/instance_domain.go @@ -18,6 +18,10 @@ func (f InstanceDomainState) Valid() bool { return f >= 0 && f < instanceDomainStateCount } +func (f InstanceDomainState) Exists() bool { + return f == InstanceDomainStateActive +} + func NewGeneratedInstanceDomain(instanceName, iamDomain string) string { return strings.ToLower(strings.ReplaceAll(instanceName, " ", "-") + "." + iamDomain) } diff --git a/internal/query/instance.go b/internal/query/instance.go index 7e911da8e6..c0356c2170 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -43,6 +43,10 @@ var ( name: projection.InstanceColumnConsoleID, table: instanceTable, } + InstanceColumnConsoleAppID = Column{ + name: projection.InstanceColumnConsoleAppID, + table: instanceTable, + } InstanceColumnSetupStarted = Column{ name: projection.InstanceColumnSetUpStarted, table: instanceTable, @@ -65,6 +69,7 @@ type Instance struct { GlobalOrgID string IAMProjectID string ConsoleID string + ConsoleAppID string DefaultLanguage language.Tag SetupStarted domain.Step SetupDone domain.Step @@ -83,6 +88,10 @@ func (i *Instance) ConsoleClientID() string { return i.ConsoleID } +func (i *Instance) ConsoleApplicationID() string { + return i.ConsoleAppID +} + func (i *Instance) RequestedDomain() string { return i.Host } @@ -142,6 +151,7 @@ func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Insta InstanceColumnGlobalOrgID.identifier(), InstanceColumnProjectID.identifier(), InstanceColumnConsoleID.identifier(), + InstanceColumnConsoleAppID.identifier(), InstanceColumnSetupStarted.identifier(), InstanceColumnSetupDone.identifier(), InstanceColumnDefaultLanguage.identifier(), @@ -157,6 +167,7 @@ func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Insta &instance.GlobalOrgID, &instance.IAMProjectID, &instance.ConsoleID, + &instance.ConsoleAppID, &instance.SetupStarted, &instance.SetupDone, &lang, diff --git a/internal/query/instance_domain.go b/internal/query/instance_domain.go index 39c3d0dd6b..f7e92b166b 100644 --- a/internal/query/instance_domain.go +++ b/internal/query/instance_domain.go @@ -19,6 +19,7 @@ type InstanceDomain struct { Domain string InstanceID string IsGenerated bool + IsPrimary bool } type InstanceDomains struct { @@ -47,8 +48,12 @@ func NewInstanceDomainInstanceIDSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(InstanceDomainInstanceIDCol, value, TextEquals) } -func NewInstanceDomainGeneratedSearchQuery(verified bool) (SearchQuery, error) { - return NewBoolQuery(InstanceDomainIsGeneratedCol, verified) +func NewInstanceDomainGeneratedSearchQuery(generated bool) (SearchQuery, error) { + return NewBoolQuery(InstanceDomainIsGeneratedCol, generated) +} + +func NewInstanceDomainPrimarySearchQuery(primary bool) (SearchQuery, error) { + return NewBoolQuery(InstanceDomainIsPrimaryCol, primary) } func (q *Queries) SearchInstanceDomains(ctx context.Context, queries *InstanceDomainSearchQueries) (domains *InstanceDomains, err error) { @@ -81,6 +86,7 @@ func prepareInstanceDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instance InstanceDomainDomainCol.identifier(), InstanceDomainInstanceIDCol.identifier(), InstanceDomainIsGeneratedCol.identifier(), + InstanceDomainIsPrimaryCol.identifier(), countColumn.identifier(), ).From(instanceDomainsTable.identifier()).PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*InstanceDomains, error) { @@ -95,6 +101,7 @@ func prepareInstanceDomainsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instance &domain.Domain, &domain.InstanceID, &domain.IsGenerated, + &domain.IsPrimary, &count, ) if err != nil { @@ -145,4 +152,8 @@ var ( name: projection.InstanceDomainIsGeneratedCol, table: instanceDomainsTable, } + InstanceDomainIsPrimaryCol = Column{ + name: projection.InstanceDomainIsPrimaryCol, + table: instanceDomainsTable, + } ) diff --git a/internal/query/instance_domain_test.go b/internal/query/instance_domain_test.go index b8445701bc..fd2acd6f10 100644 --- a/internal/query/instance_domain_test.go +++ b/internal/query/instance_domain_test.go @@ -31,6 +31,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { ` projections.instance_domains.domain,`+ ` projections.instance_domains.instance_id,`+ ` projections.instance_domains.is_generated,`+ + ` projections.instance_domains.is_primary,`+ ` COUNT(*) OVER ()`+ ` FROM projections.instance_domains`), nil, @@ -50,6 +51,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { ` projections.instance_domains.domain,`+ ` projections.instance_domains.instance_id,`+ ` projections.instance_domains.is_generated,`+ + ` projections.instance_domains.is_primary,`+ ` COUNT(*) OVER ()`+ ` FROM projections.instance_domains`), []string{ @@ -59,6 +61,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { "domain", "instance_id", "is_generated", + "is_primary", "count", }, [][]driver.Value{ @@ -69,6 +72,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { "zitadel.ch", "inst-id", true, + true, }, }, ), @@ -85,6 +89,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { Domain: "zitadel.ch", InstanceID: "inst-id", IsGenerated: true, + IsPrimary: true, }, }, }, @@ -100,6 +105,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { ` projections.instance_domains.domain,`+ ` projections.instance_domains.instance_id,`+ ` projections.instance_domains.is_generated,`+ + ` projections.instance_domains.is_primary,`+ ` COUNT(*) OVER ()`+ ` FROM projections.instance_domains`), []string{ @@ -109,6 +115,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { "domain", "instance_id", "is_generated", + "is_primary", "count", }, [][]driver.Value{ @@ -119,6 +126,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { "zitadel.ch", "inst-id", true, + true, }, { testNow, @@ -127,6 +135,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { "zitadel.com", "inst-id", false, + false, }, }, ), @@ -143,6 +152,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { Domain: "zitadel.ch", InstanceID: "inst-id", IsGenerated: true, + IsPrimary: true, }, { CreationDate: testNow, @@ -151,6 +161,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { Domain: "zitadel.com", InstanceID: "inst-id", IsGenerated: false, + IsPrimary: false, }, }, }, @@ -166,6 +177,7 @@ func Test_InstanceDomainPrepares(t *testing.T) { ` projections.instance_domains.domain,`+ ` projections.instance_domains.instance_id,`+ ` projections.instance_domains.is_generated,`+ + ` projections.instance_domains.is_primary,`+ ` COUNT(*) OVER ()`+ ` FROM projections.instance_domains`), sql.ErrConnDone, diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index b7e653a74c..90561d88cd 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -39,6 +39,7 @@ func Test_InstancePrepares(t *testing.T) { ` projections.instances.global_org_id,`+ ` projections.instances.iam_project_id,`+ ` projections.instances.console_client_id,`+ + ` projections.instances.console_app_id,`+ ` projections.instances.setup_started,`+ ` projections.instances.setup_done,`+ ` projections.instances.default_language`+ @@ -68,6 +69,7 @@ func Test_InstancePrepares(t *testing.T) { ` projections.instances.global_org_id,`+ ` projections.instances.iam_project_id,`+ ` projections.instances.console_client_id,`+ + ` projections.instances.console_app_id,`+ ` projections.instances.setup_started,`+ ` projections.instances.setup_done,`+ ` projections.instances.default_language`+ @@ -79,6 +81,7 @@ func Test_InstancePrepares(t *testing.T) { "global_org_id", "iam_project_id", "console_client_id", + "console_app_id", "setup_started", "setup_done", "default_language", @@ -90,6 +93,7 @@ func Test_InstancePrepares(t *testing.T) { "global-org-id", "project-id", "client-id", + "app-id", domain.Step2, domain.Step1, "en", @@ -103,6 +107,7 @@ func Test_InstancePrepares(t *testing.T) { GlobalOrgID: "global-org-id", IAMProjectID: "project-id", ConsoleID: "client-id", + ConsoleAppID: "app-id", SetupStarted: domain.Step2, SetupDone: domain.Step1, DefaultLanguage: language.English, @@ -121,6 +126,7 @@ func Test_InstancePrepares(t *testing.T) { ` projections.instances.global_org_id,`+ ` projections.instances.iam_project_id,`+ ` projections.instances.console_client_id,`+ + ` projections.instances.console_app_id,`+ ` projections.instances.setup_started,`+ ` projections.instances.setup_done,`+ ` projections.instances.default_language`+ diff --git a/internal/query/projection/instance.go b/internal/query/projection/instance.go index ededbd3ae7..ea00f7ba1f 100644 --- a/internal/query/projection/instance.go +++ b/internal/query/projection/instance.go @@ -18,6 +18,7 @@ const ( InstanceColumnGlobalOrgID = "global_org_id" InstanceColumnProjectID = "iam_project_id" InstanceColumnConsoleID = "console_client_id" + InstanceColumnConsoleAppID = "console_app_id" InstanceColumnSequence = "sequence" InstanceColumnSetUpStarted = "setup_started" InstanceColumnSetUpDone = "setup_done" @@ -129,6 +130,7 @@ func (p *InstanceProjection) reduceConsoleSet(event eventstore.Event) (*handler. handler.NewCol(InstanceColumnChangeDate, e.CreationDate()), handler.NewCol(InstanceColumnSequence, e.Sequence()), handler.NewCol(InstanceColumnConsoleID, e.ClientID), + handler.NewCol(InstanceColumnConsoleAppID, e.AppID), }, ), nil } diff --git a/internal/query/projection/instance_domain.go b/internal/query/projection/instance_domain.go index 9610d45d27..6e4b7f1e19 100644 --- a/internal/query/projection/instance_domain.go +++ b/internal/query/projection/instance_domain.go @@ -19,6 +19,7 @@ const ( InstanceDomainSequenceCol = "sequence" InstanceDomainDomainCol = "domain" InstanceDomainIsGeneratedCol = "is_generated" + InstanceDomainIsPrimaryCol = "is_primary" ) type InstanceDomainProjection struct { @@ -37,6 +38,7 @@ func NewInstanceDomainProjection(ctx context.Context, config crdb.StatementHandl crdb.NewColumn(InstanceDomainSequenceCol, crdb.ColumnTypeInt64), crdb.NewColumn(InstanceDomainDomainCol, crdb.ColumnTypeText), crdb.NewColumn(InstanceDomainIsGeneratedCol, crdb.ColumnTypeBool), + crdb.NewColumn(InstanceDomainIsPrimaryCol, crdb.ColumnTypeBool), }, crdb.NewPrimaryKey(InstanceDomainInstanceIDCol, InstanceDomainDomainCol), ), @@ -54,6 +56,10 @@ func (p *InstanceDomainProjection) reducers() []handler.AggregateReducer { Event: instance.InstanceDomainAddedEventType, Reduce: p.reduceDomainAdded, }, + { + Event: instance.InstanceDomainAddedEventType, + Reduce: p.reduceDomainPrimarySet, + }, { Event: instance.InstanceDomainRemovedEventType, Reduce: p.reduceDomainRemoved, @@ -77,10 +83,43 @@ func (p *InstanceDomainProjection) reduceDomainAdded(event eventstore.Event) (*h handler.NewCol(InstanceDomainDomainCol, e.Domain), handler.NewCol(InstanceDomainInstanceIDCol, e.Aggregate().ID), handler.NewCol(InstanceDomainIsGeneratedCol, e.Generated), + handler.NewCol(InstanceDomainIsPrimaryCol, false), }, ), nil } +func (p *InstanceDomainProjection) reduceDomainPrimarySet(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*instance.DomainPrimarySetEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "PROJE-f8nlw", "reduce.wrong.event.type %s", instance.InstanceDomainPrimarySetEventType) + } + return crdb.NewMultiStatement( + e, + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(InstanceDomainChangeDateCol, e.CreationDate()), + handler.NewCol(InstanceDomainSequenceCol, e.Sequence()), + handler.NewCol(InstanceDomainIsPrimaryCol, false), + }, + []handler.Condition{ + handler.NewCond(InstanceDomainInstanceIDCol, e.Aggregate().InstanceID), + handler.NewCond(InstanceDomainIsPrimaryCol, true), + }, + ), + crdb.AddUpdateStatement( + []handler.Column{ + handler.NewCol(InstanceDomainChangeDateCol, e.CreationDate()), + handler.NewCol(InstanceDomainSequenceCol, e.Sequence()), + handler.NewCol(InstanceDomainIsPrimaryCol, true), + }, + []handler.Condition{ + handler.NewCond(InstanceDomainDomainCol, e.Domain), + handler.NewCond(InstanceDomainInstanceIDCol, e.Aggregate().ID), + }, + ), + ), nil +} + func (p *InstanceDomainProjection) reduceDomainRemoved(event eventstore.Event) (*handler.Statement, error) { e, ok := event.(*instance.DomainRemovedEvent) if !ok { diff --git a/internal/query/projection/instance_domain_test.go b/internal/query/projection/instance_domain_test.go index 772a18327a..5ada417e14 100644 --- a/internal/query/projection/instance_domain_test.go +++ b/internal/query/projection/instance_domain_test.go @@ -38,7 +38,7 @@ func TestInstanceDomainProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "INSERT INTO projections.instance_domains (creation_date, change_date, sequence, domain, instance_id, is_generated) VALUES ($1, $2, $3, $4, $5, $6)", + expectedStmt: "INSERT INTO projections.instance_domains (creation_date, change_date, sequence, domain, instance_id, is_generated, is_primary) VALUES ($1, $2, $3, $4, $5, $6, $7)", expectedArgs: []interface{}{ anyArg{}, anyArg{}, @@ -46,6 +46,7 @@ func TestInstanceDomainProjection_reduces(t *testing.T) { "domain.new", "agg-id", true, + false, }, }, }, diff --git a/internal/repository/instance/domain.go b/internal/repository/instance/domain.go index 8d8a463dfe..c1e8a8784a 100644 --- a/internal/repository/instance/domain.go +++ b/internal/repository/instance/domain.go @@ -11,10 +11,11 @@ import ( ) const ( - UniqueInstanceDomain = "instance_domain" - domainEventPrefix = instanceEventTypePrefix + "domain." - InstanceDomainAddedEventType = domainEventPrefix + "added" - InstanceDomainRemovedEventType = domainEventPrefix + "removed" + UniqueInstanceDomain = "instance_domain" + domainEventPrefix = instanceEventTypePrefix + "domain." + InstanceDomainAddedEventType = domainEventPrefix + "added" + InstanceDomainPrimarySetEventType = domainEventPrefix + "primary.set" + InstanceDomainRemovedEventType = domainEventPrefix + "removed" ) func NewAddInstanceDomainUniqueConstraint(orgDomain string) *eventstore.EventUniqueConstraint { @@ -69,6 +70,43 @@ func DomainAddedEventMapper(event *repository.Event) (eventstore.Event, error) { return orgDomainAdded, nil } +type DomainPrimarySetEvent struct { + eventstore.BaseEvent `json:"-"` + + Domain string `json:"domain,omitempty"` +} + +func (e *DomainPrimarySetEvent) Data() interface{} { + return e +} + +func (e *DomainPrimarySetEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewDomainPrimarySetEvent(ctx context.Context, aggregate *eventstore.Aggregate, domain string) *DomainPrimarySetEvent { + return &DomainPrimarySetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + InstanceDomainPrimarySetEventType, + ), + Domain: domain, + } +} + +func DomainPrimarySetEventMapper(event *repository.Event) (eventstore.Event, error) { + orgDomainAdded := &DomainAddedEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := json.Unmarshal(event.Data, orgDomainAdded) + if err != nil { + return nil, errors.ThrowInternal(err, "INSTANCE-29jöF", "unable to unmarshal instance domain added") + } + + return orgDomainAdded, nil +} + type DomainRemovedEvent struct { eventstore.BaseEvent `json:"-"` diff --git a/internal/repository/instance/event_iam_project_set.go b/internal/repository/instance/event_iam_project_set.go index 1322ce6cf5..75bce840f0 100644 --- a/internal/repository/instance/event_iam_project_set.go +++ b/internal/repository/instance/event_iam_project_set.go @@ -60,6 +60,7 @@ type ConsoleSetEvent struct { eventstore.BaseEvent `json:"-"` ClientID string `json:"clientId"` + AppID string `json:"appId"` } func (e *ConsoleSetEvent) Data() interface{} { @@ -73,7 +74,8 @@ func (e *ConsoleSetEvent) UniqueConstraints() []*eventstore.EventUniqueConstrain func NewIAMConsoleSetEvent( ctx context.Context, aggregate *eventstore.Aggregate, - clientID *string, + clientID, + appID *string, ) *ConsoleSetEvent { return &ConsoleSetEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -82,6 +84,7 @@ func NewIAMConsoleSetEvent( ConsoleSetEventType, ), ClientID: *clientID, + AppID: *appID, } } diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index bb0113f515..a6b235367a 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -87,6 +87,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(CustomTextTemplateRemovedEventType, CustomTextTemplateRemovedEventMapper). RegisterFilterEventMapper(FeaturesSetEventType, FeaturesSetEventMapper). RegisterFilterEventMapper(InstanceDomainAddedEventType, DomainAddedEventMapper). + RegisterFilterEventMapper(InstanceDomainPrimarySetEventType, DomainPrimarySetEventMapper). RegisterFilterEventMapper(InstanceDomainRemovedEventType, DomainRemovedEventMapper). RegisterFilterEventMapper(InstanceAddedEventType, InstanceAddedEventMapper). RegisterFilterEventMapper(InstanceChangedEventType, InstanceChangedEventMapper). diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 622b1522d7..539ff0131d 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1,6 +1,7 @@ syntax = "proto3"; import "zitadel/idp.proto"; +import "zitadel/instance.proto"; import "zitadel/user.proto"; import "zitadel/object.proto"; import "zitadel/options.proto"; @@ -184,6 +185,13 @@ service AdminService { }; } + // Returns the domains of an instance + rpc ListInstanceDomains(ListInstanceDomainsRequest) returns (ListInstanceDomainsResponse) { + option (google.api.http) = { + get: "/domains"; + }; + } + // Set the default language rpc ListSecretGenerators(ListSecretGeneratorsRequest) returns (ListSecretGeneratorsResponse) { option (google.api.http) = { @@ -2642,6 +2650,20 @@ message GetDefaultLanguageResponse { string language = 1; } +message ListInstanceDomainsRequest { + zitadel.v1.ListQuery query = 1; + // the field the result is sorted + zitadel.instance.v1.DomainFieldName sorting_column = 2; + //criterias the client is looking for + repeated zitadel.instance.v1.DomainSearchQuery queries = 3; +} + +message ListInstanceDomainsResponse { + zitadel.v1.ListDetails details = 1; + zitadel.instance.v1.DomainFieldName sorting_column = 2; + repeated zitadel.instance.v1.Domain result = 3; +} + message ListSecretGeneratorsRequest { //list limitations and ordering zitadel.v1.ListQuery query = 1; diff --git a/proto/zitadel/instance.proto b/proto/zitadel/instance.proto index f33a62841a..8e138788cc 100644 --- a/proto/zitadel/instance.proto +++ b/proto/zitadel/instance.proto @@ -20,22 +20,12 @@ message Instance { description: "current state of the instance"; } ]; - string generated_domain = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"organization.zitadel.com\""; - } - ]; - repeated string custom_domains = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "[\"zitadel.com\", \"zitadel.cloud\", \"zitadel.ch\"]"; - } - ]; - string name = 6 [ + string name = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"ZITADEL\""; } ]; - string version = 7 [ + string version = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"v1.0.0\""; } @@ -55,8 +45,7 @@ message Query { option (validate.required) = true; IdQuery id_query = 1; - DomainsQuery domains_query = 2; - StateQuery state_query = 3; + StateQuery state_query = 2; } } @@ -70,21 +59,6 @@ message IdQuery { ]; } -message DomainsQuery { - repeated string domains = 1 [ - (validate.rules).string = {max_len: 200}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"caos.ch\""; - } - ]; - zitadel.v1.ListQueryMethod method = 2 [ - (validate.rules).enum.defined_only = true, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "defines which list equality method is used"; - } - ]; -} - //StateQuery is always equals message StateQuery { State state = 1 [ @@ -98,7 +72,68 @@ message StateQuery { enum FieldName { FIELD_NAME_UNSPECIFIED = 0; FIELD_NAME_ID = 1; - FIELD_NAME_GENERATED_DOMAIN = 2; - FIELD_NAME_NAME = 3; - FIELD_NAME_CREATION_DATE = 4; + FIELD_NAME_NAME = 2; + FIELD_NAME_CREATION_DATE = 3; +} + +message Domain { + zitadel.v1.ObjectDetails details = 1; + string domain = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; + bool primary = 3; + bool generated = 4; +} + +message DomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + DomainGeneratedQuery generated_query = 2; + DomainPrimaryQuery primary_query = 3; + } +} + +message DomainQuery { + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "zitadel.com"; + } + ]; + zitadel.v1.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + +//DomainGeneratedQuery is always equals +message DomainGeneratedQuery { + bool generated = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "generated domains"; + } + ]; +} + +//DomainPrimaryQuery is always equals +message DomainPrimaryQuery { + bool primary = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "primary domains"; + } + ]; +} + +enum DomainFieldName { + DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + DOMAIN_FIELD_NAME_DOMAIN = 1; + DOMAIN_FIELD_NAME_PRIMARY = 2; + DOMAIN_FIELD_NAME_GENERATED = 3; + DOMAIN_FIELD_NAME_CREATION_DATE = 4; } diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index e10a3ea0ba..0618d105cb 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -141,28 +141,37 @@ service SystemService { }; } - // Returns the domain of an instance - rpc GetGeneratedDomain(GetGeneratedDomainRequest) returns (GetGeneratedDomainResponse) { - option (google.api.http) = { - get: "/instances/{id}/domains/generated"; - }; - } - // Returns the custom domains of an instance - rpc GetCustomDomains(GetCustomDomainsRequest) returns (GetCustomDomainsResponse) { + rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { option (google.api.http) = { - get: "/instances/{id}/domains/custom"; + get: "/instances/{id}/domains"; }; } // Returns the domain of an instance - rpc AddCustomDomain(AddCustomDomainRequest) returns (AddCustomDomainResponse) { + rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) { option (google.api.http) = { - post: "/instances/{id}/domains/custom"; + post: "/instances/{id}/domains"; body: "*" }; } + // Returns the domain of an instance + rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) { + option (google.api.http) = { + delete: "/instances/{id}/domains/{domain}"; + }; + } + + // Returns the domain of an instance + rpc SetPrimaryDomain(SetPrimaryDomainRequest) returns (SetPrimaryDomainResponse) { + option (google.api.http) = { + post: "/instances/{id}/domains/_set_primary"; + body: "*" + }; + } + + //Returns all stored read models of ZITADEL // views are used for search optimisation and optimise request latencies // they represent the delta of the event happend on the objects @@ -346,30 +355,45 @@ message GetUsageResponse { uint64 executed_action_mins = 3; } -message GetGeneratedDomainRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +message ListDomainsRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];//list limitations and ordering + zitadel.v1.ListQuery query = 2; + // the field the result is sorted + zitadel.instance.v1.DomainFieldName sorting_column = 3; + //criterias the client is looking for + repeated zitadel.instance.v1.DomainSearchQuery queries = 4; } -message GetGeneratedDomainResponse { +message ListDomainsResponse { + zitadel.v1.ListDetails details = 1; + zitadel.instance.v1.DomainFieldName sorting_column = 2; + repeated zitadel.instance.v1.Domain result = 3; +} + +message AddDomainRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message AddDomainResponse { zitadel.v1.ObjectDetails details = 1; - string domain = 2; } -message GetCustomDomainsRequest { +message RemoveDomainRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } -message GetCustomDomainsResponse { +message RemoveDomainResponse { zitadel.v1.ObjectDetails details = 1; - repeated string domains = 2; } -message AddCustomDomainRequest { +message SetPrimaryDomainRequest { string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string custom_domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } -message AddCustomDomainResponse { +message SetPrimaryDomainResponse { zitadel.v1.ObjectDetails details = 1; }