feat: System api (#3461)

* feat: start system api

* feat: remove auth

* feat: change gitignore

* feat: run system api

* feat: remove clear view form admin api

* feat: search instances

* feat: add instance

* fix: set primary domain

* Update .gitignore

* fix: add instance

* fix: add instance

* fix: handle errors

* fix: handle instance name

* fix: test

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi 2022-04-21 12:37:39 +02:00 committed by GitHub
parent a7816a43b1
commit 3d5891eb11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1216 additions and 485 deletions

2
.gitignore vendored
View File

@ -58,7 +58,7 @@ openapi/**/*.json
/internal/api/ui/console/static/*
# local
build/local/cloud.env
build/local/*.env
migrations/cockroach/migrate_cloud.go
.notifications
.artifacts

View File

@ -24,6 +24,8 @@ type DefaultInstance struct {
domain string
defaults systemdefaults.SystemDefaults
zitadelRoles []authz.RoleMapping
baseURL string
externalSecure bool
}
func (mig *DefaultInstance) Execute(ctx context.Context) error {
@ -45,7 +47,8 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error {
mig.zitadelRoles,
nil,
nil,
webauthn_helper.Config{},
//TODO: Livio will fix this, but it ZITADEL doesn't run without this
webauthn_helper.Config{DisplayName: "HELLO LIVIO", ID: "RPID"},
nil,
nil,
nil,
@ -54,8 +57,12 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error {
nil,
nil)
if err != nil {
return err
}
ctx = authz.WithRequestedDomain(ctx, mig.domain)
_, err = cmd.SetUpInstance(ctx, &mig.InstanceSetup)
_, _, err = cmd.SetUpInstance(ctx, &mig.InstanceSetup, mig.externalSecure, mig.baseURL)
return err
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/command"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
@ -23,12 +24,19 @@ type Config struct {
ExternalSecure bool
Log *logging.Config
EncryptionKeys *encryptionKeyConfig
DefaultInstance command.InstanceSetup
}
func MustNewConfig(v *viper.Viper) *Config {
config := new(Config)
err := v.Unmarshal(config)
logging.OnError(err).Fatal("unable to read config")
err := v.Unmarshal(config,
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hook.Base64ToBytesHookFunc(),
hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
)),
)
err = config.Log.SetLogger()
logging.OnError(err).Fatal("unable to set logger")

View File

@ -51,6 +51,12 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient}
steps.s2AssetsTable = &AssetTable{dbClient: dbClient}
instanceSetup := config.DefaultInstance
instanceSetup.InstanceName = steps.S3DefaultInstance.InstanceSetup.InstanceName
instanceSetup.CustomDomain = steps.S3DefaultInstance.InstanceSetup.CustomDomain
instanceSetup.Org = steps.S3DefaultInstance.InstanceSetup.Org
steps.S3DefaultInstance.InstanceSetup = instanceSetup
steps.S3DefaultInstance.InstanceSetup.Org.Human.Email.Address = strings.TrimSpace(steps.S3DefaultInstance.InstanceSetup.Org.Human.Email.Address)
if steps.S3DefaultInstance.InstanceSetup.Org.Human.Email.Address == "" {
steps.S3DefaultInstance.InstanceSetup.Org.Human.Email.Address = "admin@" + config.ExternalDomain
@ -63,13 +69,14 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.S3DefaultInstance.domain = config.ExternalDomain
steps.S3DefaultInstance.zitadelRoles = config.InternalAuthZ.RolePermissionMappings
steps.S3DefaultInstance.userEncryptionKey = config.EncryptionKeys.User
steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure
steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure
steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
steps.S3DefaultInstance.externalSecure = config.ExternalSecure
steps.S3DefaultInstance.baseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
ctx := context.Background()
migration.Migrate(ctx, eventstoreClient, steps.s1ProjectionTable)
migration.Migrate(ctx, eventstoreClient, steps.s2AssetsTable)
migration.Migrate(ctx, eventstoreClient, steps.S3DefaultInstance)
err = migration.Migrate(ctx, eventstoreClient, steps.s1ProjectionTable)
logging.OnError(err).Fatal("unable to migrate step 1")
err = migration.Migrate(ctx, eventstoreClient, steps.s2AssetsTable)
logging.OnError(err).Fatal("unable to migrate step 3")
err = migration.Migrate(ctx, eventstoreClient, steps.S3DefaultInstance)
logging.OnError(err).Fatal("unable to migrate step 4")
}

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,9 @@ package start
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/config/hook"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
admin_es "github.com/caos/zitadel/internal/admin/repository/eventsourcing"
@ -42,14 +45,20 @@ type Config struct {
InternalAuthZ internal_authz.Config
SystemDefaults systemdefaults.SystemDefaults
EncryptionKeys *encryptionKeyConfig
DefaultInstance command.InstanceSetup
}
func MustNewConfig(v *viper.Viper) *Config {
config := new(Config)
err := v.Unmarshal(config)
logging.OnError(err).Fatal("unable to read config")
err := v.Unmarshal(config,
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
hook.Base64ToBytesHookFunc(),
hook.TagToLanguageHookFunc(),
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
)),
)
err = config.Log.SetLogger()
logging.OnError(err).Fatal("unable to set logger")

View File

@ -14,6 +14,7 @@ import (
"github.com/caos/logging"
"github.com/caos/oidc/pkg/op"
"github.com/caos/zitadel/internal/api/grpc/system"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -142,7 +143,7 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
}
verifier := internal_authz.Start(repo)
apis := api.New(config.Port, router, &repo, config.InternalAuthZ, config.ExternalSecure, config.HTTP2HostHeader)
authenticatedAPIs := api.New(config.Port, router, &repo, config.InternalAuthZ, config.ExternalSecure, config.HTTP2HostHeader)
authRepo, err := auth_es.Start(config.Auth, config.SystemDefaults, commands, queries, dbClient, assets.HandlerPrefix, keys.OIDC, keys.User)
if err != nil {
return fmt.Errorf("error starting auth repo: %w", err)
@ -151,18 +152,21 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
if err != nil {
return fmt.Errorf("error starting admin repo: %w", err)
}
if err := apis.RegisterServer(ctx, admin.CreateServer(commands, queries, adminRepo, config.SystemDefaults.Domain, assets.HandlerPrefix, keys.User)); err != nil {
if err := authenticatedAPIs.RegisterServer(ctx, system.CreateServer(commands, queries, adminRepo, config.DefaultInstance, config.ExternalPort, config.ExternalDomain, config.ExternalSecure)); err != nil {
return err
}
if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, assets.HandlerPrefix, keys.User)); err != nil {
if err := authenticatedAPIs.RegisterServer(ctx, admin.CreateServer(commands, queries, adminRepo, config.SystemDefaults.Domain, assets.HandlerPrefix, keys.User)); err != nil {
return err
}
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, assets.HandlerPrefix, keys.User)); err != nil {
if err := authenticatedAPIs.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, assets.HandlerPrefix, keys.User)); err != nil {
return err
}
if err := authenticatedAPIs.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, assets.HandlerPrefix, keys.User)); err != nil {
return err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader)
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries, instanceInterceptor.Handler))
authenticatedAPIs.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries, instanceInterceptor.Handler))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, config.ExternalDomain, id.SonyFlakeGenerator, config.ExternalSecure)
if err != nil {
@ -174,26 +178,26 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
if err != nil {
return fmt.Errorf("unable to start oidc provider: %w", err)
}
apis.RegisterHandler(oidc.HandlerPrefix, oidcProvider.HttpHandler())
authenticatedAPIs.RegisterHandler(oidc.HandlerPrefix, oidcProvider.HttpHandler())
openAPIHandler, err := openapi.Start()
if err != nil {
return fmt.Errorf("unable to start openapi handler: %w", err)
}
apis.RegisterHandler(openapi.HandlerPrefix, openAPIHandler)
authenticatedAPIs.RegisterHandler(openapi.HandlerPrefix, openAPIHandler)
baseURL := http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
c, err := console.Start(config.Console, config.ExternalDomain, baseURL, issuer, instanceInterceptor.Handler)
if err != nil {
return fmt.Errorf("unable to start console: %w", err)
}
apis.RegisterHandler(console.HandlerPrefix, c)
authenticatedAPIs.RegisterHandler(console.HandlerPrefix, c)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, config.SystemDefaults, console.HandlerPrefix+"/", config.ExternalDomain, baseURL, op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, instanceInterceptor.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
if err != nil {
return fmt.Errorf("unable to start login: %w", err)
}
apis.RegisterHandler(login.HandlerPrefix, l.Handler())
authenticatedAPIs.RegisterHandler(login.HandlerPrefix, l.Handler())
return nil
}

File diff suppressed because one or more lines are too long

View File

@ -1427,21 +1427,6 @@ they represent the delta of the event happend on the objects
POST: /views/_search
### ClearView
> **rpc** ClearView([ClearViewRequest](#clearviewrequest))
[ClearViewResponse](#clearviewresponse)
Truncates the delta of the change stream
be carefull with this function because ZITADEL has to
recompute the deltas after they got cleared.
Search requests will return wrong results until all deltas are recomputed
POST: /views/{database}/{view_name}
### ListFailedEvents
> **rpc** ListFailedEvents([ListFailedEventsRequest](#listfailedeventsrequest))
@ -1718,24 +1703,6 @@ This is an empty request
### ClearViewRequest
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| database | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| view_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
### ClearViewResponse
This is an empty response
### DeactivateIDPRequest

View File

@ -70,13 +70,13 @@ DomainPrimaryQuery is always equals
### IdQuery
### IdsQuery
IdQuery is always equals
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | string.max_len: 200<br /> |
| ids | repeated string | - | |
@ -91,7 +91,6 @@ IdQuery is always equals
| details | zitadel.v1.ObjectDetails | - | |
| state | State | - | |
| name | string | - | |
| version | string | - | |
@ -102,19 +101,7 @@ 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.state_query | StateQuery | - | |
### StateQuery
StateQuery is always equals
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| state | State | - | enum.defined_only: true<br /> |
| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.id_query | IdsQuery | - | |

View File

@ -29,7 +29,7 @@ Returns a list of ZITADEL instances/tenants
POST: /instances
POST: /instances/_search
### GetInstance
@ -70,18 +70,6 @@ This might take some time
DELETE: /instances/{id}
### GetUsage
> **rpc** GetUsage([GetUsageRequest](#getusagerequest))
[GetUsageResponse](#getusageresponse)
Returns the usage metrics of an instance
GET: /instances/{id}/usage
### ListDomains
> **rpc** ListDomains([ListDomainsRequest](#listdomainsrequest))
@ -91,7 +79,7 @@ Returns the custom domains of an instance
GET: /instances/{id}/domains
POST: /instances/{id}/domains/_search
### AddDomain
@ -227,13 +215,12 @@ failed event. You can find out if it worked on the `failure_count`
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| instance_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| first_org_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 /> |
| owner_first_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| owner_last_name | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| owner_first_name | string | - | string.max_len: 200<br /> |
| owner_last_name | string | - | string.max_len: 200<br /> |
| owner_email | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| owner_username | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| password | string | - | string.min_len: 1<br /> string.max_len: 200<br /> |
| owner_username | string | - | string.max_len: 200<br /> |
| request_limit | uint64 | - | |
| action_mins_limit | uint64 | - | |
@ -247,6 +234,7 @@ failed event. You can find out if it worked on the `failure_count`
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| id | string | - | |
| details | zitadel.v1.ObjectDetails | - | |

View File

@ -69,7 +69,9 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.Server) erro
return err
}
a.RegisterHandler(prefix, handler)
if a.verifier != nil {
a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods())
}
return nil
}

View File

@ -1,9 +1,8 @@
package admin_test
package admin
import (
"testing"
admin_grpc "github.com/caos/zitadel/internal/api/grpc/admin"
"github.com/caos/zitadel/internal/test"
"github.com/caos/zitadel/internal/view/model"
admin_pb "github.com/caos/zitadel/pkg/grpc/admin"
@ -34,7 +33,7 @@ func TestFailedEventsToPbFields(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := admin_grpc.FailedEventsViewToPb(tt.args.failedEvents)
got := FailedEventsViewToPb(tt.args.failedEvents)
for _, g := range got {
test.AssertFieldsMapped(t, g)
}
@ -64,7 +63,7 @@ func TestFailedEventToPbFields(t *testing.T) {
},
}
for _, tt := range tests {
converted := admin_grpc.FailedEventViewToPb(tt.args.failedEvent)
converted := FailedEventViewToPb(tt.args.failedEvent)
test.AssertFieldsMapped(t, converted)
}
}
@ -89,7 +88,7 @@ func TestRemoveFailedEventRequestToModelFields(t *testing.T) {
},
}
for _, tt := range tests {
converted := admin_grpc.RemoveFailedEventRequestToModel(tt.args.req)
converted := RemoveFailedEventRequestToModel(tt.args.req)
test.AssertFieldsMapped(t, converted, "FailureCount", "ErrMsg")
}
}

View File

@ -22,16 +22,3 @@ func (s *Server) ListViews(ctx context.Context, _ *admin_pb.ListViewsRequest) (*
convertedCurrentSequences = append(convertedCurrentSequences, convertedViews...)
return &admin_pb.ListViewsResponse{Result: convertedCurrentSequences}, nil
}
func (s *Server) ClearView(ctx context.Context, req *admin_pb.ClearViewRequest) (*admin_pb.ClearViewResponse, error) {
var err error
if req.Database != "zitadel" {
err = s.administrator.ClearView(ctx, req.Database, req.ViewName)
} else {
err = s.query.ClearCurrentSequence(ctx, req.ViewName)
}
if err != nil {
return nil, err
}
return &admin_pb.ClearViewResponse{}, nil
}

View File

@ -7,6 +7,46 @@ import (
instance_pb "github.com/caos/zitadel/pkg/grpc/instance"
)
func InstancesToPb(instances []*query.Instance) []*instance_pb.Instance {
list := make([]*instance_pb.Instance, len(instances))
for i, instance := range instances {
list[i] = InstanceToPb(instance)
}
return list
}
func InstanceToPb(instance *query.Instance) *instance_pb.Instance {
return &instance_pb.Instance{
Details: object.ToViewDetailsPb(
instance.Sequence,
instance.CreationDate,
instance.ChangeDate,
instance.InstanceID(),
),
Id: instance.InstanceID(),
}
}
func InstanceQueriesToModel(queries []*instance_pb.Query) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = InstanceQueryToModel(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func InstanceQueryToModel(searchQuery *instance_pb.Query) (query.SearchQuery, error) {
switch q := searchQuery.Query.(type) {
case *instance_pb.Query_IdQuery:
return query.NewInstanceIDsListSearchQuery(q.IdQuery.Ids...)
default:
return nil, errors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid")
}
}
func DomainQueriesToModel(queries []*instance_pb.DomainSearchQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
@ -27,7 +67,7 @@ func DomainQueryToModel(searchQuery *instance_pb.DomainSearchQuery) (query.Searc
case *instance_pb.DomainSearchQuery_PrimaryQuery:
return query.NewInstanceDomainPrimarySearchQuery(q.PrimaryQuery.Primary)
default:
return nil, errors.ThrowInvalidArgument(nil, "ORG-Ags42", "List.Query.Invalid")
return nil, errors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid")
}
}

View File

@ -15,6 +15,10 @@ import (
func AuthorizationInterceptor(verifier *authz.TokenVerifier, authConfig authz.Config) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
//TODO: Change as soon as we know how to authenticate system api
if verifier == nil {
return handler(ctx, req)
}
return authorize(ctx, req, info, handler, verifier, authConfig)
}
}

View File

@ -3,6 +3,7 @@ package middleware
import (
"context"
"fmt"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@ -16,13 +17,19 @@ type InstanceVerifier interface {
GetInstance(ctx context.Context)
}
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string) grpc.UnaryServerInterceptor {
func InstanceInterceptor(verifier authz.InstanceVerifier, headerName string, ignoredServices ...string) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return setInstance(ctx, req, info, handler, verifier, headerName)
return setInstance(ctx, req, info, handler, verifier, headerName, ignoredServices...)
}
}
func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, headerName string) (_ interface{}, err error) {
func setInstance(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, verifier authz.InstanceVerifier, headerName string, ignoredServices ...string) (_ interface{}, err error) {
for _, service := range ignoredServices {
if strings.HasPrefix(info.FullMethod, service) {
return handler(ctx, req)
}
}
host, err := hostNameFromContext(ctx, headerName)
if err != nil {
return nil, status.Error(codes.PermissionDenied, err.Error())

View File

@ -30,7 +30,8 @@ func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, querie
middleware.SentryHandler(),
middleware.NoCacheInterceptor(),
middleware.ErrorHandler(),
middleware.InstanceInterceptor(queries, hostHeaderName),
//TODO: Handle Ignored Services
middleware.InstanceInterceptor(queries, hostHeaderName, "/zitadel.system.v1.SystemService"),
middleware.AuthorizationInterceptor(verifier, authConfig),
middleware.TranslationHandler(queries),
middleware.ValidationHandler(),

View File

@ -0,0 +1,37 @@
package system
import (
"context"
"github.com/caos/zitadel/internal/query"
system_pb "github.com/caos/zitadel/pkg/grpc/system"
)
func (s *Server) ListFailedEvents(ctx context.Context, req *system_pb.ListFailedEventsRequest) (*system_pb.ListFailedEventsResponse, error) {
failedEventsOld, err := s.administrator.GetFailedEvents(ctx)
if err != nil {
return nil, err
}
convertedOld := FailedEventsViewToPb(failedEventsOld)
failedEvents, err := s.query.SearchFailedEvents(ctx, new(query.FailedEventSearchQueries))
if err != nil {
return nil, err
}
convertedNew := FailedEventsToPb(failedEvents)
convertedOld = append(convertedOld, convertedNew...)
return &system_pb.ListFailedEventsResponse{Result: convertedOld}, nil
}
func (s *Server) RemoveFailedEvent(ctx context.Context, req *system_pb.RemoveFailedEventRequest) (*system_pb.RemoveFailedEventResponse, error) {
var err error
if req.Database != "zitadel" {
err = s.administrator.RemoveFailedEvent(ctx, RemoveFailedEventRequestToModel(req))
} else {
err = s.query.RemoveFailedEvent(ctx, req.ViewName, req.FailedSequence)
}
if err != nil {
return nil, err
}
return &system_pb.RemoveFailedEventResponse{}, nil
}

View File

@ -0,0 +1,51 @@
package system
import (
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/view/model"
system_pb "github.com/caos/zitadel/pkg/grpc/system"
)
func FailedEventsViewToPb(failedEvents []*model.FailedEvent) []*system_pb.FailedEvent {
events := make([]*system_pb.FailedEvent, len(failedEvents))
for i, failedEvent := range failedEvents {
events[i] = FailedEventViewToPb(failedEvent)
}
return events
}
func FailedEventViewToPb(failedEvent *model.FailedEvent) *system_pb.FailedEvent {
return &system_pb.FailedEvent{
Database: failedEvent.Database,
ViewName: failedEvent.ViewName,
FailedSequence: failedEvent.FailedSequence,
FailureCount: failedEvent.FailureCount,
ErrorMessage: failedEvent.ErrMsg,
}
}
func FailedEventsToPb(failedEvents *query.FailedEvents) []*system_pb.FailedEvent {
events := make([]*system_pb.FailedEvent, len(failedEvents.FailedEvents))
for i, failedEvent := range failedEvents.FailedEvents {
events[i] = FailedEventToPb(failedEvent)
}
return events
}
func FailedEventToPb(failedEvent *query.FailedEvent) *system_pb.FailedEvent {
return &system_pb.FailedEvent{
Database: "zitadel",
ViewName: failedEvent.ProjectionName,
FailedSequence: failedEvent.FailedSequence,
FailureCount: failedEvent.FailureCount,
ErrorMessage: failedEvent.Error,
}
}
func RemoveFailedEventRequestToModel(req *system_pb.RemoveFailedEventRequest) *model.FailedEvent {
return &model.FailedEvent{
Database: req.Database,
ViewName: req.ViewName,
FailedSequence: req.FailedSequence,
}
}

View File

@ -0,0 +1,95 @@
package system_test
import (
"testing"
system_grpc "github.com/caos/zitadel/internal/api/grpc/system"
"github.com/caos/zitadel/internal/test"
"github.com/caos/zitadel/internal/view/model"
system_pb "github.com/caos/zitadel/pkg/grpc/system"
)
func TestFailedEventsToPbFields(t *testing.T) {
type args struct {
failedEvents []*model.FailedEvent
}
tests := []struct {
name string
args args
}{
{
name: "all fields",
args: args{
failedEvents: []*model.FailedEvent{
{
Database: "admin",
ViewName: "users",
FailedSequence: 456,
FailureCount: 5,
ErrMsg: "some error",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := system_grpc.FailedEventsViewToPb(tt.args.failedEvents)
for _, g := range got {
test.AssertFieldsMapped(t, g)
}
})
}
}
func TestFailedEventToPbFields(t *testing.T) {
type args struct {
failedEvent *model.FailedEvent
}
tests := []struct {
name string
args args
}{
{
"all fields",
args{
failedEvent: &model.FailedEvent{
Database: "admin",
ViewName: "users",
FailedSequence: 456,
FailureCount: 5,
ErrMsg: "some error",
},
},
},
}
for _, tt := range tests {
converted := system_grpc.FailedEventViewToPb(tt.args.failedEvent)
test.AssertFieldsMapped(t, converted)
}
}
func TestRemoveFailedEventRequestToModelFields(t *testing.T) {
type args struct {
req *system_pb.RemoveFailedEventRequest
}
tests := []struct {
name string
args args
}{
{
"all fields",
args{
req: &system_pb.RemoveFailedEventRequest{
Database: "admin",
ViewName: "users",
FailedSequence: 456,
},
},
},
}
for _, tt := range tests {
converted := system_grpc.RemoveFailedEventRequestToModel(tt.args.req)
test.AssertFieldsMapped(t, converted, "FailureCount", "ErrMsg")
}
}

View File

@ -0,0 +1,127 @@
package system
import (
"context"
"github.com/caos/zitadel/internal/api/authz"
instance_grpc "github.com/caos/zitadel/internal/api/grpc/instance"
"github.com/caos/zitadel/internal/api/grpc/object"
object_pb "github.com/caos/zitadel/pkg/grpc/object"
system_pb "github.com/caos/zitadel/pkg/grpc/system"
)
func (s *Server) ListInstances(ctx context.Context, req *system_pb.ListInstancesRequest) (*system_pb.ListInstancesResponse, error) {
queries, err := ListInstancesRequestToModel(req)
if err != nil {
return nil, err
}
result, err := s.query.SearchInstances(ctx, queries)
if err != nil {
return nil, err
}
return &system_pb.ListInstancesResponse{
Result: instance_grpc.InstancesToPb(result.Instances),
Details: &object_pb.ListDetails{
TotalResult: result.Count,
},
}, nil
}
func (s *Server) GetInstance(ctx context.Context, req *system_pb.GetInstanceRequest) (*system_pb.GetInstanceResponse, error) {
ctx = authz.WithInstanceID(ctx, req.Id)
instance, err := s.query.Instance(ctx)
if err != nil {
return nil, err
}
return &system_pb.GetInstanceResponse{
Instance: instance_grpc.InstanceToPb(instance),
}, nil
}
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), s.ExternalSecure, s.BaseURL)
if err != nil {
return nil, err
}
return &system_pb.AddInstanceResponse{
Id: id,
Details: object.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
return nil, nil
}
func (s *Server) ListDomains(ctx context.Context, req *system_pb.ListDomainsRequest) (*system_pb.ListDomainsResponse, error) {
ctx = authz.WithInstanceID(ctx, req.Id)
queries, err := ListInstanceDomainsRequestToModel(req)
if err != nil {
return nil, err
}
domains, err := s.query.SearchInstanceDomains(ctx, queries)
if err != nil {
return nil, err
}
return &system_pb.ListDomainsResponse{
Result: instance_grpc.DomainsToPb(domains.Domains),
Details: object.ToListDetails(
domains.Count,
domains.Sequence,
domains.Timestamp,
),
}, nil
}
func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest) (*system_pb.AddDomainResponse, error) {
ctx = authz.WithInstanceID(ctx, req.Id)
instance, err := s.query.Instance(ctx)
if err != nil {
return nil, err
}
ctx = authz.WithInstance(ctx, instance)
details, err := s.command.AddInstanceDomain(ctx, req.Domain)
if err != nil {
return nil, err
}
return &system_pb.AddDomainResponse{
Details: object.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}
func (s *Server) RemoveDomain(ctx context.Context, req *system_pb.RemoveDomainRequest) (*system_pb.RemoveDomainResponse, error) {
ctx = authz.WithInstanceID(ctx, req.Id)
details, err := s.command.RemoveInstanceDomain(ctx, req.Domain)
if err != nil {
return nil, err
}
return &system_pb.RemoveDomainResponse{
Details: object.ChangeToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}
func (s *Server) SetPrimaryDomain(ctx context.Context, req *system_pb.SetPrimaryDomainRequest) (*system_pb.SetPrimaryDomainResponse, error) {
ctx = authz.WithInstanceID(ctx, req.Id)
details, err := s.command.SetPrimaryInstanceDomain(ctx, req.Domain)
if err != nil {
return nil, err
}
return &system_pb.SetPrimaryDomainResponse{
Details: object.ChangeToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
}, nil
}

View File

@ -0,0 +1,97 @@
package system
import (
instance_grpc "github.com/caos/zitadel/internal/api/grpc/instance"
"github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/query"
instance_pb "github.com/caos/zitadel/pkg/grpc/instance"
system_pb "github.com/caos/zitadel/pkg/grpc/system"
)
func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, 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 req.OwnerEmail != "" {
defaultInstance.Org.Human.Email.Address = req.OwnerEmail
}
if req.OwnerUsername != "" {
defaultInstance.Org.Human.Username = req.OwnerUsername
}
if req.OwnerFirstName != "" {
defaultInstance.Org.Human.FirstName = req.OwnerFirstName
}
if req.OwnerLastName != "" {
defaultInstance.Org.Human.LastName = req.OwnerLastName
}
return &defaultInstance
}
func ListInstancesRequestToModel(req *system_pb.ListInstancesRequest) (*query.InstanceSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := instance_grpc.InstanceQueriesToModel(req.Queries)
if err != nil {
return nil, err
}
return &query.InstanceSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: fieldNameToInstanceColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func fieldNameToInstanceColumn(fieldName instance_pb.FieldName) query.Column {
switch fieldName {
case instance_pb.FieldName_FIELD_NAME_ID:
return query.InstanceColumnID
case instance_pb.FieldName_FIELD_NAME_NAME:
return query.InstanceColumnName
case instance_pb.FieldName_FIELD_NAME_CREATION_DATE:
return query.InstanceColumnCreationDate
default:
return query.Column{}
}
}
func ListInstanceDomainsRequestToModel(req *system_pb.ListDomainsRequest) (*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: fieldNameToInstanceDomainColumn(req.SortingColumn),
},
Queries: queries,
}, nil
}
func fieldNameToInstanceDomainColumn(fieldName instance_pb.DomainFieldName) query.Column {
switch fieldName {
case instance_pb.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN:
return query.InstanceDomainDomainCol
case instance_pb.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED:
return query.InstanceDomainIsGeneratedCol
case instance_pb.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY:
return query.InstanceDomainIsPrimaryCol
case instance_pb.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE:
return query.InstanceDomainCreationDateCol
default:
return query.Column{}
}
}

View File

@ -0,0 +1,75 @@
package system
import (
"github.com/caos/zitadel/internal/admin/repository"
http_util "github.com/caos/zitadel/internal/api/http"
"google.golang.org/grpc"
"github.com/caos/zitadel/internal/admin/repository/eventsourcing"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/api/grpc/server"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/pkg/grpc/system"
)
const (
systemAPI = "System-API"
)
var _ system.SystemServiceServer = (*Server)(nil)
type Server struct {
system.UnimplementedSystemServiceServer
command *command.Commands
query *query.Queries
administrator repository.AdministratorRepository
DefaultInstance command.InstanceSetup
ExternalSecure bool
BaseURL string
}
type Config struct {
Repository eventsourcing.Config
}
func CreateServer(command *command.Commands,
query *query.Queries,
repo repository.Repository,
defaultInstance command.InstanceSetup,
externalPort uint16,
externalDomain string,
externalSecure bool) *Server {
return &Server{
command: command,
query: query,
administrator: repo,
DefaultInstance: defaultInstance,
ExternalSecure: externalSecure,
BaseURL: http_util.BuildHTTP(externalDomain, externalPort, externalSecure),
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
system.RegisterSystemServiceServer(grpcServer, s)
}
func (s *Server) AppName() string {
return systemAPI
}
func (s *Server) MethodPrefix() string {
return system.SystemService_MethodPrefix
}
func (s *Server) AuthMethods() authz.MethodMapping {
return system.SystemService_AuthMethods
}
func (s *Server) RegisterGateway() server.GatewayFunc {
return system.RegisterSystemServiceHandlerFromEndpoint
}
func (s *Server) GatewayPathPrefix() string {
return "/system/v1"
}

View File

@ -0,0 +1,37 @@
package system
import (
"context"
"github.com/caos/zitadel/internal/query"
system_pb "github.com/caos/zitadel/pkg/grpc/system"
)
func (s *Server) ListViews(ctx context.Context, _ *system_pb.ListViewsRequest) (*system_pb.ListViewsResponse, error) {
currentSequences, err := s.query.SearchCurrentSequences(ctx, new(query.CurrentSequencesSearchQueries))
if err != nil {
return nil, err
}
convertedCurrentSequences := CurrentSequencesToPb(currentSequences)
views, err := s.administrator.GetViews()
if err != nil {
return nil, err
}
convertedViews := ViewsToPb(views)
convertedCurrentSequences = append(convertedCurrentSequences, convertedViews...)
return &system_pb.ListViewsResponse{Result: convertedCurrentSequences}, nil
}
func (s *Server) ClearView(ctx context.Context, req *system_pb.ClearViewRequest) (*system_pb.ClearViewResponse, error) {
var err error
if req.Database != "zitadel" {
err = s.administrator.ClearView(ctx, req.Database, req.ViewName)
} else {
err = s.query.ClearCurrentSequence(ctx, req.ViewName)
}
if err != nil {
return nil, err
}
return &system_pb.ClearViewResponse{}, nil
}

View File

@ -0,0 +1,43 @@
package system
import (
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/view/model"
system_pb "github.com/caos/zitadel/pkg/grpc/system"
"google.golang.org/protobuf/types/known/timestamppb"
)
func ViewsToPb(views []*model.View) []*system_pb.View {
v := make([]*system_pb.View, len(views))
for i, view := range views {
v[i] = ViewToPb(view)
}
return v
}
func ViewToPb(view *model.View) *system_pb.View {
return &system_pb.View{
Database: view.Database,
ViewName: view.ViewName,
LastSuccessfulSpoolerRun: timestamppb.New(view.LastSuccessfulSpoolerRun),
ProcessedSequence: view.CurrentSequence,
EventTimestamp: timestamppb.New(view.EventTimestamp),
}
}
func CurrentSequencesToPb(currentSequences *query.CurrentSequences) []*system_pb.View {
v := make([]*system_pb.View, len(currentSequences.CurrentSequences))
for i, currentSequence := range currentSequences.CurrentSequences {
v[i] = CurrentSequenceToPb(currentSequence)
}
return v
}
func CurrentSequenceToPb(currentSequence *query.CurrentSequence) *system_pb.View {
return &system_pb.View{
Database: "zitadel",
ViewName: currentSequence.ProjectionName,
ProcessedSequence: currentSequence.CurrentSequence,
EventTimestamp: timestamppb.New(currentSequence.Timestamp),
}
}

View File

@ -28,9 +28,21 @@ const (
consolePostLogoutPath = console.HandlerPrefix + "/signedout"
)
type AddInstance struct {
InstanceName string
CustomDomain string
FirstOrgName string
OwnerEmail string
OwnerUsername string
OwnerFirstName string
OwnerLastName string
}
type InstanceSetup struct {
zitadel ZitadelConfig
InstanceName string
CustomDomain string
Org OrgSetup
Zitadel ZitadelConfig
Features struct {
TierName string
TierDescription string
@ -120,9 +132,6 @@ type InstanceSetup struct {
}
type ZitadelConfig struct {
IsDevMode bool
BaseURL string
projectID string
mgmtAppID string
adminAppID string
@ -131,41 +140,41 @@ type ZitadelConfig struct {
}
func (s *InstanceSetup) generateIDs() (err error) {
s.Zitadel.projectID, err = id.SonyFlakeGenerator.Next()
s.zitadel.projectID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.mgmtAppID, err = id.SonyFlakeGenerator.Next()
s.zitadel.mgmtAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.adminAppID, err = id.SonyFlakeGenerator.Next()
s.zitadel.adminAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.authAppID, err = id.SonyFlakeGenerator.Next()
s.zitadel.authAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
s.Zitadel.consoleAppID, err = id.SonyFlakeGenerator.Next()
s.zitadel.consoleAppID, err = id.SonyFlakeGenerator.Next()
if err != nil {
return err
}
return nil
}
func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*domain.ObjectDetails, error) {
func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup, externalSecure bool, baseURL string) (string, *domain.ObjectDetails, error) {
instanceID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
return "", nil, err
}
if err = c.eventstore.NewInstance(ctx, instanceID); err != nil {
return nil, err
return "", nil, err
}
ctx = authz.SetCtxData(authz.WithInstanceID(ctx, instanceID), authz.CtxData{OrgID: instanceID, ResourceOwner: instanceID})
@ -174,16 +183,16 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*do
orgID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
return "", nil, err
}
userID, err := id.SonyFlakeGenerator.Next()
if err != nil {
return nil, err
return "", nil, err
}
if err = setup.generateIDs(); err != nil {
return nil, err
return "", nil, err
}
setup.Org.Human.PasswordChangeRequired = true
@ -191,9 +200,11 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*do
instanceAgg := instance.NewAggregate(instanceID)
orgAgg := org.NewAggregate(orgID)
userAgg := user.NewAggregate(userID, orgID)
projectAgg := project.NewAggregate(setup.Zitadel.projectID, orgID)
projectAgg := project.NewAggregate(setup.zitadel.projectID, orgID)
validations := []preparation.Validation{
addInstance(instanceAgg, setup.InstanceName),
c.addGeneratedInstanceDomain(instanceAgg, setup.InstanceName),
SetDefaultFeatures(
instanceAgg,
setup.Features.TierName,
@ -289,20 +300,24 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*do
validations = append(validations, SetInstanceCustomTexts(instanceAgg, msg))
}
if setup.CustomDomain != "" {
validations = append(validations, c.addInstanceDomain(instanceAgg, setup.CustomDomain, false))
}
console := &addOIDCApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.consoleAppID,
ID: setup.zitadel.consoleAppID,
Name: consoleAppName,
},
Version: domain.OIDCVersionV1,
RedirectUris: []string{setup.Zitadel.BaseURL + consoleRedirectPath},
RedirectUris: []string{baseURL + consoleRedirectPath},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: domain.OIDCApplicationTypeUserAgent,
AuthMethodType: domain.OIDCAuthMethodTypeNone,
PostLogoutRedirectUris: []string{setup.Zitadel.BaseURL + consolePostLogoutPath},
DevMode: setup.Zitadel.IsDevMode,
PostLogoutRedirectUris: []string{baseURL + consolePostLogoutPath},
DevMode: !externalSecure,
AccessTokenType: domain.OIDCTokenTypeBearer,
AccessTokenRoleAssertion: false,
IDTokenRoleAssertion: false,
@ -323,7 +338,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*do
&addAPIApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.mgmtAppID,
ID: setup.zitadel.mgmtAppID,
Name: mgmtAppName,
},
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
@ -335,7 +350,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*do
&addAPIApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.adminAppID,
ID: setup.zitadel.adminAppID,
Name: adminAppName,
},
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
@ -347,7 +362,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*do
&addAPIApp{
AddApp: AddApp{
Aggregate: *projectAgg,
ID: setup.Zitadel.authAppID,
ID: setup.zitadel.authAppID,
Name: authAppName,
},
AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT,
@ -356,25 +371,35 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (*do
),
AddOIDCAppCommand(console, nil),
SetIAMConsoleID(instanceAgg, &console.ClientID, &setup.Zitadel.consoleAppID),
SetIAMConsoleID(instanceAgg, &console.ClientID, &setup.zitadel.consoleAppID),
)
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...)
if err != nil {
return nil, err
return "", nil, err
}
events, err := c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
return "", nil, err
}
return &domain.ObjectDetails{
return instanceID, &domain.ObjectDetails{
Sequence: events[len(events)-1].Sequence(),
EventDate: events[len(events)-1].CreationDate(),
ResourceOwner: orgID,
}, nil
}
func addInstance(a *instance.Aggregate, instanceName string) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return []eventstore.Command{
instance.NewInstanceAddedEvent(ctx, &a.Aggregate, instanceName),
}, nil
}, nil
}
}
//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) {

View File

@ -67,6 +67,11 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri
}, nil
}
func (c *Commands) addGeneratedInstanceDomain(a *instance.Aggregate, instanceName string) preparation.Validation {
domain := domain.NewGeneratedInstanceDomain(instanceName, c.iamDomain)
return c.addInstanceDomain(a, domain, true)
}
func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain string, generated bool) preparation.Validation {
return func() (preparation.CreateCommands, error) {
if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" {
@ -80,10 +85,14 @@ 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")
}
events := []eventstore.Command{
instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated),
}
appWriteModel, err := c.getOIDCAppWriteModel(ctx, authz.GetInstance(ctx).ProjectID(), authz.GetInstance(ctx).ConsoleApplicationID(), "")
if err != nil {
return nil, err
}
if appWriteModel.State.Exists() {
redirectUrls := append(appWriteModel.RedirectUris, instanceDomain+consoleRedirectPath)
logoutUrls := append(appWriteModel.PostLogoutRedirectUris, instanceDomain+consolePostLogoutPath)
consoleChangeEvent, err := project.NewOIDCConfigChangedEvent(
@ -98,10 +107,10 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin
if err != nil {
return nil, err
}
return []eventstore.Command{
instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated),
consoleChangeEvent,
}, nil
events = append(events, consoleChangeEvent)
}
return events, nil
}, nil
}
}

View File

@ -291,7 +291,7 @@ func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID,
return err
}
if !app.State.Exists() {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-D6hba", "Errors.Project.App.NoExisting")
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-D6hba", "Errors.Project.App.NotExisting")
}
if !app.IsOIDC() {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-BHgn2", "Errors.Project.App.IsNotOIDC")

View File

@ -23,5 +23,7 @@ func (f InstanceDomainState) Exists() bool {
}
func NewGeneratedInstanceDomain(instanceName, iamDomain string) string {
//TODO: Add random number/string to be unique
instanceName = strings.TrimSpace(instanceName)
return strings.ToLower(strings.ReplaceAll(instanceName, " ", "-") + "." + iamDomain)
}

View File

@ -34,8 +34,12 @@ func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration
err = migration.Execute(ctx)
logging.OnError(err).Error("migration failed")
_, err = es.Push(ctx, setupDoneCmd(migration, err))
_, pushErr := es.Push(ctx, setupDoneCmd(migration, err))
logging.OnError(pushErr).Error("migration failed")
if err != nil {
return err
}
return pushErr
}
func shouldExec(ctx context.Context, es *eventstore.Eventstore, migration Migration) (should bool, err error) {

View File

@ -23,6 +23,14 @@ var (
name: projection.InstanceColumnID,
table: instanceTable,
}
InstanceColumnName = Column{
name: projection.InstanceColumnName,
table: instanceTable,
}
InstanceColumnCreationDate = Column{
name: projection.InstanceColumnCreationDate,
table: instanceTable,
}
InstanceColumnChangeDate = Column{
name: projection.InstanceColumnChangeDate,
table: instanceTable,
@ -64,6 +72,7 @@ var (
type Instance struct {
ID string
ChangeDate time.Time
CreationDate time.Time
Sequence uint64
GlobalOrgID string
@ -76,6 +85,11 @@ type Instance struct {
Host string
}
type Instances struct {
SearchResponse
Instances []*Instance
}
func (i *Instance) InstanceID() string {
return i.ID
}
@ -101,6 +115,14 @@ type InstanceSearchQueries struct {
Queries []SearchQuery
}
func NewInstanceIDsListSearchQuery(ids ...string) (SearchQuery, error) {
list := make([]interface{}, len(ids))
for i, value := range ids {
list[i] = value
}
return NewListQuery(InstanceColumnID, list, ListIn)
}
func (q *InstanceSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
query = q.SearchRequest.toQuery(query)
for _, q := range q.Queries {
@ -109,6 +131,24 @@ func (q *InstanceSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder
return query
}
func (q *Queries) SearchInstances(ctx context.Context, queries *InstanceSearchQueries) (instances *Instances, err error) {
query, scan := prepareInstancesQuery()
stmt, args, err := queries.toQuery(query).ToSql()
if err != nil {
return nil, errors.ThrowInvalidArgument(err, "QUERY-M9fow", "Errors.Query.SQLStatement")
}
rows, err := q.client.QueryContext(ctx, stmt, args...)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-3j98f", "Errors.Internal")
}
instances, err = scan(rows)
if err != nil {
return nil, err
}
return instances, err
}
func (q *Queries) Instance(ctx context.Context) (*Instance, error) {
stmt, scan := prepareInstanceQuery(authz.GetInstance(ctx).RequestedDomain())
query, args, err := stmt.Where(sq.Eq{
@ -146,6 +186,7 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag {
func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) {
return sq.Select(
InstanceColumnID.identifier(),
InstanceColumnCreationDate.identifier(),
InstanceColumnChangeDate.identifier(),
InstanceColumnSequence.identifier(),
InstanceColumnGlobalOrgID.identifier(),
@ -162,6 +203,7 @@ func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Insta
lang := ""
err := row.Scan(
&instance.ID,
&instance.CreationDate,
&instance.ChangeDate,
&instance.Sequence,
&instance.GlobalOrgID,
@ -182,3 +224,58 @@ func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Insta
return instance, nil
}
}
func prepareInstancesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) {
return sq.Select(
InstanceColumnID.identifier(),
InstanceColumnCreationDate.identifier(),
InstanceColumnChangeDate.identifier(),
InstanceColumnSequence.identifier(),
InstanceColumnGlobalOrgID.identifier(),
InstanceColumnProjectID.identifier(),
InstanceColumnConsoleID.identifier(),
InstanceColumnConsoleAppID.identifier(),
InstanceColumnSetupStarted.identifier(),
InstanceColumnSetupDone.identifier(),
InstanceColumnDefaultLanguage.identifier(),
countColumn.identifier(),
).From(instanceTable.identifier()).PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*Instances, error) {
instances := make([]*Instance, 0)
var count uint64
for rows.Next() {
instance := new(Instance)
lang := ""
//TODO: Get Host
err := rows.Scan(
&instance.ID,
&instance.CreationDate,
&instance.ChangeDate,
&instance.Sequence,
&instance.GlobalOrgID,
&instance.IAMProjectID,
&instance.ConsoleID,
&instance.ConsoleAppID,
&instance.SetupStarted,
&instance.SetupDone,
&lang,
&count,
)
if err != nil {
return nil, err
}
instances = append(instances, instance)
}
if err := rows.Close(); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-8nlWW", "Errors.Query.CloseRows")
}
return &Instances{
Instances: instances,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}

View File

@ -34,6 +34,7 @@ func Test_InstancePrepares(t *testing.T) {
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(`SELECT projections.instances.id,`+
` projections.instances.creation_date,`+
` projections.instances.change_date,`+
` projections.instances.sequence,`+
` projections.instances.global_org_id,`+
@ -64,6 +65,7 @@ func Test_InstancePrepares(t *testing.T) {
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(`SELECT projections.instances.id,`+
` projections.instances.creation_date,`+
` projections.instances.change_date,`+
` projections.instances.sequence,`+
` projections.instances.global_org_id,`+
@ -76,6 +78,7 @@ func Test_InstancePrepares(t *testing.T) {
` FROM projections.instances`),
[]string{
"id",
"creation_date",
"change_date",
"sequence",
"global_org_id",
@ -89,6 +92,7 @@ func Test_InstancePrepares(t *testing.T) {
[]driver.Value{
"id",
testNow,
testNow,
uint64(20211108),
"global-org-id",
"project-id",
@ -102,6 +106,7 @@ func Test_InstancePrepares(t *testing.T) {
},
object: &Instance{
ID: "id",
CreationDate: testNow,
ChangeDate: testNow,
Sequence: 20211108,
GlobalOrgID: "global-org-id",
@ -121,6 +126,7 @@ func Test_InstancePrepares(t *testing.T) {
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(`SELECT projections.instances.id,`+
` projections.instances.creation_date,`+
` projections.instances.change_date,`+
` projections.instances.sequence,`+
` projections.instances.global_org_id,`+

View File

@ -14,7 +14,9 @@ const (
InstanceProjectionTable = "projections.instances"
InstanceColumnID = "id"
InstanceColumnName = "name"
InstanceColumnChangeDate = "change_date"
InstanceColumnCreationDate = "creation_date"
InstanceColumnGlobalOrgID = "global_org_id"
InstanceColumnProjectID = "iam_project_id"
InstanceColumnConsoleID = "console_client_id"
@ -36,10 +38,13 @@ func NewInstanceProjection(ctx context.Context, config crdb.StatementHandlerConf
config.InitCheck = crdb.NewTableCheck(
crdb.NewTable([]*crdb.Column{
crdb.NewColumn(InstanceColumnID, crdb.ColumnTypeText),
crdb.NewColumn(InstanceColumnName, crdb.ColumnTypeText, crdb.Default("")),
crdb.NewColumn(InstanceColumnChangeDate, crdb.ColumnTypeTimestamp),
crdb.NewColumn(InstanceColumnCreationDate, crdb.ColumnTypeTimestamp),
crdb.NewColumn(InstanceColumnGlobalOrgID, crdb.ColumnTypeText, crdb.Default("")),
crdb.NewColumn(InstanceColumnProjectID, crdb.ColumnTypeText, crdb.Default("")),
crdb.NewColumn(InstanceColumnConsoleID, crdb.ColumnTypeText, crdb.Default("")),
crdb.NewColumn(InstanceColumnConsoleAppID, crdb.ColumnTypeText, crdb.Default("")),
crdb.NewColumn(InstanceColumnSequence, crdb.ColumnTypeInt64),
crdb.NewColumn(InstanceColumnSetUpStarted, crdb.ColumnTypeInt64, crdb.Default(0)),
crdb.NewColumn(InstanceColumnSetUpDone, crdb.ColumnTypeInt64, crdb.Default(0)),
@ -57,6 +62,10 @@ func (p *InstanceProjection) reducers() []handler.AggregateReducer {
{
Aggregate: instance.AggregateType,
EventRedusers: []handler.EventReducer{
{
Event: instance.InstanceAddedEventType,
Reduce: p.reduceInstanceAdded,
},
{
Event: instance.GlobalOrgSetEventType,
Reduce: p.reduceGlobalOrgSet,
@ -86,19 +95,38 @@ func (p *InstanceProjection) reducers() []handler.AggregateReducer {
}
}
func (p *InstanceProjection) reduceInstanceAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*instance.InstanceAddedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-29nlS", "reduce.wrong.event.type %s", instance.InstanceAddedEventType)
}
return crdb.NewCreateStatement(
e,
[]handler.Column{
handler.NewCol(InstanceColumnID, e.Aggregate().InstanceID),
handler.NewCol(InstanceColumnCreationDate, e.CreationDate()),
handler.NewCol(InstanceColumnChangeDate, e.CreationDate()),
handler.NewCol(InstanceColumnSequence, e.Sequence()),
handler.NewCol(InstanceColumnName, e.Name),
},
), nil
}
func (p *InstanceProjection) reduceGlobalOrgSet(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*instance.GlobalOrgSetEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-2n9f2", "reduce.wrong.event.type %s", instance.GlobalOrgSetEventType)
}
return crdb.NewUpsertStatement(
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(InstanceColumnID, e.Aggregate().InstanceID),
handler.NewCol(InstanceColumnChangeDate, e.CreationDate()),
handler.NewCol(InstanceColumnSequence, e.Sequence()),
handler.NewCol(InstanceColumnGlobalOrgID, e.OrgID),
},
[]handler.Condition{
handler.NewCond(InstanceColumnID, e.Aggregate().InstanceID),
},
), nil
}
@ -107,14 +135,16 @@ func (p *InstanceProjection) reduceIAMProjectSet(event eventstore.Event) (*handl
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-30o0e", "reduce.wrong.event.type %s", instance.ProjectSetEventType)
}
return crdb.NewUpsertStatement(
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(InstanceColumnID, e.Aggregate().InstanceID),
handler.NewCol(InstanceColumnChangeDate, e.CreationDate()),
handler.NewCol(InstanceColumnSequence, e.Sequence()),
handler.NewCol(InstanceColumnProjectID, e.ProjectID),
},
[]handler.Condition{
handler.NewCond(InstanceColumnID, e.Aggregate().InstanceID),
},
), nil
}
@ -123,15 +153,17 @@ func (p *InstanceProjection) reduceConsoleSet(event eventstore.Event) (*handler.
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Dgf11", "reduce.wrong.event.type %s", instance.ConsoleSetEventType)
}
return crdb.NewUpsertStatement(
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(InstanceColumnID, e.Aggregate().InstanceID),
handler.NewCol(InstanceColumnChangeDate, e.CreationDate()),
handler.NewCol(InstanceColumnSequence, e.Sequence()),
handler.NewCol(InstanceColumnConsoleID, e.ClientID),
handler.NewCol(InstanceColumnConsoleAppID, e.AppID),
},
[]handler.Condition{
handler.NewCond(InstanceColumnID, e.Aggregate().InstanceID),
},
), nil
}
@ -140,14 +172,16 @@ func (p *InstanceProjection) reduceDefaultLanguageSet(event eventstore.Event) (*
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-30o0e", "reduce.wrong.event.type %s", instance.DefaultLanguageSetEventType)
}
return crdb.NewUpsertStatement(
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(InstanceColumnID, e.Aggregate().InstanceID),
handler.NewCol(InstanceColumnChangeDate, e.CreationDate()),
handler.NewCol(InstanceColumnSequence, e.Sequence()),
handler.NewCol(InstanceColumnDefaultLanguage, e.Language.String()),
},
[]handler.Condition{
handler.NewCond(InstanceColumnID, e.Aggregate().InstanceID),
},
), nil
}

View File

@ -57,7 +57,7 @@ func (p *InstanceDomainProjection) reducers() []handler.AggregateReducer {
Reduce: p.reduceDomainAdded,
},
{
Event: instance.InstanceDomainAddedEventType,
Event: instance.InstanceDomainPrimarySetEventType,
Reduce: p.reduceDomainPrimarySet,
},
{

View File

@ -20,7 +20,37 @@ func TestInstanceProjection_reduces(t *testing.T) {
args args
reduce func(event eventstore.Event) (*handler.Statement, error)
want wantReduce
}{
}{{
name: "reduceInstanceAdded",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.InstanceAddedEventType),
instance.AggregateType,
[]byte(`{"name": "Name"}`),
), instance.InstanceAddedEventMapper),
},
reduce: (&InstanceProjection{}).reduceInstanceAdded,
want: wantReduce{
projection: InstanceProjectionTable,
aggregateType: eventstore.AggregateType("instance"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.instances (id, creation_date, change_date, sequence, name) VALUES ($1, $2, $3, $4, $5)",
expectedArgs: []interface{}{
"instance-id",
anyArg{},
anyArg{},
uint64(15),
"Name",
},
},
},
},
},
},
{
name: "reduceGlobalOrgSet",
args: args{
@ -39,12 +69,12 @@ func TestInstanceProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPSERT INTO projections.instances (id, change_date, sequence, global_org_id) VALUES ($1, $2, $3, $4)",
expectedStmt: "UPDATE projections.instances SET (change_date, sequence, global_org_id) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
"instance-id",
anyArg{},
uint64(15),
"orgid",
"instance-id",
},
},
},
@ -69,12 +99,12 @@ func TestInstanceProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPSERT INTO projections.instances (id, change_date, sequence, iam_project_id) VALUES ($1, $2, $3, $4)",
expectedStmt: "UPDATE projections.instances SET (change_date, sequence, iam_project_id) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
"instance-id",
anyArg{},
uint64(15),
"project-id",
"instance-id",
},
},
},
@ -99,12 +129,12 @@ func TestInstanceProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPSERT INTO projections.instances (id, change_date, sequence, default_language) VALUES ($1, $2, $3, $4)",
expectedStmt: "UPDATE projections.instances SET (change_date, sequence, default_language) = ($1, $2, $3) WHERE (id = $4)",
expectedArgs: []interface{}{
"instance-id",
anyArg{},
uint64(15),
"en",
"instance-id",
},
},
},

View File

@ -96,7 +96,7 @@ func NewDomainPrimarySetEvent(ctx context.Context, aggregate *eventstore.Aggrega
}
func DomainPrimarySetEventMapper(event *repository.Event) (eventstore.Event, error) {
orgDomainAdded := &DomainAddedEvent{
orgDomainAdded := &DomainPrimarySetEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := json.Unmarshal(event.Data, orgDomainAdded)

View File

@ -2525,34 +2525,6 @@ service AdminService {
};
}
//Truncates the delta of the change stream
// be carefull with this function because ZITADEL has to
// recompute the deltas after they got cleared.
// Search requests will return wrong results until all deltas are recomputed
rpc ClearView(ClearViewRequest) returns (ClearViewResponse) {
option (google.api.http) = {
post: "/views/{database}/{view_name}";
};
option (zitadel.v1.auth_option) = {
permission: "iam.write";
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags: "views";
external_docs: {
url: "https://docs.zitadel.ch/concepts#Software_Architecture";
description: "details of ZITADEL's event driven software concepts";
};
responses: {
key: "200";
value: {
description: "View cleared";
};
};
};
}
//Returns event descriptions which cannot be processed.
// It's possible that some events need some retries.
// For example if the SMTP-API wasn't able to send an email at the first time
@ -4512,34 +4484,6 @@ message ListViewsResponse {
repeated View result = 1;
}
message ClearViewRequest {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
json_schema: {
required: ["database", "view_name"]
};
};
string database = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"adminapi\"";
min_length: 1;
max_length: 200;
}
];
string view_name = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"iam_members\"";
min_length: 1;
max_length: 200;
}
];
}
//This is an empty response
message ClearViewResponse {}
//This is an empty request
message ListFailedEventsRequest {}

View File

@ -25,11 +25,6 @@ message Instance {
example: "\"ZITADEL\"";
}
];
string version = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"v1.0.0\"";
}
];
}
enum State {
@ -44,31 +39,19 @@ message Query {
oneof query {
option (validate.required) = true;
IdQuery id_query = 1;
StateQuery state_query = 2;
IdsQuery id_query = 1;
}
}
//IdQuery is always equals
message IdQuery {
string id = 1 [
(validate.rules).string = {max_len: 200},
message IdsQuery {
repeated string ids = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "4820840938402429";
}
];
}
//StateQuery is always equals
message StateQuery {
State state = 1 [
(validate.rules).enum.defined_only = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "current state of the instance";
}
];
}
enum FieldName {
FIELD_NAME_UNSPECIFIED = 0;
FIELD_NAME_ID = 1;

View File

@ -105,7 +105,7 @@ service SystemService {
// Returns a list of ZITADEL instances/tenants
rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) {
option (google.api.http) = {
post: "/instances"
post: "/instances/_search"
body: "*"
};
}
@ -134,17 +134,11 @@ service SystemService {
};
}
// Returns the usage metrics of an instance
rpc GetUsage(GetUsageRequest) returns (GetUsageResponse) {
option (google.api.http) = {
get: "/instances/{id}/usage";
};
}
// Returns the custom domains of an instance
rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) {
option (google.api.http) = {
get: "/instances/{id}/domains";
post: "/instances/{id}/domains/_search";
body: "*"
};
}
@ -178,6 +172,7 @@ service SystemService {
rpc ListViews(ListViewsRequest) returns (ListViewsResponse) {
option (google.api.http) = {
post: "/views/_search";
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
@ -229,6 +224,7 @@ service SystemService {
rpc ListFailedEvents(ListFailedEventsRequest) returns (ListFailedEventsResponse) {
option (google.api.http) = {
post: "/failedevents/_search";
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
@ -322,19 +318,19 @@ message GetInstanceResponse {
message AddInstanceRequest {
string instance_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
string first_org_name = 2 [(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}];
string owner_first_name = 4 [(validate.rules).string = {min_len: 1, max_len: 200}];
string owner_last_name = 5 [(validate.rules).string = {min_len: 1, max_len: 200}];
string owner_first_name = 4 [(validate.rules).string = {max_len: 200}];
string owner_last_name = 5 [(validate.rules).string = {max_len: 200}];
string owner_email = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
string owner_username = 7 [(validate.rules).string = {min_len: 1, max_len: 200}];
string password = 8 [(validate.rules).string = {min_len: 1, max_len: 200}];
uint64 request_limit = 9;
uint64 action_mins_limit = 10;
string owner_username = 7 [(validate.rules).string = {max_len: 200}];
uint64 request_limit = 8;
uint64 action_mins_limit = 9;
}
message AddInstanceResponse {
string id = 1;
zitadel.v1.ObjectDetails details = 2;
}
message RemoveInstanceRequest {