diff --git a/.gitignore b/.gitignore index b5f00620af..9f5525c78a 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/cmd/admin/setup/03.go b/cmd/admin/setup/03.go index 76ba6b31c9..248f861755 100644 --- a/cmd/admin/setup/03.go +++ b/cmd/admin/setup/03.go @@ -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 } diff --git a/cmd/admin/setup/config.go b/cmd/admin/setup/config.go index 75676484a5..57c520ac9e 100644 --- a/cmd/admin/setup/config.go +++ b/cmd/admin/setup/config.go @@ -4,6 +4,7 @@ import ( "bytes" "github.com/caos/logging" + "github.com/caos/zitadel/internal/command" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" @@ -15,20 +16,27 @@ import ( ) type Config struct { - Database database.Config - SystemDefaults systemdefaults.SystemDefaults - InternalAuthZ authz.Config - ExternalPort uint16 - ExternalDomain string - ExternalSecure bool - Log *logging.Config - EncryptionKeys *encryptionKeyConfig + Database database.Config + SystemDefaults systemdefaults.SystemDefaults + InternalAuthZ authz.Config + ExternalPort uint16 + ExternalDomain string + 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") diff --git a/cmd/admin/setup/setup.go b/cmd/admin/setup/setup.go index 6604958985..dd05bd3c2d 100644 --- a/cmd/admin/setup/setup.go +++ b/cmd/admin/setup/setup.go @@ -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") } diff --git a/cmd/admin/setup/steps.yaml b/cmd/admin/setup/steps.yaml index f391897ad8..05b1131978 100644 --- a/cmd/admin/setup/steps.yaml +++ b/cmd/admin/setup/steps.yaml @@ -1,5 +1,7 @@ S3DefaultInstance: InstanceSetup: + InstanceName: Localhost + CustomDomain: localhost Org: Name: ZITADEL Human: @@ -8,7 +10,7 @@ S3DefaultInstance: LastName: Admin NickName: DisplayName: - Email: + Email: Address: #autogenerated if empty. uses domain from config and prefixes admin@. for example: admin@domain.tdl Verified: true PreferredLanguage: @@ -17,200 +19,3 @@ S3DefaultInstance: Number: Verified: Password: Password1! - SecretGenerators: - PasswordSaltCost: 14 - ClientSecret: - Length: 64 - IncludeLowerLetters: true - IncludeUpperLetters: true - IncludeDigits: true - IncludeSymbols: false - InitializeUserCode: - Length: 6 - Expiry: '72h' - IncludeLowerLetters: false - IncludeUpperLetters: true - IncludeDigits: true - IncludeSymbols: false - EmailVerificationCode: - Length: 6 - Expiry: '1h' - IncludeLowerLetters: false - IncludeUpperLetters: true - IncludeDigits: true - IncludeSymbols: false - PhoneVerificationCode: - Length: 6 - Expiry: '1h' - IncludeLowerLetters: false - IncludeUpperLetters: true - IncludeDigits: true - IncludeSymbols: false - PasswordVerificationCode: - Length: 6 - Expiry: '1h' - IncludeLowerLetters: false - IncludeUpperLetters: true - IncludeDigits: true - IncludeSymbols: false - PasswordlessInitCode: - Length: 12 - Expiry: '1h' - IncludeLowerLetters: true - IncludeUpperLetters: true - IncludeDigits: true - IncludeSymbols: false - DomainVerification: - Length: 32 - IncludeLowerLetters: true - IncludeUpperLetters: true - IncludeDigits: true - IncludeSymbols: false - Features: - TierName: Default Tier - TierDescription: "" - State: 1 #active - StateDescription: "" - Retention: 8760h #1year - LoginPolicyFactors: true - LoginPolicyIDP: true - LoginPolicyPasswordless: true - LoginPolicyRegistration: true - LoginPolicyUsernameLogin: true - LoginPolicyPasswordReset: true - PasswordComplexityPolicy: true - LabelPolicyPrivateLabel: true - LabelPolicyWatermark: true - CustomDomain: true - PrivacyPolicy: true - MetadataUser: true - CustomTextMessage: true - CustomTextLogin: true - LockoutPolicy: true - ActionsAllowed: 2 #ActionsAllowedUnlimited - MaxActions: #not necessary because of ActionsAllowedUnlimited - PasswordComplexityPolicy: - MinLength: 8 - HasLowercase: true - HasUppercase: true - HasNumber: true - HasSymbol: true - PasswordAgePolicy: - ExpireWarnDays: 0 - MaxAgeDays: 0 - DomainPolicy: - UserLoginMustBeDomain: true - ValidateOrgDomains: true - LoginPolicy: - AllowUsernamePassword: true - AllowRegister: true - AllowExternalIDP: true - ForceMFA: false - HidePasswordReset: false - PasswordlessType: 1 #1: allowed 0: not allowed - PasswordCheckLifetime: 240h #10d - ExternalLoginCheckLifetime: 240h #10d - MfaInitSkipLifetime: 720h #30d - SecondFactorCheckLifetime: 18h - MultiFactorCheckLifetime: 12h - PrivacyPolicy: - TOSLink: https://docs.zitadel.ch/docs/legal/terms-of-service - PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy - HelpLink: '' - LabelPolicy: - PrimaryColor: '#5469d4' - BackgroundColor: '#fafafa' - WarnColor: '#f44336' - FontColor: '#000000' - PrimaryColorDark: '#5469d4' - BackgroundColorDark: '#212121' - WarnColorDark: '#f44336' - FontColorDark: '#ffffff' - HideLoginNameSuffix: false - ErrorMsgPopup: false - DisableWatermark: false - LockoutPolicy: - MaxAttempts: 0 - ShouldShowLockoutFailure: true - EmailTemplate: CjwhZG9jdHlwZSBodG1sPgo8aHRtbCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+CjxoZWFkPgogIDx0aXRsZT4KCiAgPC90aXRsZT4KICA8IS0tW2lmICFtc29dPjwhLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICA8IS0tPCFbZW5kaWZdLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgICNvdXRsb29rIGEgeyBwYWRkaW5nOjA7IH0KICAgIGJvZHkgeyBtYXJnaW46MDtwYWRkaW5nOjA7LXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OjEwMCU7LW1zLXRleHQtc2l6ZS1hZGp1c3Q6MTAwJTsgfQogICAgdGFibGUsIHRkIHsgYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO21zby10YWJsZS1sc3BhY2U6MHB0O21zby10YWJsZS1yc3BhY2U6MHB0OyB9CiAgICBpbWcgeyBib3JkZXI6MDtoZWlnaHQ6YXV0bztsaW5lLWhlaWdodDoxMDAlOyBvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7LW1zLWludGVycG9sYXRpb24tbW9kZTpiaWN1YmljOyB9CiAgICBwIHsgZGlzcGxheTpibG9jazttYXJnaW46MTNweCAwOyB9CiAgPC9zdHlsZT4KICA8IS0tW2lmIG1zb10+CiAgPHhtbD4KICAgIDxvOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgICAgIDxvOkFsbG93UE5HLz4KICAgICAgPG86UGl4ZWxzUGVySW5jaD45NjwvbzpQaXhlbHNQZXJJbmNoPgogICAgPC9vOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgPC94bWw+CiAgPCFbZW5kaWZdLS0+CiAgPCEtLVtpZiBsdGUgbXNvIDExXT4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLm1qLW91dGxvb2stZ3JvdXAtZml4IHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyB9CiAgPC9zdHlsZT4KICA8IVtlbmRpZl0tLT4KCiAgPCEtLVtpZiAhbXNvXT48IS0tPgogIDxsaW5rIGhyZWY9Imh0dHBzOi8vZm9udHMuZ29vZ2xlYXBpcy5jb20vY3NzP2ZhbWlseT1VYnVudHU6MzAwLDQwMCw1MDAsNzAwIiByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIj4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgQGltcG9ydCB1cmwoaHR0cHM6Ly9mb250cy5nb29nbGVhcGlzLmNvbS9jc3M/ZmFtaWx5PVVidW50dTozMDAsNDAwLDUwMCw3MDApOwogIDwvc3R5bGU+CiAgPCEtLTwhW2VuZGlmXS0tPgoKCgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4td2lkdGg6NDgwcHgpIHsKICAgICAgLm1qLWNvbHVtbi1wZXItMTAwIHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IH0KICAgICAgLm1qLWNvbHVtbi1wZXItNjAgeyB3aWR0aDo2MCUgIWltcG9ydGFudDsgbWF4LXdpZHRoOiA2MCU7IH0KICAgIH0KICA8L3N0eWxlPgoKCiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCgoKICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDo0ODBweCkgewogICAgICB0YWJsZS5tai1mdWxsLXdpZHRoLW1vYmlsZSB7IHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7IH0KICAgICAgdGQubWotZnVsbC13aWR0aC1tb2JpbGUgeyB3aWR0aDogYXV0byAhaW1wb3J0YW50OyB9CiAgICB9CgogIDwvc3R5bGU+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc2hhZG93IGEgewogICAgYm94LXNoYWRvdzogMHB4IDNweCAxcHggLTJweCByZ2JhKDAsIDAsIDAsIDAuMiksIDBweCAycHggMnB4IDBweCByZ2JhKDAsIDAsIDAsIDAuMTQpLCAwcHggMXB4IDVweCAwcHggcmdiYSgwLCAwLCAwLCAwLjEyKTsKICB9PC9zdHlsZT4KCiAge3tpZiAuRm9udFVSTH19CiAgPHN0eWxlPgogICAgQGZvbnQtZmFjZSB7CiAgICAgIGZvbnQtZmFtaWx5OiAne3suRm9udEZhbWlseX19JzsKICAgICAgZm9udC1zdHlsZTogbm9ybWFsOwogICAgICBmb250LWRpc3BsYXk6IHN3YXA7CiAgICAgIHNyYzogdXJsKHt7LkZvbnRVUkx9fSk7CiAgICB9CiAgPC9zdHlsZT4KICB7e2VuZH19Cgo8L2hlYWQ+Cjxib2R5IHN0eWxlPSJ3b3JkLXNwYWNpbmc6bm9ybWFsOyI+CgoKPGRpdgogICAgICAgIHN0eWxlPSIiCj4KCiAgPHRhYmxlCiAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYmFja2dyb3VuZDp7ey5CYWNrZ3JvdW5kQ29sb3J9fTtiYWNrZ3JvdW5kLWNvbG9yOnt7LkJhY2tncm91bmRDb2xvcn19O3dpZHRoOjEwMCU7Ym9yZGVyLXJhZGl1czoxNnB4OyIKICA+CiAgICA8dGJvZHk+CiAgICA8dHI+CiAgICAgIDx0ZD4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgIDxkaXYgIHN0eWxlPSJtYXJnaW46MHB4IGF1dG87Ym9yZGVyLXJhZGl1czoxNnB4O21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTtib3JkZXItcmFkaXVzOjE2cHg7IgogICAgICAgICAgPgogICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJkaXJlY3Rpb246bHRyO2ZvbnQtc2l6ZTowcHg7cGFkZGluZzoyMHB4IDA7cGFkZGluZy1sZWZ0OjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHdpZHRoPSI4MDBweCIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgIDx0ZD4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiB3aWR0aD0iODAwIiA+PHRyPjx0ZCBzdHlsZT0ibGluZS1oZWlnaHQ6MHB4O2ZvbnQtc2l6ZTowcHg7bXNvLWxpbmUtaGVpZ2h0LXJ1bGU6ZXhhY3RseTsiPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MDt0ZXh0LWFsaWduOmNlbnRlcjsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MDtsaW5lLWhlaWdodDowO3RleHQtYWxpZ246bGVmdDtkaXNwbGF5OmlubGluZS1ibG9jazt3aWR0aDoxMDAlO2RpcmVjdGlvbjpsdHI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iID48dHI+PHRkIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6ODAwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJtai1jb2x1bW4tcGVyLTEwMCBtai1vdXRsb29rLWdyb3VwLWZpeCIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7dGV4dC1hbGlnbjpsZWZ0O2RpcmVjdGlvbjpsdHI7ZGlzcGxheTppbmxpbmUtYmxvY2s7dmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgd2lkdGg9IjEwMCUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IiIgd2lkdGg9IjEwMCUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6NTBweCAwIDMwcHggMDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTtib3JkZXItc3BhY2luZzowcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgIHN0eWxlPSJ3aWR0aDoxODBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpbWcKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaGVpZ2h0PSJhdXRvIiBzcmM9Int7LkxvZ29VUkx9fSIgc3R5bGU9ImJvcmRlcjowO2JvcmRlci1yYWRpdXM6OHB4O2Rpc3BsYXk6YmxvY2s7b3V0bGluZTpub25lO3RleHQtZGVjb3JhdGlvbjpub25lO2hlaWdodDphdXRvO3dpZHRoOjEwMCU7Zm9udC1zaXplOjEzcHg7IiB3aWR0aD0iMTgwIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLz4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48dHI+PHRkIGNsYXNzPSIiIHdpZHRoPSI4MDBweCIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgIDx0ZD4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiB3aWR0aD0iODAwIiA+PHRyPjx0ZCBzdHlsZT0ibGluZS1oZWlnaHQ6MHB4O2ZvbnQtc2l6ZTowcHg7bXNvLWxpbmUtaGVpZ2h0LXJ1bGU6ZXhhY3RseTsiPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MDt0ZXh0LWFsaWduOmNlbnRlcjsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6NDgwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9Im1qLWNvbHVtbi1wZXItNjAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkICBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3BhZGRpbmc6MDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSIiIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MjRweDtmb250LXdlaWdodDo1MDA7bGluZS1oZWlnaHQ6MTt0ZXh0LWFsaWduOmNlbnRlcjtjb2xvcjp7ey5Gb250Q29sb3J9fTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPnt7LkdyZWV0aW5nfX08L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7d29yZC1icmVhazpicmVhay13b3JkOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZm9udC1mYW1pbHk6e3suRm9udEZhbWlseX19O2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OmxpZ2h0O2xpbmUtaGVpZ2h0OjEuNTt0ZXh0LWFsaWduOmNlbnRlcjtjb2xvcjp7ey5Gb250Q29sb3J9fTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPnt7LlRleHR9fTwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgdmVydGljYWwtYWxpZ249Im1pZGRsZSIgY2xhc3M9InNoYWRvdyIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7d29yZC1icmVhazpicmVhay13b3JkOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJib3JkZXItY29sbGFwc2U6c2VwYXJhdGU7bGluZS1oZWlnaHQ6MTAwJTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBiZ2NvbG9yPSJ7ey5QcmltYXJ5Q29sb3J9fSIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyOm5vbmU7Ym9yZGVyLXJhZGl1czo2cHg7Y3Vyc29yOmF1dG87bXNvLXBhZGRpbmctYWx0OjEwcHggMjVweDtiYWNrZ3JvdW5kOnt7LlByaW1hcnlDb2xvcn19OyIgdmFsaWduPSJtaWRkbGUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaHJlZj0ie3suVVJMfX0iIHJlbD0ibm9vcGVuZXIgbm9yZWZlcnJlciIgc3R5bGU9ImRpc3BsYXk6aW5saW5lLWJsb2NrO2JhY2tncm91bmQ6e3suUHJpbWFyeUNvbG9yfX07Y29sb3I6I2ZmZmZmZjtmb250LWZhbWlseTpVYnVudHUsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWY7Zm9udC1zaXplOjE0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjEyMCU7bWFyZ2luOjA7dGV4dC1kZWNvcmF0aW9uOm5vbmU7dGV4dC10cmFuc2Zvcm06bm9uZTtwYWRkaW5nOjEwcHggMjVweDttc28tcGFkZGluZy1hbHQ6MHB4O2JvcmRlci1yYWRpdXM6NnB4OyIgdGFyZ2V0PSJfYmxhbmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3suQnV0dG9uVGV4dH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5JbmNsdWRlRm9vdGVyfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7cGFkZGluZy10b3A6MjBweDtwYWRkaW5nLXJpZ2h0OjIwcHg7cGFkZGluZy1ib3R0b206MjBweDtwYWRkaW5nLWxlZnQ6MjBweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iYm9yZGVyLXRvcDpzb2xpZCAycHggI2RiZGJkYjtmb250LXNpemU6MXB4O21hcmdpbjowcHggYXV0bzt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHN0eWxlPSJib3JkZXItdG9wOnNvbGlkIDJweCAjZGJkYmRiO2ZvbnQtc2l6ZToxcHg7bWFyZ2luOjBweCBhdXRvO3dpZHRoOjQ0MHB4OyIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iNDQwcHgiID48dHI+PHRkIHN0eWxlPSJoZWlnaHQ6MDtsaW5lLWhlaWdodDowOyI+ICZuYnNwOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxNnB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTNweDtsaW5lLWhlaWdodDoxO3RleHQtYWxpZ246Y2VudGVyO2NvbG9yOnt7LkZvbnRDb2xvcn19OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+e3suRm9vdGVyVGV4dH19PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgogICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICA8L2Rpdj4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgIDwvdGQ+CiAgICA8L3RyPgogICAgPC90Ym9keT4KICA8L3RhYmxlPgoKPC9kaXY+Cgo8L2JvZHk+CjwvaHRtbD4KICA= - MessageTexts: - - MessageTextType: InitCode - Language: de - Title: Zitadel - User initialisieren - PreHeader: User initialisieren - Subject: User initialisieren - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Dieser Benutzer wurde soeben im Zitadel erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren. - ButtonText: Initialisierung abschliessen - - MessageTextType: PasswordReset - Language: de - Title: Zitadel - Passwort zurücksetzen - PreHeader: Passwort zurücksetzen - Subject: Passwort zurücksetzen - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren. - ButtonText: Passwort zurücksetzen - - MessageTextType: VerifyEmail - Language: de - Title: Zitadel - Email verifizieren - PreHeader: Email verifizieren - Subject: Email verifizieren - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren <br>(Code <strong>{{.Code}}</strong>).<br> Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren. - ButtonText: Email verifizieren - - MessageTextType: VerifyPhone - Language: de - Title: Zitadel - Telefonnummer verifizieren - PreHeader: Telefonnummer verifizieren - Subject: Telefonnummer verifizieren - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst (Code {{.Code}}) - ButtonText: Telefon verifizieren - - MessageTextType: DomainClaimed - Language: de - Title: Zitadel - Domain wurde beansprucht - PreHeader: Email / Username ändern - Subject: Domain wurde beansprucht - Greeting: Hallo {{.FirstName}} {{.LastName}}, - Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt. - ButtonText: Login - - MessageTextType: InitCode - Language: en - Title: Zitadel - Initialize User - PreHeader: Initialize User - Subject: Initialize User - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: This user was created in Zitadel. Use the username {{.PreferredLoginName}} to login. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. - ButtonText: Finish initialization - - MessageTextType: PasswordReset - Language: en - Title: Zitadel - Reset password - PreHeader: Reset password - Subject: Reset password - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: We received a password reset request. Please use the button below to reset your password. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. - ButtonText: Reset password - - MessageTextType: VerifyEmail - Language: en - Title: Zitadel - Verify email - PreHeader: Verify email - Subject: Verify email - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email. - ButtonText: Verify email - - MessageTextType: VerifyPhone - Language: en - Title: Zitadel - Verify phone - PreHeader: Verify phone - Subject: Verify phone - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}. - ButtonText: Verify phone - - MessageTextType: DomainClaimed - Language: en - Title: Zitadel - Domain has been claimed - PreHeader: Change email / username - Subject: Domain has been claimed - Greeting: Hello {{.FirstName}} {{.LastName}}, - Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.UserName}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login. - ButtonText: Login diff --git a/cmd/admin/start/config.go b/cmd/admin/start/config.go index be4f0a5b24..63e8c6e045 100644 --- a/cmd/admin/start/config.go +++ b/cmd/admin/start/config.go @@ -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") diff --git a/cmd/admin/start/start.go b/cmd/admin/start/start.go index 7b89d657e2..8e9baa78be 100644 --- a/cmd/admin/start/start.go +++ b/cmd/admin/start/start.go @@ -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 } diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 5f25a723fa..00cb1f9802 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -184,6 +184,223 @@ SystemDefaults: SigningKeyRotationCheck: 10s SigningKeyGracefulPeriod: 10m +DefaultInstance: + InstanceName: + Org: + Name: + Human: + UserName: zitadel-admin + FirstName: ZITADEL + LastName: Admin + NickName: + DisplayName: + Email: + Address: + Verified: false + PreferredLanguage: + Gender: + Phone: + Number: + Verified: + Password: + SecretGenerators: + PasswordSaltCost: 14 + ClientSecret: + Length: 64 + IncludeLowerLetters: true + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + InitializeUserCode: + Length: 6 + Expiry: '72h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + EmailVerificationCode: + Length: 6 + Expiry: '1h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + PhoneVerificationCode: + Length: 6 + Expiry: '1h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + PasswordVerificationCode: + Length: 6 + Expiry: '1h' + IncludeLowerLetters: false + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + PasswordlessInitCode: + Length: 12 + Expiry: '1h' + IncludeLowerLetters: true + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + DomainVerification: + Length: 32 + IncludeLowerLetters: true + IncludeUpperLetters: true + IncludeDigits: true + IncludeSymbols: false + Features: + TierName: Default Tier + TierDescription: "" + State: 1 #active + StateDescription: "" + Retention: 8760h #1year + LoginPolicyFactors: true + LoginPolicyIDP: true + LoginPolicyPasswordless: true + LoginPolicyRegistration: true + LoginPolicyUsernameLogin: true + LoginPolicyPasswordReset: true + PasswordComplexityPolicy: true + LabelPolicyPrivateLabel: true + LabelPolicyWatermark: true + CustomDomain: true + PrivacyPolicy: true + MetadataUser: true + CustomTextMessage: true + CustomTextLogin: true + LockoutPolicy: true + ActionsAllowed: 2 #ActionsAllowedUnlimited + MaxActions: #not necessary because of ActionsAllowedUnlimited + PasswordComplexityPolicy: + MinLength: 8 + HasLowercase: true + HasUppercase: true + HasNumber: true + HasSymbol: true + PasswordAgePolicy: + ExpireWarnDays: 0 + MaxAgeDays: 0 + DomainPolicy: + UserLoginMustBeDomain: true + ValidateOrgDomains: true + LoginPolicy: + AllowUsernamePassword: true + AllowRegister: true + AllowExternalIDP: true + ForceMFA: false + HidePasswordReset: false + PasswordlessType: 1 #1: allowed 0: not allowed + PasswordCheckLifetime: 240h #10d + ExternalLoginCheckLifetime: 240h #10d + MfaInitSkipLifetime: 720h #30d + SecondFactorCheckLifetime: 18h + MultiFactorCheckLifetime: 12h + PrivacyPolicy: + TOSLink: https://docs.zitadel.ch/docs/legal/terms-of-service + PrivacyLink: https://docs.zitadel.ch/docs/legal/privacy-policy + HelpLink: '' + LabelPolicy: + PrimaryColor: '#5469d4' + BackgroundColor: '#fafafa' + WarnColor: '#f44336' + FontColor: '#000000' + PrimaryColorDark: '#5469d4' + BackgroundColorDark: '#212121' + WarnColorDark: '#f44336' + FontColorDark: '#ffffff' + HideLoginNameSuffix: false + ErrorMsgPopup: false + DisableWatermark: false + LockoutPolicy: + MaxAttempts: 0 + ShouldShowLockoutFailure: true + EmailTemplate: CjwhZG9jdHlwZSBodG1sPgo8aHRtbCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+CjxoZWFkPgogIDx0aXRsZT4KCiAgPC90aXRsZT4KICA8IS0tW2lmICFtc29dPjwhLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICA8IS0tPCFbZW5kaWZdLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgICNvdXRsb29rIGEgeyBwYWRkaW5nOjA7IH0KICAgIGJvZHkgeyBtYXJnaW46MDtwYWRkaW5nOjA7LXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OjEwMCU7LW1zLXRleHQtc2l6ZS1hZGp1c3Q6MTAwJTsgfQogICAgdGFibGUsIHRkIHsgYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO21zby10YWJsZS1sc3BhY2U6MHB0O21zby10YWJsZS1yc3BhY2U6MHB0OyB9CiAgICBpbWcgeyBib3JkZXI6MDtoZWlnaHQ6YXV0bztsaW5lLWhlaWdodDoxMDAlOyBvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7LW1zLWludGVycG9sYXRpb24tbW9kZTpiaWN1YmljOyB9CiAgICBwIHsgZGlzcGxheTpibG9jazttYXJnaW46MTNweCAwOyB9CiAgPC9zdHlsZT4KICA8IS0tW2lmIG1zb10+CiAgPHhtbD4KICAgIDxvOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgICAgIDxvOkFsbG93UE5HLz4KICAgICAgPG86UGl4ZWxzUGVySW5jaD45NjwvbzpQaXhlbHNQZXJJbmNoPgogICAgPC9vOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgPC94bWw+CiAgPCFbZW5kaWZdLS0+CiAgPCEtLVtpZiBsdGUgbXNvIDExXT4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLm1qLW91dGxvb2stZ3JvdXAtZml4IHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyB9CiAgPC9zdHlsZT4KICA8IVtlbmRpZl0tLT4KCiAgPCEtLVtpZiAhbXNvXT48IS0tPgogIDxsaW5rIGhyZWY9Imh0dHBzOi8vZm9udHMuZ29vZ2xlYXBpcy5jb20vY3NzP2ZhbWlseT1VYnVudHU6MzAwLDQwMCw1MDAsNzAwIiByZWw9InN0eWxlc2hlZXQiIHR5cGU9InRleHQvY3NzIj4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgQGltcG9ydCB1cmwoaHR0cHM6Ly9mb250cy5nb29nbGVhcGlzLmNvbS9jc3M/ZmFtaWx5PVVidW50dTozMDAsNDAwLDUwMCw3MDApOwogIDwvc3R5bGU+CiAgPCEtLTwhW2VuZGlmXS0tPgoKCgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4td2lkdGg6NDgwcHgpIHsKICAgICAgLm1qLWNvbHVtbi1wZXItMTAwIHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IH0KICAgICAgLm1qLWNvbHVtbi1wZXItNjAgeyB3aWR0aDo2MCUgIWltcG9ydGFudDsgbWF4LXdpZHRoOiA2MCU7IH0KICAgIH0KICA8L3N0eWxlPgoKCiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCgoKICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDo0ODBweCkgewogICAgICB0YWJsZS5tai1mdWxsLXdpZHRoLW1vYmlsZSB7IHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7IH0KICAgICAgdGQubWotZnVsbC13aWR0aC1tb2JpbGUgeyB3aWR0aDogYXV0byAhaW1wb3J0YW50OyB9CiAgICB9CgogIDwvc3R5bGU+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc2hhZG93IGEgewogICAgYm94LXNoYWRvdzogMHB4IDNweCAxcHggLTJweCByZ2JhKDAsIDAsIDAsIDAuMiksIDBweCAycHggMnB4IDBweCByZ2JhKDAsIDAsIDAsIDAuMTQpLCAwcHggMXB4IDVweCAwcHggcmdiYSgwLCAwLCAwLCAwLjEyKTsKICB9PC9zdHlsZT4KCiAge3tpZiAuRm9udFVSTH19CiAgPHN0eWxlPgogICAgQGZvbnQtZmFjZSB7CiAgICAgIGZvbnQtZmFtaWx5OiAne3suRm9udEZhbWlseX19JzsKICAgICAgZm9udC1zdHlsZTogbm9ybWFsOwogICAgICBmb250LWRpc3BsYXk6IHN3YXA7CiAgICAgIHNyYzogdXJsKHt7LkZvbnRVUkx9fSk7CiAgICB9CiAgPC9zdHlsZT4KICB7e2VuZH19Cgo8L2hlYWQ+Cjxib2R5IHN0eWxlPSJ3b3JkLXNwYWNpbmc6bm9ybWFsOyI+CgoKPGRpdgogICAgICAgIHN0eWxlPSIiCj4KCiAgPHRhYmxlCiAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYmFja2dyb3VuZDp7ey5CYWNrZ3JvdW5kQ29sb3J9fTtiYWNrZ3JvdW5kLWNvbG9yOnt7LkJhY2tncm91bmRDb2xvcn19O3dpZHRoOjEwMCU7Ym9yZGVyLXJhZGl1czoxNnB4OyIKICA+CiAgICA8dGJvZHk+CiAgICA8dHI+CiAgICAgIDx0ZD4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgIDxkaXYgIHN0eWxlPSJtYXJnaW46MHB4IGF1dG87Ym9yZGVyLXJhZGl1czoxNnB4O21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTtib3JkZXItcmFkaXVzOjE2cHg7IgogICAgICAgICAgPgogICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJkaXJlY3Rpb246bHRyO2ZvbnQtc2l6ZTowcHg7cGFkZGluZzoyMHB4IDA7cGFkZGluZy1sZWZ0OjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHdpZHRoPSI4MDBweCIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgIDx0ZD4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiB3aWR0aD0iODAwIiA+PHRyPjx0ZCBzdHlsZT0ibGluZS1oZWlnaHQ6MHB4O2ZvbnQtc2l6ZTowcHg7bXNvLWxpbmUtaGVpZ2h0LXJ1bGU6ZXhhY3RseTsiPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MDt0ZXh0LWFsaWduOmNlbnRlcjsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MDtsaW5lLWhlaWdodDowO3RleHQtYWxpZ246bGVmdDtkaXNwbGF5OmlubGluZS1ibG9jazt3aWR0aDoxMDAlO2RpcmVjdGlvbjpsdHI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iID48dHI+PHRkIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6ODAwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJtai1jb2x1bW4tcGVyLTEwMCBtai1vdXRsb29rLWdyb3VwLWZpeCIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7dGV4dC1hbGlnbjpsZWZ0O2RpcmVjdGlvbjpsdHI7ZGlzcGxheTppbmxpbmUtYmxvY2s7dmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgd2lkdGg9IjEwMCUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IiIgd2lkdGg9IjEwMCUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6NTBweCAwIDMwcHggMDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTtib3JkZXItc3BhY2luZzowcHg7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgIHN0eWxlPSJ3aWR0aDoxODBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxpbWcKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaGVpZ2h0PSJhdXRvIiBzcmM9Int7LkxvZ29VUkx9fSIgc3R5bGU9ImJvcmRlcjowO2JvcmRlci1yYWRpdXM6OHB4O2Rpc3BsYXk6YmxvY2s7b3V0bGluZTpub25lO3RleHQtZGVjb3JhdGlvbjpub25lO2hlaWdodDphdXRvO3dpZHRoOjEwMCU7Zm9udC1zaXplOjEzcHg7IiB3aWR0aD0iMTgwIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLz4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGFibGU+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48dHI+PHRkIGNsYXNzPSIiIHdpZHRoPSI4MDBweCIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0id2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgIDx0ZD4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PHRhYmxlIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiB3aWR0aD0iODAwIiA+PHRyPjx0ZCBzdHlsZT0ibGluZS1oZWlnaHQ6MHB4O2ZvbnQtc2l6ZTowcHg7bXNvLWxpbmUtaGVpZ2h0LXJ1bGU6ZXhhY3RseTsiPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO21heC13aWR0aDo4MDBweDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MDt0ZXh0LWFsaWduOmNlbnRlcjsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSByb2xlPSJwcmVzZW50YXRpb24iIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIj48dHI+PHRkIGNsYXNzPSIiIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6NDgwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9Im1qLWNvbHVtbi1wZXItNjAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkICBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3BhZGRpbmc6MDsiPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSIiIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MjRweDtmb250LXdlaWdodDo1MDA7bGluZS1oZWlnaHQ6MTt0ZXh0LWFsaWduOmNlbnRlcjtjb2xvcjp7ey5Gb250Q29sb3J9fTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPnt7LkdyZWV0aW5nfX08L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7d29yZC1icmVhazpicmVhay13b3JkOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZm9udC1mYW1pbHk6e3suRm9udEZhbWlseX19O2ZvbnQtc2l6ZToxNnB4O2ZvbnQtd2VpZ2h0OmxpZ2h0O2xpbmUtaGVpZ2h0OjEuNTt0ZXh0LWFsaWduOmNlbnRlcjtjb2xvcjp7ey5Gb250Q29sb3J9fTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPnt7LlRleHR9fTwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgdmVydGljYWwtYWxpZ249Im1pZGRsZSIgY2xhc3M9InNoYWRvdyIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7d29yZC1icmVhazpicmVhay13b3JkOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJib3JkZXItY29sbGFwc2U6c2VwYXJhdGU7bGluZS1oZWlnaHQ6MTAwJTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBiZ2NvbG9yPSJ7ey5QcmltYXJ5Q29sb3J9fSIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyOm5vbmU7Ym9yZGVyLXJhZGl1czo2cHg7Y3Vyc29yOmF1dG87bXNvLXBhZGRpbmctYWx0OjEwcHggMjVweDtiYWNrZ3JvdW5kOnt7LlByaW1hcnlDb2xvcn19OyIgdmFsaWduPSJtaWRkbGUiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8YQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgaHJlZj0ie3suVVJMfX0iIHJlbD0ibm9vcGVuZXIgbm9yZWZlcnJlciIgc3R5bGU9ImRpc3BsYXk6aW5saW5lLWJsb2NrO2JhY2tncm91bmQ6e3suUHJpbWFyeUNvbG9yfX07Y29sb3I6I2ZmZmZmZjtmb250LWZhbWlseTpVYnVudHUsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWY7Zm9udC1zaXplOjE0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjEyMCU7bWFyZ2luOjA7dGV4dC1kZWNvcmF0aW9uOm5vbmU7dGV4dC10cmFuc2Zvcm06bm9uZTtwYWRkaW5nOjEwcHggMjVweDttc28tcGFkZGluZy1hbHQ6MHB4O2JvcmRlci1yYWRpdXM6NnB4OyIgdGFyZ2V0PSJfYmxhbmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3suQnV0dG9uVGV4dH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5JbmNsdWRlRm9vdGVyfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7cGFkZGluZy10b3A6MjBweDtwYWRkaW5nLXJpZ2h0OjIwcHg7cGFkZGluZy1ib3R0b206MjBweDtwYWRkaW5nLWxlZnQ6MjBweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iYm9yZGVyLXRvcDpzb2xpZCAycHggI2RiZGJkYjtmb250LXNpemU6MXB4O21hcmdpbjowcHggYXV0bzt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHN0eWxlPSJib3JkZXItdG9wOnNvbGlkIDJweCAjZGJkYmRiO2ZvbnQtc2l6ZToxcHg7bWFyZ2luOjBweCBhdXRvO3dpZHRoOjQ0MHB4OyIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iNDQwcHgiID48dHI+PHRkIHN0eWxlPSJoZWlnaHQ6MDtsaW5lLWhlaWdodDowOyI+ICZuYnNwOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxNnB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTNweDtsaW5lLWhlaWdodDoxO3RleHQtYWxpZ246Y2VudGVyO2NvbG9yOnt7LkZvbnRDb2xvcn19OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+e3suRm9vdGVyVGV4dH19PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgogICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICA8L2Rpdj4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgIDwvdGQ+CiAgICA8L3RyPgogICAgPC90Ym9keT4KICA8L3RhYmxlPgoKPC9kaXY+Cgo8L2JvZHk+CjwvaHRtbD4KICA= + MessageTexts: + - MessageTextType: InitCode + Language: de + Title: Zitadel - User initialisieren + PreHeader: User initialisieren + Subject: User initialisieren + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Dieser Benutzer wurde soeben im Zitadel erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren. + ButtonText: Initialisierung abschliessen + - MessageTextType: PasswordReset + Language: de + Title: Zitadel - Passwort zurücksetzen + PreHeader: Passwort zurücksetzen + Subject: Passwort zurücksetzen + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren. + ButtonText: Passwort zurücksetzen + - MessageTextType: VerifyEmail + Language: de + Title: Zitadel - Email verifizieren + PreHeader: Email verifizieren + Subject: Email verifizieren + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren <br>(Code <strong>{{.Code}}</strong>).<br> Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren. + ButtonText: Email verifizieren + - MessageTextType: VerifyPhone + Language: de + Title: Zitadel - Telefonnummer verifizieren + PreHeader: Telefonnummer verifizieren + Subject: Telefonnummer verifizieren + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst (Code {{.Code}}) + ButtonText: Telefon verifizieren + - MessageTextType: DomainClaimed + Language: de + Title: Zitadel - Domain wurde beansprucht + PreHeader: Email / Username ändern + Subject: Domain wurde beansprucht + Greeting: Hallo {{.FirstName}} {{.LastName}}, + Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt. + ButtonText: Login + - MessageTextType: InitCode + Language: en + Title: Zitadel - Initialize User + PreHeader: Initialize User + Subject: Initialize User + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: This user was created in Zitadel. Use the username {{.PreferredLoginName}} to login. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. + ButtonText: Finish initialization + - MessageTextType: PasswordReset + Language: en + Title: Zitadel - Reset password + PreHeader: Reset password + Subject: Reset password + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: We received a password reset request. Please use the button below to reset your password. (Code {{.Code}}) If you didn't ask for this mail, please ignore it. + ButtonText: Reset password + - MessageTextType: VerifyEmail + Language: en + Title: Zitadel - Verify email + PreHeader: Verify email + Subject: Verify email + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email. + ButtonText: Verify email + - MessageTextType: VerifyPhone + Language: en + Title: Zitadel - Verify phone + PreHeader: Verify phone + Subject: Verify phone + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}. + ButtonText: Verify phone + - MessageTextType: DomainClaimed + Language: en + Title: Zitadel - Domain has been claimed + PreHeader: Change email / username + Subject: Domain has been claimed + Greeting: Hello {{.FirstName}} {{.LastName}}, + Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.UserName}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login. + ButtonText: Login + InternalAuthZ: RolePermissionMappings: - Role: 'IAM_OWNER' diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 89d07061b4..300300fdcc 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -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)) @@ -1463,7 +1448,7 @@ For example if the SMTP-API wasn't able to send an email at the first time Deletes the event from failed events view. the event is not removed from the change stream -This call is usefull if the system was able to process the event later. +This call is usefull if the system was able to process the event later. e.g. if the second try of sending an email was successful. the first try produced a failed event. You can find out if it worked on the `failure_count` @@ -1718,24 +1703,6 @@ This is an empty request -### ClearViewRequest - - - -| Field | Type | Description | Validation | -| ----- | ---- | ----------- | ----------- | -| database | string | - | string.min_len: 1
string.max_len: 200
| -| view_name | string | - | string.min_len: 1
string.max_len: 200
| - - - - -### ClearViewResponse -This is an empty response - - - - ### DeactivateIDPRequest diff --git a/docs/docs/apis/proto/instance.md b/docs/docs/apis/proto/instance.md index dacfab4921..a20b1f0802 100644 --- a/docs/docs/apis/proto/instance.md +++ b/docs/docs/apis/proto/instance.md @@ -70,13 +70,13 @@ DomainPrimaryQuery is always equals -### IdQuery +### IdsQuery IdQuery is always equals | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | string.max_len: 200
| +| 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
| +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) query.id_query | IdsQuery | - | | diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index aae6b40457..caf4c73ecc 100644 --- a/docs/docs/apis/proto/system.md +++ b/docs/docs/apis/proto/system.md @@ -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
string.max_len: 200
| -| first_org_name | string | - | string.min_len: 1
string.max_len: 200
| +| first_org_name | string | - | string.max_len: 200
| | custom_domain | string | - | string.max_len: 200
| -| owner_first_name | string | - | string.min_len: 1
string.max_len: 200
| -| owner_last_name | string | - | string.min_len: 1
string.max_len: 200
| +| owner_first_name | string | - | string.max_len: 200
| +| owner_last_name | string | - | string.max_len: 200
| | owner_email | string | - | string.min_len: 1
string.max_len: 200
| -| owner_username | string | - | string.min_len: 1
string.max_len: 200
| -| password | string | - | string.min_len: 1
string.max_len: 200
| +| owner_username | string | - | string.max_len: 200
| | 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 | - | | diff --git a/internal/api/api.go b/internal/api/api.go index 50070f87bd..f6ac0377c2 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -69,7 +69,9 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.Server) erro return err } a.RegisterHandler(prefix, handler) - a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods()) + if a.verifier != nil { + a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods()) + } return nil } diff --git a/internal/api/grpc/admin/failed_event_converter_test.go b/internal/api/grpc/admin/failed_event_converter_test.go index f653f4bd14..2179bf2386 100644 --- a/internal/api/grpc/admin/failed_event_converter_test.go +++ b/internal/api/grpc/admin/failed_event_converter_test.go @@ -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") } } diff --git a/internal/api/grpc/admin/view.go b/internal/api/grpc/admin/view.go index 6b99bc447a..dcaf31a851 100644 --- a/internal/api/grpc/admin/view.go +++ b/internal/api/grpc/admin/view.go @@ -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 -} diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index f944a1f18e..c809706f21 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -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") } } diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index 165be2a6f9..828631b651 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -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) } } diff --git a/internal/api/grpc/server/middleware/instance_interceptor.go b/internal/api/grpc/server/middleware/instance_interceptor.go index 3d4fa4af18..c2dc958a0b 100644 --- a/internal/api/grpc/server/middleware/instance_interceptor.go +++ b/internal/api/grpc/server/middleware/instance_interceptor.go @@ -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()) diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 2be5603e83..c0b7ec78c2 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -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(), diff --git a/internal/api/grpc/system/failed_event.go b/internal/api/grpc/system/failed_event.go new file mode 100644 index 0000000000..6e3ae82a8f --- /dev/null +++ b/internal/api/grpc/system/failed_event.go @@ -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 +} diff --git a/internal/api/grpc/system/failed_event_converter.go b/internal/api/grpc/system/failed_event_converter.go new file mode 100644 index 0000000000..2b0a87af76 --- /dev/null +++ b/internal/api/grpc/system/failed_event_converter.go @@ -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, + } +} diff --git a/internal/api/grpc/system/failed_event_converter_test.go b/internal/api/grpc/system/failed_event_converter_test.go new file mode 100644 index 0000000000..7dc3897522 --- /dev/null +++ b/internal/api/grpc/system/failed_event_converter_test.go @@ -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") + } +} diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go new file mode 100644 index 0000000000..053c25563f --- /dev/null +++ b/internal/api/grpc/system/instance.go @@ -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 +} diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go new file mode 100644 index 0000000000..958b4001ca --- /dev/null +++ b/internal/api/grpc/system/instance_converter.go @@ -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{} + } +} diff --git a/internal/api/grpc/system/server.go b/internal/api/grpc/system/server.go new file mode 100644 index 0000000000..007dd0548b --- /dev/null +++ b/internal/api/grpc/system/server.go @@ -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" +} diff --git a/internal/api/grpc/system/view.go b/internal/api/grpc/system/view.go new file mode 100644 index 0000000000..2ae458651e --- /dev/null +++ b/internal/api/grpc/system/view.go @@ -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 +} diff --git a/internal/api/grpc/system/view_converter.go b/internal/api/grpc/system/view_converter.go new file mode 100644 index 0000000000..5d2273aaf9 --- /dev/null +++ b/internal/api/grpc/system/view_converter.go @@ -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), + } +} diff --git a/internal/command/instance.go b/internal/command/instance.go index d26aa57e9d..3037e1dc65 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -28,10 +28,22 @@ 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 { - Org OrgSetup - Zitadel ZitadelConfig - Features struct { + zitadel ZitadelConfig + InstanceName string + CustomDomain string + Org OrgSetup + Features struct { TierName string TierDescription string Retention time.Duration @@ -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) { diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go index 757df7abb7..686a0b2527 100644 --- a/internal/command/instance_domain.go +++ b/internal/command/instance_domain.go @@ -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,28 +85,32 @@ 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 } - 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 + if appWriteModel.State.Exists() { + 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 + } + events = append(events, consoleChangeEvent) } - return []eventstore.Command{ - instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated), - consoleChangeEvent, - }, nil + + return events, nil }, nil } } diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index 6d987fb6c3..eada5bfcef 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -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") diff --git a/internal/domain/instance_domain.go b/internal/domain/instance_domain.go index be1284ed6a..14ecbce5f0 100644 --- a/internal/domain/instance_domain.go +++ b/internal/domain/instance_domain.go @@ -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) } diff --git a/internal/migration/migration.go b/internal/migration/migration.go index 2aca242be5..93d7db1d37 100644 --- a/internal/migration/migration.go +++ b/internal/migration/migration.go @@ -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)) - return 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) { diff --git a/internal/query/instance.go b/internal/query/instance.go index c0356c2170..8e256a9814 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -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, @@ -62,9 +70,10 @@ var ( ) type Instance struct { - ID string - ChangeDate time.Time - Sequence uint64 + ID string + ChangeDate time.Time + CreationDate time.Time + Sequence uint64 GlobalOrgID string IAMProjectID 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 + } +} diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index 90561d88cd..5380e7e05b 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -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,`+ diff --git a/internal/query/projection/instance.go b/internal/query/projection/instance.go index ea00f7ba1f..dfae71ab81 100644 --- a/internal/query/projection/instance.go +++ b/internal/query/projection/instance.go @@ -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 } diff --git a/internal/query/projection/instance_domain.go b/internal/query/projection/instance_domain.go index 6e4b7f1e19..028848f6d2 100644 --- a/internal/query/projection/instance_domain.go +++ b/internal/query/projection/instance_domain.go @@ -57,7 +57,7 @@ func (p *InstanceDomainProjection) reducers() []handler.AggregateReducer { Reduce: p.reduceDomainAdded, }, { - Event: instance.InstanceDomainAddedEventType, + Event: instance.InstanceDomainPrimarySetEventType, Reduce: p.reduceDomainPrimarySet, }, { diff --git a/internal/query/projection/instance_test.go b/internal/query/projection/instance_test.go index ecec5131c4..8ec26e0ee6 100644 --- a/internal/query/projection/instance_test.go +++ b/internal/query/projection/instance_test.go @@ -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", }, }, }, diff --git a/internal/repository/instance/domain.go b/internal/repository/instance/domain.go index c1e8a8784a..78c4a35f03 100644 --- a/internal/repository/instance/domain.go +++ b/internal/repository/instance/domain.go @@ -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) diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 539ff0131d..59c3a7e697 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -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 @@ -2582,7 +2554,7 @@ service AdminService { //Deletes the event from failed events view. // the event is not removed from the change stream - // This call is usefull if the system was able to process the event later. + // This call is usefull if the system was able to process the event later. // e.g. if the second try of sending an email was successful. the first try produced a // failed event. You can find out if it worked on the `failure_count` rpc RemoveFailedEvent(RemoveFailedEventRequest) returns (RemoveFailedEventResponse) { @@ -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 {} diff --git a/proto/zitadel/instance.proto b/proto/zitadel/instance.proto index 8e138788cc..075a601f03 100644 --- a/proto/zitadel/instance.proto +++ b/proto/zitadel/instance.proto @@ -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; diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 0618d105cb..94b8582ef4 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -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 {