diff --git a/.releaserc.js b/.releaserc.js index 6500efe28c..16be63ca2c 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -3,7 +3,7 @@ module.exports = { {name: 'main'}, {name: '1.x.x', range: '1.x.x', channel: '1.x.x'}, {name: 'v2-alpha', prerelease: true}, - {name: 'scheduler', prerelease: true}, + {name: 'v2-alpha-import', prerelease: true}, ], plugins: [ "@semantic-release/commit-analyzer" diff --git a/build/zitadel/generate-grpc.sh b/build/zitadel/generate-grpc.sh index 89d0efa41c..f4e6b4d1e8 100755 --- a/build/zitadel/generate-grpc.sh +++ b/build/zitadel/generate-grpc.sh @@ -182,6 +182,9 @@ protoc \ -I=/proto/include \ --doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,settings.md \ ${PROTO_PATH}/settings.proto - +protoc \ + -I=/proto/include \ + --doc_out=${DOCS_PATH} --doc_opt=${PROTO_PATH}/docs/zitadel-md.tmpl,v1.md \ + ${PROTO_PATH}/v1.proto echo "done generating grpc" diff --git a/cmd/start/start.go b/cmd/start/start.go index 313ef460e3..92668f4d24 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -100,7 +100,7 @@ func startZitadel(config *Config, masterKey string) error { return fmt.Errorf("cannot start eventstore for queries: %w", err) } - queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, keys.OIDC, config.InternalAuthZ.RolePermissionMappings) + queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, config.SystemDefaults, keys.IDPConfig, keys.OTP, keys.OIDC, config.InternalAuthZ.RolePermissionMappings) if err != nil { return fmt.Errorf("cannot start queries: %w", err) } @@ -178,7 +178,7 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, adminRepo, config.Database.Database, config.DefaultInstance)); err != nil { return err } - if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.Database, commands, queries, adminRepo, config.ExternalSecure, keys.User)); err != nil { + if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.Database, commands, queries, config.SystemDefaults, adminRepo, config.ExternalSecure, keys.User)); err != nil { return err } if err := apis.RegisterServer(ctx, management.CreateServer(commands, queries, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil { diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index d8b8a8332a..50d6b506cf 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -386,7 +386,7 @@ all queries need to match (AND) > **rpc** SetUpOrg([SetUpOrgRequest](#setuporgrequest)) [SetUpOrgResponse](#setuporgresponse) -Creates a new org and user +Creates a new org and user and adds the user to the orgs members as ORG_OWNER @@ -1469,7 +1469,7 @@ they represent the delta of the event happend on the objects [ListFailedEventsResponse](#listfailedeventsresponse) Returns event descriptions which cannot be processed. -It's possible that some events need some retries. +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 @@ -1493,6 +1493,30 @@ failed event. You can find out if it worked on the `failure_count` DELETE: /failedevents/{database}/{view_name}/{failed_sequence} +### ImportData + +> **rpc** ImportData([ImportDataRequest](#importdatarequest)) +[ImportDataResponse](#importdataresponse) + +Imports data into instance and creates different objects + + + + POST: /import + + +### ExportData + +> **rpc** ExportData([ExportDataRequest](#exportdatarequest)) +[ExportDataResponse](#exportdataresponse) + +Exports data from instance + + + + POST: /export + + @@ -1789,6 +1813,49 @@ This is an empty request +### DataOrg + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| org_id | string | - | | +| org | zitadel.management.v1.AddOrgRequest | - | | +| domain_policy | AddCustomDomainPolicyRequest | - | | +| label_policy | zitadel.management.v1.AddCustomLabelPolicyRequest | - | | +| lockout_policy | zitadel.management.v1.AddCustomLockoutPolicyRequest | - | | +| login_policy | zitadel.management.v1.AddCustomLoginPolicyRequest | - | | +| password_complexity_policy | zitadel.management.v1.AddCustomPasswordComplexityPolicyRequest | - | | +| privacy_policy | zitadel.management.v1.AddCustomPrivacyPolicyRequest | - | | +| projects | repeated zitadel.v1.v1.DataProject | - | | +| project_roles | repeated zitadel.management.v1.AddProjectRoleRequest | - | | +| api_apps | repeated zitadel.v1.v1.DataAPIApplication | - | | +| oidc_apps | repeated zitadel.v1.v1.DataOIDCApplication | - | | +| human_users | repeated zitadel.v1.v1.DataHumanUser | - | | +| machine_users | repeated zitadel.v1.v1.DataMachineUser | - | | +| trigger_actions | repeated zitadel.management.v1.SetTriggerActionsRequest | - | | +| actions | repeated zitadel.v1.v1.DataAction | - | | +| project_grants | repeated zitadel.v1.v1.DataProjectGrant | - | | +| user_grants | repeated zitadel.management.v1.AddUserGrantRequest | - | | +| org_members | repeated zitadel.management.v1.AddOrgMemberRequest | - | | +| project_members | repeated zitadel.management.v1.AddProjectMemberRequest | - | | +| project_grant_members | repeated zitadel.management.v1.AddProjectGrantMemberRequest | - | | +| user_metadata | repeated zitadel.management.v1.SetUserMetadataRequest | - | | +| login_texts | repeated zitadel.management.v1.SetCustomLoginTextsRequest | - | | +| init_messages | repeated zitadel.management.v1.SetCustomInitMessageTextRequest | - | | +| password_reset_messages | repeated zitadel.management.v1.SetCustomPasswordResetMessageTextRequest | - | | +| verify_email_messages | repeated zitadel.management.v1.SetCustomVerifyEmailMessageTextRequest | - | | +| verify_phone_messages | repeated zitadel.management.v1.SetCustomVerifyPhoneMessageTextRequest | - | | +| domain_claimed_messages | repeated zitadel.management.v1.SetCustomDomainClaimedMessageTextRequest | - | | +| passwordless_registration_messages | repeated zitadel.management.v1.SetCustomPasswordlessRegistrationMessageTextRequest | - | | +| oidc_idps | repeated zitadel.v1.v1.DataOIDCIDP | - | | +| jwt_idps | repeated zitadel.v1.v1.DataJWTIDP | - | | +| user_links | repeated zitadel.idp.v1.IDPUserLink | - | | +| domains | repeated zitadel.org.v1.Domain | - | | + + + + ### DeactivateIDPRequest @@ -1833,6 +1900,76 @@ This is an empty request +### ExportDataRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| org_ids | repeated string | - | | +| excluded_org_ids | repeated string | - | | +| with_passwords | bool | - | | +| with_otp | bool | - | | +| response_output | bool | - | | +| local_output | ExportDataRequest.LocalOutput | - | | +| s3_output | ExportDataRequest.S3Output | - | | +| gcs_output | ExportDataRequest.GCSOutput | - | | +| timeout | string | - | | + + + + +### ExportDataRequest.GCSOutput + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| bucket | string | - | | +| serviceaccount_json | string | - | | +| path | string | - | | + + + + +### ExportDataRequest.LocalOutput + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| path | string | - | | + + + + +### ExportDataRequest.S3Output + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| path | string | - | | +| endpoint | string | - | | +| access_key_id | string | - | | +| secret_access_key | string | - | | +| ssl | bool | - | | +| bucket | string | - | | + + + + +### ExportDataResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| orgs | repeated DataOrg | - | | + + + + ### FailedEvent @@ -2603,6 +2740,218 @@ This is an empty response +### ImportDataError + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| type | string | - | | +| id | string | - | | +| message | string | - | | + + + + +### ImportDataOrg + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| orgs | repeated DataOrg | - | | + + + + +### ImportDataRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgs | ImportDataOrg | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgsv1 | zitadel.v1.v1.ImportDataOrg | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgs_local | ImportDataRequest.LocalInput | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgsv1_local | ImportDataRequest.LocalInput | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgs_s3 | ImportDataRequest.S3Input | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgsv1_s3 | ImportDataRequest.S3Input | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgs_gcs | ImportDataRequest.GCSInput | - | | +| [**oneof**](https://developers.google.com/protocol-buffers/docs/proto3#oneof) data.data_orgsv1_gcs | ImportDataRequest.GCSInput | - | | +| timeout | string | - | | + + + + +### ImportDataRequest.GCSInput + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| bucket | string | - | | +| serviceaccount_json | string | - | | +| path | string | - | | + + + + +### ImportDataRequest.LocalInput + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| path | string | - | | + + + + +### ImportDataRequest.S3Input + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| path | string | - | | +| endpoint | string | - | | +| access_key_id | string | - | | +| secret_access_key | string | - | | +| ssl | bool | - | | +| bucket | string | - | | + + + + +### ImportDataResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| errors | repeated ImportDataError | - | | +| success | ImportDataSuccess | - | | + + + + +### ImportDataSuccess + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| orgs | repeated ImportDataSuccessOrg | - | | + + + + +### ImportDataSuccessOrg + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| org_id | string | - | | +| project_ids | repeated string | - | | +| project_roles | repeated string | - | | +| oidc_app_ids | repeated string | - | | +| api_app_ids | repeated string | - | | +| human_user_ids | repeated string | - | | +| machine_user_ids | repeated string | - | | +| action_ids | repeated string | - | | +| trigger_actions | repeated zitadel.management.v1.SetTriggerActionsRequest | - | | +| project_grants | repeated ImportDataSuccessProjectGrant | - | | +| user_grants | repeated ImportDataSuccessUserGrant | - | | +| org_members | repeated string | - | | +| project_members | repeated ImportDataSuccessProjectMember | - | | +| project_grant_members | repeated ImportDataSuccessProjectGrantMember | - | | +| oidc_ipds | repeated string | - | | +| jwt_idps | repeated string | - | | +| idp_links | repeated string | - | | +| user_links | repeated ImportDataSuccessUserLinks | - | | +| user_metadata | repeated ImportDataSuccessUserMetadata | - | | +| domains | repeated string | - | | + + + + +### ImportDataSuccessProjectGrant + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| grant_id | string | - | | +| project_id | string | - | | +| org_id | string | - | | + + + + +### ImportDataSuccessProjectGrantMember + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| project_id | string | - | | +| grant_id | string | - | | +| user_id | string | - | | + + + + +### ImportDataSuccessProjectMember + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| project_id | string | - | | +| user_id | string | - | | + + + + +### ImportDataSuccessUserGrant + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| project_id | string | - | | +| user_id | string | - | | + + + + +### ImportDataSuccessUserLinks + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_id | string | - | | +| external_user_id | string | - | | +| display_name | string | - | | +| idp_id | string | - | | + + + + +### ImportDataSuccessUserMetadata + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_id | string | - | | +| key | string | - | | + + + + ### IsOrgUniqueRequest if name or domain is already in use, org is not unique at least one argument has to be provided diff --git a/docs/docs/apis/proto/management.md b/docs/docs/apis/proto/management.md index 2eac7a106c..0c5dbaa08d 100644 --- a/docs/docs/apis/proto/management.md +++ b/docs/docs/apis/proto/management.md @@ -5176,8 +5176,10 @@ This is an empty response | email | ImportHumanUserRequest.Email | - | message.required: true
| | phone | ImportHumanUserRequest.Phone | - | | | password | string | - | | +| hashed_password | ImportHumanUserRequest.HashedPassword | - | | | password_change_required | bool | - | | | request_passwordless_registration | bool | - | | +| otp_code | string | - | | @@ -5194,6 +5196,18 @@ This is an empty response +### ImportHumanUserRequest.HashedPassword + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| value | string | - | | +| algorithm | string | - | | + + + + ### ImportHumanUserRequest.Phone diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index 2d75c73df6..8574121251 100644 --- a/docs/docs/apis/proto/system.md +++ b/docs/docs/apis/proto/system.md @@ -150,8 +150,8 @@ they represent the delta of the event happend on the objects [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. +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 @@ -165,7 +165,7 @@ Search requests will return wrong results until all deltas are recomputed [ListFailedEventsResponse](#listfailedeventsresponse) Returns event descriptions which cannot be processed. -It's possible that some events need some retries. +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 @@ -180,7 +180,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` diff --git a/docs/docs/apis/proto/v1.md b/docs/docs/apis/proto/v1.md new file mode 100644 index 0000000000..b8f4638bef --- /dev/null +++ b/docs/docs/apis/proto/v1.md @@ -0,0 +1,261 @@ +--- +title: zitadel/v1.proto +--- +> This document reflects the state from API 1.0 (available from 20.04.2021) + + + + +## Messages + + +### AddCustomOrgIAMPolicyRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| org_id | string | - | string.min_len: 1
string.max_len: 200
| +| user_login_must_be_domain | bool | the username has to end with the domain of it's organisation (uniqueness is organisation based) | | + + + + +### DataAPIApplication + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| app_id | string | - | | +| app | zitadel.management.v1.AddAPIAppRequest | - | | + + + + +### DataAction + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| action_id | string | - | | +| action | zitadel.management.v1.CreateActionRequest | - | | + + + + +### DataHumanUser + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_id | string | - | | +| user | zitadel.management.v1.ImportHumanUserRequest | - | | + + + + +### DataJWTIDP + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| idp_id | string | - | | +| idp | zitadel.management.v1.AddOrgJWTIDPRequest | - | | + + + + +### DataMachineUser + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_id | string | - | | +| user | zitadel.management.v1.AddMachineUserRequest | - | | + + + + +### DataOIDCApplication + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| app_id | string | - | | +| app | zitadel.management.v1.AddOIDCAppRequest | - | | + + + + +### DataOIDCIDP + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| idp_id | string | - | | +| idp | zitadel.management.v1.AddOrgOIDCIDPRequest | - | | + + + + +### DataOrg + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| org_id | string | - | | +| org | zitadel.management.v1.AddOrgRequest | - | | +| iam_policy | AddCustomOrgIAMPolicyRequest | - | | +| label_policy | zitadel.management.v1.AddCustomLabelPolicyRequest | - | | +| lockout_policy | zitadel.management.v1.AddCustomLockoutPolicyRequest | - | | +| login_policy | zitadel.management.v1.AddCustomLoginPolicyRequest | - | | +| password_complexity_policy | zitadel.management.v1.AddCustomPasswordComplexityPolicyRequest | - | | +| privacy_policy | zitadel.management.v1.AddCustomPrivacyPolicyRequest | - | | +| projects | repeated DataProject | - | | +| project_roles | repeated zitadel.management.v1.AddProjectRoleRequest | - | | +| api_apps | repeated DataAPIApplication | - | | +| oidc_apps | repeated DataOIDCApplication | - | | +| human_users | repeated DataHumanUser | - | | +| machine_users | repeated DataMachineUser | - | | +| trigger_actions | repeated zitadel.management.v1.SetTriggerActionsRequest | - | | +| actions | repeated DataAction | - | | +| project_grants | repeated DataProjectGrant | - | | +| user_grants | repeated zitadel.management.v1.AddUserGrantRequest | - | | +| org_members | repeated zitadel.management.v1.AddOrgMemberRequest | - | | +| project_members | repeated zitadel.management.v1.AddProjectMemberRequest | - | | +| project_grant_members | repeated zitadel.management.v1.AddProjectGrantMemberRequest | - | | +| user_metadata | repeated zitadel.management.v1.SetUserMetadataRequest | - | | +| login_texts | repeated zitadel.management.v1.SetCustomLoginTextsRequest | - | | +| init_messages | repeated zitadel.management.v1.SetCustomInitMessageTextRequest | - | | +| password_reset_messages | repeated zitadel.management.v1.SetCustomPasswordResetMessageTextRequest | - | | +| verify_email_messages | repeated zitadel.management.v1.SetCustomVerifyEmailMessageTextRequest | - | | +| verify_phone_messages | repeated zitadel.management.v1.SetCustomVerifyPhoneMessageTextRequest | - | | +| domain_claimed_messages | repeated zitadel.management.v1.SetCustomDomainClaimedMessageTextRequest | - | | +| passwordless_registration_messages | repeated zitadel.management.v1.SetCustomPasswordlessRegistrationMessageTextRequest | - | | +| oidc_idps | repeated DataOIDCIDP | - | | +| jwt_idps | repeated DataJWTIDP | - | | +| second_factors | repeated zitadel.management.v1.AddSecondFactorToLoginPolicyRequest | - | | +| multi_factors | repeated zitadel.management.v1.AddMultiFactorToLoginPolicyRequest | - | | +| idps | repeated zitadel.management.v1.AddIDPToLoginPolicyRequest | - | | +| user_links | repeated zitadel.idp.v1.IDPUserLink | - | | +| domains | repeated zitadel.org.v1.Domain | - | | + + + + +### DataProject + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| project_id | string | - | | +| project | zitadel.management.v1.AddProjectRequest | - | | + + + + +### DataProjectGrant + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| grant_id | string | - | | +| project_grant | zitadel.management.v1.AddProjectGrantRequest | - | | + + + + +### ExportHumanUser + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| user_name | string | - | string.min_len: 1
string.max_len: 200
| +| profile | ExportHumanUser.Profile | - | message.required: true
| +| email | ExportHumanUser.Email | - | message.required: true
| +| phone | ExportHumanUser.Phone | - | | +| password | string | - | | +| hashed_password | ExportHumanUser.HashedPassword | - | | +| password_change_required | bool | - | | +| request_passwordless_registration | bool | - | | +| otp_code | string | - | | + + + + +### ExportHumanUser.Email + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| email | string | TODO: check if no value is allowed | string.email: true
| +| is_email_verified | bool | - | | + + + + +### ExportHumanUser.HashedPassword + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| value | string | - | | +| algorithm | string | - | | + + + + +### ExportHumanUser.Phone + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| phone | string | has to be a global number | string.min_len: 1
string.max_len: 50
string.prefix: +
| +| is_phone_verified | bool | - | | + + + + +### ExportHumanUser.Profile + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| first_name | string | - | string.min_len: 1
string.max_len: 200
| +| last_name | string | - | string.min_len: 1
string.max_len: 200
| +| nick_name | string | - | string.max_len: 200
| +| display_name | string | - | string.max_len: 200
| +| preferred_language | string | - | string.max_len: 10
| +| gender | zitadel.user.v1.Gender | - | | + + + + +### ImportDataOrg + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| orgs | repeated DataOrg | - | | + + + + + + diff --git a/docs/docs/guides/api/export-and-import.md b/docs/docs/guides/api/export-and-import.md new file mode 100644 index 0000000000..d47df6d174 --- /dev/null +++ b/docs/docs/guides/api/export-and-import.md @@ -0,0 +1,129 @@ +--- +title: Export and import with ZITADEL +--- + +## Export from V1 to Import into V2 + +To migrate from ZITADEL V1 to V2 the API provides you with a possibility to export all resources which are under your organizations. +Currently, this doesn't include the following points: + +* Global policies +* IAM members +* Global IDPs +* Global second/multi factors +* Machine keys +* PAT's +* Application keys + +Which results in that if you want to import, and you have no defined organization-specific custom policies, the experience for your users will not be exactly like in your old instance. +```suggestion + +::note Note that the ressources will be migrated without the event stream. This means that you will not have the audit trail for the imported objects. + +*** With this export and import the current audit trail is not included, the resources will be newly created *** + +### Export from V1 to import into V2 directly + +***To use this requests you have to have an access token with enough permissions to export and import.*** + +To export all necessary data you only have to use one request, as an example: + +```bash +curl --request POST \ + --url {your_domain}/admin/v1/export \ + --header 'Authorization: Bearer XXXX' \ + --header 'Content-Type: application/json' \ + --data '{ + "org_ids": [ "70669144072186707", "70671105999825752" ], + "excluded_org_ids": [ ], + "with_passwords": true, + "with_otp": true + "timeout": "30s", + "response_output": true, +}' -o export.json +``` + +* "org_ids": to select which organizations should be exported +* "excluded_org_ids": to exclude several organization, if for example no organizations are selected +* "with_passwords": to include the hashed_passwords of the users in the export +* "with_otp": to include the OTP-code of the users in the export +* "timeout": timeout of the call to export the data +* "response_output": to output the export as response to the call + +***To import the exported data into you new instance, you have to have an already existing instance on a ZITADEL V2, with all desired configuration and global resources.*** + +Then as an example you can use one request for the import: + +```bash +curl --request POST \ + --url {your_domain}/admin/v1/import \ + --header 'Authorization: Bearer XXXX' \ + --header 'Content-Type: application/json' \ + --data '{ + "data_orgsv1": '$(cat export.json)' +}' +``` + +## Export from V1 to Import into V2 thorugh GCS + +***To use this requests you have to have an access token with enough permissions to export and import.*** + +***The used serviceaccount has to have at least the role "Storage Object Creator" to create objects on GCS*** + +To export all necessary data you only have to use one request which results in a file in your GCS, as an example: + +```bash +curl --request POST \ + --url {your_domain}/admin/v1/export \ + --header 'Authorization: Bearer XXXX' \ + --header 'Content-Type: application/json' \ + --data ' "{ + "org_ids": [ "70669144072186707", "70671105999825752" ], + "excluded_org_ids": [ ], + "with_passwords": true, + "with_otp": true, + "timeout": "10m", + "gcs_output": { + "path": "export.json", + "bucket": "caos-zitadel-exports", + "serviceaccount_json": "XXXX" + }' -o export.json +``` + +* "org_ids": to select which organizations should be exported +* "excluded_org_ids": to exclude several organization, if for example no organizations are selected +* "with_passwords": to include the hashed_passwords of the users in the export +* "with_otp": to include the OTP-code of the users in the export +* "timeout": timeout for the call to export the data +* "gcs_output": to write a file into GCS as output to the call + * "path": path to the output file on GCS + * "bucket": used bucket for output on GCS + * "serviceaccount_json": base64-encoded serviceaccount.json used to output the file on GCS + +***To import the exported data into you new instance, you have to have an already existing instance on a ZITADEL V2, with all desired configuration and global resources.*** + +***The used serviceaccount has to have at least the role "Storage Object Viewer" to create objects on GCS*** + +Then as an example you can use one request for the import: + +```bash +curl --request POST \ + --url {your_domain}/admin/v1/import \ + --header 'Authorization: Bearer XXXX' \ + --header 'Content-Type: application/json' \ + --data '{ + "timeout": "10m", + "data_orgsv1_gcs": { + "path": "export.json", + "bucket": "caos-zitadel-exports", + "serviceaccount_json": "XXXX" + } +}' +``` + +* "timeout": timeout for the import task +* "data_orgsv1_gcs": to read the export from GCS directly + * "path": path to the exported file on GCS + * "bucket": used bucket to read from GCS + * "serviceaccount_json": base64-encoded serviceaccount.json used to read the file from GCS + diff --git a/docs/sidebars.js b/docs/sidebars.js index 271d14a54f..6b3f880432 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -100,7 +100,10 @@ module.exports = { type: "category", label: "API", collapsed: false, - items: ["guides/api/access-zitadel-apis"], + items: [ + "guides/api/access-zitadel-apis", + "guides/api/export-and-import" + ], }, { type: "category", diff --git a/go.mod b/go.mod index 679d91caea..f56d089b5a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/zitadel/zitadel go 1.17 require ( + cloud.google.com/go/storage v1.14.0 github.com/BurntSushi/toml v0.4.1 github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.0.0 @@ -171,7 +172,7 @@ require ( golang.org/x/mod v0.5.1 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/api v0.63.0 // indirect + google.golang.org/api v0.63.0 google.golang.org/appengine v1.6.7 // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 7f10eaf680..4110d1ee0a 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/trace v1.0.0 h1:laKx2y7IWMjguCe5zZx6n7qLtREk4kyE69SXVC0VSN8= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= @@ -366,9 +367,11 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= diff --git a/internal/api/grpc/admin/domain_policy.go b/internal/api/grpc/admin/domain_policy.go index c86ca13723..b7628d33bb 100644 --- a/internal/api/grpc/admin/domain_policy.go +++ b/internal/api/grpc/admin/domain_policy.go @@ -27,7 +27,7 @@ func (s *Server) GetCustomDomainPolicy(ctx context.Context, req *admin_pb.GetCus } func (s *Server) AddCustomDomainPolicy(ctx context.Context, req *admin_pb.AddCustomDomainPolicyRequest) (*admin_pb.AddCustomDomainPolicyResponse, error) { - policy, err := s.command.AddOrgDomainPolicy(ctx, req.OrgId, domainPolicyToDomain(req.UserLoginMustBeDomain, req.ValidateOrgDomains, req.SmtpSenderAddressMatchesInstanceDomain)) + policy, err := s.command.AddOrgDomainPolicy(ctx, req.OrgId, DomainPolicyToDomain(req.UserLoginMustBeDomain, req.ValidateOrgDomains, req.SmtpSenderAddressMatchesInstanceDomain)) if err != nil { return nil, err } @@ -76,7 +76,7 @@ func (s *Server) ResetCustomDomainPolicyToDefault(ctx context.Context, req *admi return &admin_pb.ResetCustomDomainPolicyToDefaultResponse{Details: object.DomainToChangeDetailsPb(details)}, nil } -func domainPolicyToDomain(userLoginMustBeDomain, validateOrgDomains, smtpSenderAddressMatchesInstanceDomain bool) *domain.DomainPolicy { +func DomainPolicyToDomain(userLoginMustBeDomain, validateOrgDomains, smtpSenderAddressMatchesInstanceDomain bool) *domain.DomainPolicy { return &domain.DomainPolicy{ UserLoginMustBeDomain: userLoginMustBeDomain, ValidateOrgDomains: validateOrgDomains, @@ -104,7 +104,7 @@ func updateCustomDomainPolicyToDomain(req *admin_pb.UpdateCustomDomainPolicyRequ } func (s *Server) AddCustomOrgIAMPolicy(ctx context.Context, req *admin_pb.AddCustomOrgIAMPolicyRequest) (*admin_pb.AddCustomOrgIAMPolicyResponse, error) { - policy, err := s.command.AddOrgDomainPolicy(ctx, req.OrgId, domainPolicyToDomain(req.UserLoginMustBeDomain, true, true)) + policy, err := s.command.AddOrgDomainPolicy(ctx, req.OrgId, DomainPolicyToDomain(req.UserLoginMustBeDomain, true, true)) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go new file mode 100644 index 0000000000..e7d637ac2f --- /dev/null +++ b/internal/api/grpc/admin/export.go @@ -0,0 +1,1151 @@ +package admin + +import ( + "context" + text_grpc "github.com/zitadel/zitadel/internal/api/grpc/text" + "github.com/zitadel/zitadel/internal/domain" + caos_errors "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + action_pb "github.com/zitadel/zitadel/pkg/grpc/action" + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + app_pb "github.com/zitadel/zitadel/pkg/grpc/app" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp" + management_pb "github.com/zitadel/zitadel/pkg/grpc/management" + org_pb "github.com/zitadel/zitadel/pkg/grpc/org" + policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project" + user_pb "github.com/zitadel/zitadel/pkg/grpc/user" + v1_pb "github.com/zitadel/zitadel/pkg/grpc/v1" + "google.golang.org/protobuf/types/known/durationpb" +) + +func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest) (_ *admin_pb.ExportDataResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + orgSearchQuery := &query.OrgSearchQueries{} + if len(req.OrgIds) > 0 { + orgIDsSearchQuery, err := query.NewOrgIDsSearchQuery(req.OrgIds...) + if err != nil { + return nil, err + } + orgSearchQuery.Queries = []query.SearchQuery{orgIDsSearchQuery} + } + queriedOrgs, err := s.query.SearchOrgs(ctx, orgSearchQuery) + if err != nil { + return nil, err + } + + orgs := make([]*admin_pb.DataOrg, len(queriedOrgs.Orgs)) + processedOrgs := make([]string, len(queriedOrgs.Orgs)) + processedProjects := make([]string, 0) + processedGrants := make([]string, 0) + processedUsers := make([]string, 0) + processedActions := make([]string, 0) + + for i, queriedOrg := range queriedOrgs.Orgs { + if req.ExcludedOrgIds != nil { + found := false + for _, excludedOrg := range req.ExcludedOrgIds { + if excludedOrg == queriedOrg.ID { + found = true + } + } + if found { + continue + } + } + processedOrgs = append(processedOrgs, queriedOrg.ID) + + /****************************************************************************************************************** + Organization + ******************************************************************************************************************/ + org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} + orgs[i] = org + } + + for _, org := range orgs { + + org.DomainPolicy, err = s.getDomainPolicy(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.Domains, err = s.getDomains(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.OidcIdps, org.JwtIdps, err = s.getIDPs(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.LabelPolicy, err = s.getLabelPolicy(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.LoginPolicy, err = s.getLoginPolicy(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.UserLinks, err = s.getUserLinks(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.LockoutPolicy, err = s.getLockoutPolicy(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.PasswordComplexityPolicy, err = s.getPasswordComplexityPolicy(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + org.PrivacyPolicy, err = s.getPrivacyPolicy(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + + langResp, err := s.GetSupportedLanguages(ctx, &admin_pb.GetSupportedLanguagesRequest{}) + if err != nil { + return nil, err + } + + org.LoginTexts, err = s.getCustomLoginTexts(ctx, org.GetOrgId(), langResp.Languages) + if err != nil { + return nil, err + } + + org.InitMessages, err = s.getCustomInitMessageTexts(ctx, org.GetOrgId(), langResp.Languages) + if err != nil { + return nil, err + } + + org.PasswordResetMessages, err = s.getCustomPasswordResetMessageTexts(ctx, org.GetOrgId(), langResp.Languages) + if err != nil { + return nil, err + } + + org.VerifyEmailMessages, err = s.getCustomVerifyEmailMessageTexts(ctx, org.GetOrgId(), langResp.Languages) + if err != nil { + return nil, err + } + + org.VerifyPhoneMessages, err = s.getCustomVerifyPhoneMessageTexts(ctx, org.GetOrgId(), langResp.Languages) + if err != nil { + return nil, err + } + + org.DomainClaimedMessages, err = s.getCustomDomainClaimedMessageTexts(ctx, org.GetOrgId(), langResp.Languages) + if err != nil { + return nil, err + } + + org.PasswordlessRegistrationMessages, err = s.getCustomPasswordlessRegistrationMessageTexts(ctx, org.GetOrgId(), langResp.Languages) + if err != nil { + return nil, err + } + + /****************************************************************************************************************** + Users + ******************************************************************************************************************/ + org.HumanUsers, org.MachineUsers, org.UserMetadata, err = s.getUsers(ctx, org.GetOrgId(), req.WithPasswords, req.WithOtp) + if err != nil { + return nil, err + } + for _, processedUser := range org.HumanUsers { + processedUsers = append(processedUsers, processedUser.UserId) + } + for _, processedUser := range org.MachineUsers { + processedUsers = append(processedUsers, processedUser.UserId) + } + + /****************************************************************************************************************** + Project and Applications + ******************************************************************************************************************/ + org.Projects, org.ProjectRoles, org.OidcApps, org.ApiApps, err = s.getProjectsAndApps(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + for _, processedProject := range org.Projects { + processedProjects = append(processedProjects, processedProject.ProjectId) + } + + /****************************************************************************************************************** + Actions + ******************************************************************************************************************/ + org.Actions, err = s.getActions(ctx, org.GetOrgId()) + if err != nil { + return nil, err + } + for _, processedAction := range org.Actions { + processedActions = append(processedActions, processedAction.ActionId) + } + } + + for _, org := range orgs { + /****************************************************************************************************************** + Flows + ******************************************************************************************************************/ + org.TriggerActions, err = s.getTriggerActions(ctx, org.OrgId, processedActions) + if err != nil { + return nil, err + } + + /****************************************************************************************************************** + Grants + ******************************************************************************************************************/ + org.ProjectGrants, err = s.getNecessaryProjectGrantsForOrg(ctx, org.OrgId, processedOrgs, processedProjects) + if err != nil { + return nil, err + } + for _, processedGrant := range org.ProjectGrants { + processedGrants = append(processedGrants, processedGrant.GrantId) + } + + org.UserGrants, err = s.getNecessaryUserGrantsForOrg(ctx, org.OrgId, processedProjects, processedGrants, processedUsers) + if err != nil { + return nil, err + } + } + + for _, org := range orgs { + /****************************************************************************************************************** + Members + ******************************************************************************************************************/ + org.OrgMembers, err = s.getNecessaryOrgMembersForOrg(ctx, org.OrgId, processedUsers) + if err != nil { + return nil, err + } + + org.ProjectMembers, err = s.getNecessaryProjectMembersForOrg(ctx, processedProjects, processedUsers) + if err != nil { + return nil, err + } + + org.ProjectGrantMembers, err = s.getNecessaryProjectGrantMembersForOrg(ctx, org.OrgId, processedProjects, processedGrants, processedUsers) + if err != nil { + return nil, err + } + } + + return &admin_pb.ExportDataResponse{ + Orgs: orgs, + }, nil +} + +func (s *Server) getDomainPolicy(ctx context.Context, orgID string) (_ *admin_pb.AddCustomDomainPolicyRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + queriedDomain, err := s.query.DomainPolicyByOrg(ctx, true, orgID) + if err != nil { + return nil, err + } + if !queriedDomain.IsDefault { + return &admin_pb.AddCustomDomainPolicyRequest{ + OrgId: orgID, + UserLoginMustBeDomain: queriedDomain.UserLoginMustBeDomain, + ValidateOrgDomains: queriedDomain.ValidateOrgDomains, + SmtpSenderAddressMatchesInstanceDomain: queriedDomain.SMTPSenderAddressMatchesInstanceDomain, + }, nil + } + return nil, nil +} + +func (s *Server) getDomains(ctx context.Context, orgID string) (_ []*org_pb.Domain, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + orgDomainOrgIDQuery, err := query.NewOrgDomainOrgIDSearchQuery(orgID) + if err != nil { + return nil, err + } + orgDomainsQuery, err := s.query.SearchOrgDomains(ctx, &query.OrgDomainSearchQueries{Queries: []query.SearchQuery{orgDomainOrgIDQuery}}) + if err != nil { + return nil, err + } + orgDomains := make([]*org_pb.Domain, len(orgDomainsQuery.Domains)) + for i, orgDomain := range orgDomainsQuery.Domains { + orgDomains[i] = &org_pb.Domain{ + OrgId: orgDomain.OrgID, + DomainName: orgDomain.Domain, + IsVerified: orgDomain.IsVerified, + IsPrimary: orgDomain.IsPrimary, + ValidationType: org_pb.DomainValidationType(orgDomain.ValidationType), + } + } + return orgDomains, nil +} + +func (s *Server) getIDPs(ctx context.Context, orgID string) (_ []*v1_pb.DataOIDCIDP, _ []*v1_pb.DataJWTIDP, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + ownerType, err := query.NewIDPOwnerTypeSearchQuery(domain.IdentityProviderTypeOrg) + if err != nil { + return nil, nil, err + } + idpQuery, err := query.NewIDPResourceOwnerSearchQuery(orgID) + if err != nil { + return nil, nil, err + } + idps, err := s.query.IDPs(ctx, &query.IDPSearchQueries{Queries: []query.SearchQuery{idpQuery, ownerType}}) + if err != nil { + return nil, nil, err + } + oidcIdps := make([]*v1_pb.DataOIDCIDP, 0) + jwtIdps := make([]*v1_pb.DataJWTIDP, 0) + for _, idp := range idps.IDPs { + if idp.OIDCIDP != nil { + clientSecret, err := s.query.GetOIDCIDPClientSecret(ctx, false, orgID, idp.ID) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, nil, err + } + oidcIdps = append(oidcIdps, &v1_pb.DataOIDCIDP{ + IdpId: idp.ID, + Idp: &management_pb.AddOrgOIDCIDPRequest{ + Name: idp.Name, + StylingType: idp_pb.IDPStylingType(idp.StylingType), + ClientId: idp.ClientID, + ClientSecret: clientSecret, + Issuer: idp.OIDCIDP.Issuer, + Scopes: idp.Scopes, + DisplayNameMapping: idp_pb.OIDCMappingField(idp.DisplayNameMapping), + UsernameMapping: idp_pb.OIDCMappingField(idp.UsernameMapping), + AutoRegister: idp.AutoRegister, + }, + }) + } else if idp.JWTIDP != nil { + jwtIdps = append(jwtIdps, &v1_pb.DataJWTIDP{ + IdpId: idp.ID, + Idp: &management_pb.AddOrgJWTIDPRequest{ + Name: idp.Name, + StylingType: idp_pb.IDPStylingType(idp.StylingType), + JwtEndpoint: idp.JWTIDP.Endpoint, + Issuer: idp.JWTIDP.Issuer, + KeysEndpoint: idp.KeysEndpoint, + HeaderName: idp.HeaderName, + AutoRegister: idp.AutoRegister, + }, + }) + } + } + return oidcIdps, jwtIdps, nil +} + +func (s *Server) getLabelPolicy(ctx context.Context, orgID string) (_ *management_pb.AddCustomLabelPolicyRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + queriedLabel, err := s.query.ActiveLabelPolicyByOrg(ctx, orgID) + if err != nil { + return nil, err + } + if !queriedLabel.IsDefault { + return &management_pb.AddCustomLabelPolicyRequest{ + PrimaryColor: queriedLabel.Light.PrimaryColor, + HideLoginNameSuffix: queriedLabel.HideLoginNameSuffix, + WarnColor: queriedLabel.Light.WarnColor, + BackgroundColor: queriedLabel.Light.BackgroundColor, + FontColor: queriedLabel.Light.FontColor, + PrimaryColorDark: queriedLabel.Dark.PrimaryColor, + BackgroundColorDark: queriedLabel.Dark.BackgroundColor, + WarnColorDark: queriedLabel.Dark.WarnColor, + FontColorDark: queriedLabel.Dark.FontColor, + DisableWatermark: queriedLabel.WatermarkDisabled, + }, nil + } + return nil, nil +} + +func (s *Server) getLoginPolicy(ctx context.Context, orgID string) (_ *management_pb.AddCustomLoginPolicyRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + queriedLogin, err := s.query.LoginPolicyByID(ctx, false, orgID) + if err != nil { + return nil, err + } + if !queriedLogin.IsDefault { + pwCheck := durationpb.New(queriedLogin.PasswordCheckLifetime) + externalLogin := durationpb.New(queriedLogin.ExternalLoginCheckLifetime) + mfaInitSkip := durationpb.New(queriedLogin.MFAInitSkipLifetime) + secondFactor := durationpb.New(queriedLogin.SecondFactorCheckLifetime) + multiFactor := durationpb.New(queriedLogin.MultiFactorCheckLifetime) + + secondFactors := []policy_pb.SecondFactorType{} + for _, factor := range queriedLogin.SecondFactors { + secondFactors = append(secondFactors, policy_pb.SecondFactorType(factor)) + } + + multiFactors := []policy_pb.MultiFactorType{} + for _, factor := range queriedLogin.MultiFactors { + multiFactors = append(multiFactors, policy_pb.MultiFactorType(factor)) + } + + idpLinksQuery, err := s.query.IDPLoginPolicyLinks(ctx, orgID, &query.IDPLoginPolicyLinksSearchQuery{}) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, err + } + idpLinks := make([]*management_pb.AddCustomLoginPolicyRequest_IDP, 0) + if !caos_errors.IsNotFound(err) && idpLinksQuery != nil { + for _, idpLink := range idpLinksQuery.Links { + idpLinks = append(idpLinks, &management_pb.AddCustomLoginPolicyRequest_IDP{ + IdpId: idpLink.IDPID, + OwnerType: idp_pb.IDPOwnerType(idpLink.IDPType), + }) + } + } + + return &management_pb.AddCustomLoginPolicyRequest{ + AllowUsernamePassword: queriedLogin.AllowUsernamePassword, + AllowRegister: queriedLogin.AllowRegister, + AllowExternalIdp: queriedLogin.AllowExternalIDPs, + ForceMfa: queriedLogin.ForceMFA, + PasswordlessType: policy_pb.PasswordlessType(queriedLogin.PasswordlessType), + HidePasswordReset: queriedLogin.HidePasswordReset, + IgnoreUnknownUsernames: queriedLogin.IgnoreUnknownUsernames, + DefaultRedirectUri: queriedLogin.DefaultRedirectURI, + PasswordCheckLifetime: pwCheck, + ExternalLoginCheckLifetime: externalLogin, + MfaInitSkipLifetime: mfaInitSkip, + SecondFactorCheckLifetime: secondFactor, + MultiFactorCheckLifetime: multiFactor, + SecondFactors: secondFactors, + MultiFactors: multiFactors, + Idps: idpLinks, + }, nil + } + + return nil, nil +} + +func (s *Server) getUserLinks(ctx context.Context, orgID string) (_ []*idp_pb.IDPUserLink, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + userLinksResourceOwner, err := query.NewIDPUserLinksResourceOwnerSearchQuery(orgID) + if err != nil { + return nil, err + } + idpUserLinks, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: []query.SearchQuery{userLinksResourceOwner}}) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, err + } + userLinks := make([]*idp_pb.IDPUserLink, 0) + if !caos_errors.IsNotFound(err) && idpUserLinks != nil { + for _, idpUserLink := range idpUserLinks.Links { + userLinks = append(userLinks, &idp_pb.IDPUserLink{ + UserId: idpUserLink.UserID, + IdpId: idpUserLink.IDPID, + IdpName: idpUserLink.IDPName, + ProvidedUserId: idpUserLink.ProvidedUserID, + ProvidedUserName: idpUserLink.ProvidedUsername, + IdpType: idp_pb.IDPType(idpUserLink.IDPType), + }) + } + } + + return userLinks, nil +} + +func (s *Server) getLockoutPolicy(ctx context.Context, orgID string) (_ *management_pb.AddCustomLockoutPolicyRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + queriedLockout, err := s.query.LockoutPolicyByOrg(ctx, false, orgID) + if err != nil { + return nil, err + } + if !queriedLockout.IsDefault { + return &management_pb.AddCustomLockoutPolicyRequest{ + MaxPasswordAttempts: uint32(queriedLockout.MaxPasswordAttempts), + }, nil + } + return nil, nil +} + +func (s *Server) getPasswordComplexityPolicy(ctx context.Context, orgID string) (_ *management_pb.AddCustomPasswordComplexityPolicyRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + queriedPasswordComplexity, err := s.query.PasswordComplexityPolicyByOrg(ctx, false, orgID) + if err != nil { + return nil, err + } + if !queriedPasswordComplexity.IsDefault { + return &management_pb.AddCustomPasswordComplexityPolicyRequest{ + MinLength: queriedPasswordComplexity.MinLength, + HasUppercase: queriedPasswordComplexity.HasUppercase, + HasLowercase: queriedPasswordComplexity.HasLowercase, + HasNumber: queriedPasswordComplexity.HasNumber, + HasSymbol: queriedPasswordComplexity.HasSymbol, + }, nil + } + return nil, nil +} + +func (s *Server) getPrivacyPolicy(ctx context.Context, orgID string) (_ *management_pb.AddCustomPrivacyPolicyRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + queriedPrivacy, err := s.query.PrivacyPolicyByOrg(ctx, false, orgID) + if err != nil { + return nil, err + } + if !queriedPrivacy.IsDefault { + return &management_pb.AddCustomPrivacyPolicyRequest{ + TosLink: queriedPrivacy.TOSLink, + PrivacyLink: queriedPrivacy.PrivacyLink, + HelpLink: queriedPrivacy.HelpLink, + }, nil + } + return nil, nil +} + +func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, withOTP bool) (_ []*v1_pb.DataHumanUser, _ []*v1_pb.DataMachineUser, _ []*management_pb.SetUserMetadataRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + orgSearch, err := query.NewUserResourceOwnerSearchQuery(org, query.TextEquals) + if err != nil { + return nil, nil, nil, err + } + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, nil, nil, err + } + humanUsers := make([]*v1_pb.DataHumanUser, 0) + machineUsers := make([]*v1_pb.DataMachineUser, 0) + userMetadata := make([]*management_pb.SetUserMetadataRequest, 0) + if err != nil && caos_errors.IsNotFound(err) { + return humanUsers, machineUsers, userMetadata, nil + } + for _, user := range users.Users { + switch user.Type { + case domain.UserTypeHuman: + dataUser := &v1_pb.DataHumanUser{ + UserId: user.ID, + User: &management_pb.ImportHumanUserRequest{ + UserName: user.Username, + Profile: &management_pb.ImportHumanUserRequest_Profile{ + FirstName: user.Human.FirstName, + LastName: user.Human.LastName, + NickName: user.Human.NickName, + DisplayName: user.Human.DisplayName, + PreferredLanguage: user.Human.PreferredLanguage.String(), + Gender: user_pb.Gender(user.Human.Gender), + }, + }, + } + if user.Human.Email != "" { + dataUser.User.Email = &management_pb.ImportHumanUserRequest_Email{ + Email: user.Human.Email, + IsEmailVerified: user.Human.IsEmailVerified, + } + } + if user.Human.Phone != "" { + dataUser.User.Phone = &management_pb.ImportHumanUserRequest_Phone{ + Phone: user.Human.Phone, + IsPhoneVerified: user.Human.IsPhoneVerified, + } + } + if withPasswords { + ctx, pwspan := tracing.NewSpan(ctx) + hashedPassword, hashAlgorithm, err := s.query.GetHumanPassword(ctx, org, user.ID) + pwspan.EndWithError(err) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, nil, nil, err + } + if err == nil && hashedPassword != nil { + dataUser.User.HashedPassword = &management_pb.ImportHumanUserRequest_HashedPassword{ + Value: string(hashedPassword), + Algorithm: hashAlgorithm, + } + } + } + if withOTP { + ctx, otpspan := tracing.NewSpan(ctx) + code, err := s.query.GetHumanOTPSecret(ctx, user.ID, org) + otpspan.EndWithError(err) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, nil, nil, err + } + if err == nil && code != "" { + dataUser.User.OtpCode = code + } + } + + humanUsers = append(humanUsers, dataUser) + case domain.UserTypeMachine: + machineUsers = append(machineUsers, &v1_pb.DataMachineUser{ + UserId: user.ID, + User: &management_pb.AddMachineUserRequest{ + UserName: user.Username, + Name: user.Machine.Name, + Description: user.Machine.Description, + }, + }) + } + + ctx, metaspan := tracing.NewSpan(ctx) + metadataOrgSearch, err := query.NewUserMetadataResourceOwnerSearchQuery(org) + if err != nil { + return nil, nil, nil, err + } + metadataList, err := s.query.SearchUserMetadata(ctx, false, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{metadataOrgSearch}}) + metaspan.EndWithError(err) + if err != nil { + return nil, nil, nil, err + } + for _, metadata := range metadataList.Metadata { + userMetadata = append(userMetadata, &management_pb.SetUserMetadataRequest{ + Id: user.ID, + Key: metadata.Key, + Value: metadata.Value, + }) + } + } + return humanUsers, machineUsers, userMetadata, nil +} + +func (s *Server) getTriggerActions(ctx context.Context, org string, processedActions []string) (_ []*management_pb.SetTriggerActionsRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + flowTypes := []domain.FlowType{domain.FlowTypeExternalAuthentication} + triggerActions := make([]*management_pb.SetTriggerActionsRequest, 0) + + for _, flowType := range flowTypes { + flow, err := s.query.GetFlow(ctx, flowType, org) + if err != nil { + return nil, err + } + + for triggerType, triggerAction := range flow.TriggerActions { + actions := make([]string, 0) + for _, action := range triggerAction { + for _, actionID := range processedActions { + if action.ID == actionID { + actions = append(actions, action.ID) + } + } + } + + triggerActions = append(triggerActions, &management_pb.SetTriggerActionsRequest{ + FlowType: action_pb.FlowType(flowType), + TriggerType: action_pb.TriggerType(triggerType), + ActionIds: actions, + }) + } + } + return triggerActions, nil +} + +func (s *Server) getActions(ctx context.Context, org string) ([]*v1_pb.DataAction, error) { + actionSearch, err := query.NewActionResourceOwnerQuery(org) + if err != nil { + return nil, err + } + queriedActions, err := s.query.SearchActions(ctx, &query.ActionSearchQueries{Queries: []query.SearchQuery{actionSearch}}) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, err + } + actions := make([]*v1_pb.DataAction, len(queriedActions.Actions)) + if err != nil && caos_errors.IsNotFound(err) { + return actions, nil + } + for i, action := range queriedActions.Actions { + timeout := durationpb.New(action.Timeout) + + actions[i] = &v1_pb.DataAction{ + ActionId: action.ID, + Action: &management_pb.CreateActionRequest{ + Name: action.Name, + Script: action.Script, + Timeout: timeout, + AllowedToFail: action.AllowedToFail, + }, + } + } + + return actions, nil +} + +func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.DataProject, []*management_pb.AddProjectRoleRequest, []*v1_pb.DataOIDCApplication, []*v1_pb.DataAPIApplication, error) { + projectSearch, err := query.NewProjectResourceOwnerSearchQuery(org) + if err != nil { + return nil, nil, nil, nil, err + } + queriedProjects, err := s.query.SearchProjects(ctx, &query.ProjectSearchQueries{Queries: []query.SearchQuery{projectSearch}}) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, nil, nil, nil, err + } + + projects := make([]*v1_pb.DataProject, len(queriedProjects.Projects)) + orgProjectRoles := make([]*management_pb.AddProjectRoleRequest, 0) + oidcApps := make([]*v1_pb.DataOIDCApplication, 0) + apiApps := make([]*v1_pb.DataAPIApplication, 0) + if err != nil && caos_errors.IsNotFound(err) { + return projects, orgProjectRoles, oidcApps, apiApps, nil + } + for i, queriedProject := range queriedProjects.Projects { + projects[i] = &v1_pb.DataProject{ + ProjectId: queriedProject.ID, + Project: &management_pb.AddProjectRequest{ + Name: queriedProject.Name, + ProjectRoleAssertion: queriedProject.ProjectRoleAssertion, + ProjectRoleCheck: queriedProject.ProjectRoleCheck, + HasProjectCheck: queriedProject.HasProjectCheck, + PrivateLabelingSetting: project_pb.PrivateLabelingSetting(queriedProject.PrivateLabelingSetting), + }, + } + + projectRoleSearch, err := query.NewProjectRoleProjectIDSearchQuery(queriedProject.ID) + if err != nil { + return nil, nil, nil, nil, err + } + + queriedProjectRoles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectRoleSearch}}) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, nil, nil, nil, err + } + if queriedProjectRoles != nil { + for _, role := range queriedProjectRoles.ProjectRoles { + orgProjectRoles = append(orgProjectRoles, &management_pb.AddProjectRoleRequest{ + ProjectId: role.ProjectID, + RoleKey: role.Key, + DisplayName: role.DisplayName, + Group: role.Group, + }) + } + } + + appSearch, err := query.NewAppProjectIDSearchQuery(queriedProject.ID) + if err != nil { + return nil, nil, nil, nil, err + } + apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}) + if err != nil && !caos_errors.IsNotFound(err) { + return nil, nil, nil, nil, err + } + if apps != nil { + for _, app := range apps.Apps { + if app.OIDCConfig != nil { + responseTypes := make([]app_pb.OIDCResponseType, 0) + for _, ty := range app.OIDCConfig.ResponseTypes { + responseTypes = append(responseTypes, app_pb.OIDCResponseType(ty)) + } + + grantTypes := make([]app_pb.OIDCGrantType, 0) + for _, ty := range app.OIDCConfig.GrantTypes { + grantTypes = append(grantTypes, app_pb.OIDCGrantType(ty)) + } + + oidcApps = append(oidcApps, &v1_pb.DataOIDCApplication{ + AppId: app.ID, + App: &management_pb.AddOIDCAppRequest{ + ProjectId: app.ProjectID, + Name: app.Name, + RedirectUris: app.OIDCConfig.RedirectURIs, + ResponseTypes: responseTypes, + GrantTypes: grantTypes, + AppType: app_pb.OIDCAppType(app.OIDCConfig.AppType), + AuthMethodType: app_pb.OIDCAuthMethodType(app.OIDCConfig.AuthMethodType), + PostLogoutRedirectUris: app.OIDCConfig.PostLogoutRedirectURIs, + Version: app_pb.OIDCVersion(app.OIDCConfig.Version), + DevMode: app.OIDCConfig.IsDevMode, + AccessTokenType: app_pb.OIDCTokenType(app.OIDCConfig.AccessTokenType), + AccessTokenRoleAssertion: app.OIDCConfig.AssertAccessTokenRole, + IdTokenRoleAssertion: app.OIDCConfig.AssertIDTokenRole, + IdTokenUserinfoAssertion: app.OIDCConfig.AssertIDTokenUserinfo, + ClockSkew: durationpb.New(app.OIDCConfig.ClockSkew), + AdditionalOrigins: app.OIDCConfig.AdditionalOrigins, + }, + }) + } + if app.APIConfig != nil { + apiApps = append(apiApps, &v1_pb.DataAPIApplication{ + AppId: app.ID, + App: &management_pb.AddAPIAppRequest{ + ProjectId: app.ProjectID, + Name: app.Name, + AuthMethodType: app_pb.APIAuthMethodType(app.APIConfig.AuthMethodType), + }, + }) + } + } + } + } + return projects, orgProjectRoles, oidcApps, apiApps, nil +} + +func (s *Server) getNecessaryProjectGrantMembersForOrg(ctx context.Context, org string, processedProjects []string, processedGrants []string, processedUsers []string) ([]*management_pb.AddProjectGrantMemberRequest, error) { + projectMembers := make([]*management_pb.AddProjectGrantMemberRequest, 0) + + for _, projectID := range processedProjects { + for _, grantID := range processedGrants { + search, err := query.NewMemberResourceOwnerSearchQuery(org) + if err != nil { + return nil, err + } + + queriedProjectMembers, err := s.query.ProjectGrantMembers(ctx, &query.ProjectGrantMembersQuery{ProjectID: projectID, OrgID: org, GrantID: grantID, MembersQuery: query.MembersQuery{Queries: []query.SearchQuery{search}}}) + if err != nil { + return nil, err + } + for _, projectMember := range queriedProjectMembers.Members { + for _, userID := range processedUsers { + if userID == projectMember.UserID { + + projectMembers = append(projectMembers, &management_pb.AddProjectGrantMemberRequest{ + ProjectId: projectID, + UserId: userID, + GrantId: grantID, + Roles: projectMember.Roles, + }) + break + } + } + + } + } + } + return projectMembers, nil +} + +func (s *Server) getNecessaryProjectMembersForOrg(ctx context.Context, processedProjects []string, processedUsers []string) ([]*management_pb.AddProjectMemberRequest, error) { + projectMembers := make([]*management_pb.AddProjectMemberRequest, 0) + + for _, projectID := range processedProjects { + queriedProjectMembers, err := s.query.ProjectMembers(ctx, &query.ProjectMembersQuery{ProjectID: projectID}) + if err != nil { + return nil, err + } + for _, projectMember := range queriedProjectMembers.Members { + for _, userID := range processedUsers { + if userID == projectMember.UserID { + projectMembers = append(projectMembers, &management_pb.AddProjectMemberRequest{ + ProjectId: projectID, + UserId: userID, + Roles: projectMember.Roles, + }) + break + } + } + } + } + return projectMembers, nil +} + +func (s *Server) getNecessaryOrgMembersForOrg(ctx context.Context, org string, processedUsers []string) ([]*management_pb.AddOrgMemberRequest, error) { + queriedOrgMembers, err := s.query.OrgMembers(ctx, &query.OrgMembersQuery{OrgID: org}) + if err != nil { + return nil, err + } + orgMembers := make([]*management_pb.AddOrgMemberRequest, 0, len(queriedOrgMembers.Members)) + for _, orgMember := range queriedOrgMembers.Members { + for _, userID := range processedUsers { + if userID == orgMember.UserID { + orgMembers = append(orgMembers, &management_pb.AddOrgMemberRequest{ + UserId: orgMember.UserID, + Roles: orgMember.Roles, + }) + break + } + } + } + return orgMembers, nil +} + +func (s *Server) getNecessaryProjectGrantsForOrg(ctx context.Context, org string, processedOrgs []string, processedProjects []string) ([]*v1_pb.DataProjectGrant, error) { + + projectGrantSearchOrg, err := query.NewProjectGrantResourceOwnerSearchQuery(org) + if err != nil { + return nil, err + } + queriedProjectGrants, err := s.query.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectGrantSearchOrg}}) + if err != nil { + return nil, err + } + projectGrants := make([]*v1_pb.DataProjectGrant, 0, len(queriedProjectGrants.ProjectGrants)) + for _, projectGrant := range queriedProjectGrants.ProjectGrants { + for _, projectID := range processedProjects { + if projectID == projectGrant.ProjectID { + foundOrg := false + for _, orgID := range processedOrgs { + if orgID == projectGrant.GrantedOrgID { + projectGrants = append(projectGrants, &v1_pb.DataProjectGrant{ + GrantId: projectGrant.GrantID, + ProjectGrant: &management_pb.AddProjectGrantRequest{ + ProjectId: projectGrant.ProjectID, + GrantedOrgId: projectGrant.GrantedOrgID, + RoleKeys: projectGrant.GrantedRoleKeys, + }, + }) + foundOrg = true + break + } + } + if foundOrg { + break + } + } + } + } + return projectGrants, nil +} + +func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, processedProjects []string, processedGrants []string, processedUsers []string) ([]*management_pb.AddUserGrantRequest, error) { + userGrantSearchOrg, err := query.NewUserGrantResourceOwnerSearchQuery(org) + if err != nil { + return nil, err + } + + queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}) + if err != nil { + return nil, err + } + userGrants := make([]*management_pb.AddUserGrantRequest, 0, len(queriedUserGrants.UserGrants)) + for _, userGrant := range queriedUserGrants.UserGrants { + for _, projectID := range processedProjects { + if projectID == userGrant.ProjectID { + //if usergrant is on a granted project + if userGrant.GrantID != "" { + for _, grantID := range processedGrants { + if grantID == userGrant.GrantID { + for _, userID := range processedUsers { + if userID == userGrant.UserID { + userGrants = append(userGrants, &management_pb.AddUserGrantRequest{ + UserId: userGrant.UserID, + ProjectId: userGrant.ProjectID, + ProjectGrantId: userGrant.GrantID, + RoleKeys: userGrant.Roles, + }) + } + } + } + } + } else { + for _, userID := range processedUsers { + if userID == userGrant.UserID { + userGrants = append(userGrants, &management_pb.AddUserGrantRequest{ + UserId: userGrant.UserID, + ProjectId: userGrant.ProjectID, + ProjectGrantId: userGrant.GrantID, + RoleKeys: userGrant.Roles, + }) + } + } + } + } + } + } + return userGrants, nil +} +func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomLoginTextsRequest, error) { + customTexts := make([]*management_pb.SetCustomLoginTextsRequest, 0, len(languages)) + for _, lang := range languages { + text, err := s.query.GetCustomLoginTexts(ctx, org, lang) + if err != nil { + return nil, err + } + if !text.IsDefault { + customTexts = append(customTexts, &management_pb.SetCustomLoginTextsRequest{ + Language: lang, + SelectAccountText: text_grpc.SelectAccountScreenToPb(text.SelectAccount), + LoginText: text_grpc.LoginScreenTextToPb(text.Login), + PasswordText: text_grpc.PasswordScreenTextToPb(text.Password), + UsernameChangeText: text_grpc.UsernameChangeScreenTextToPb(text.UsernameChange), + UsernameChangeDoneText: text_grpc.UsernameChangeDoneScreenTextToPb(text.UsernameChangeDone), + InitPasswordText: text_grpc.InitPasswordScreenTextToPb(text.InitPassword), + InitPasswordDoneText: text_grpc.InitPasswordDoneScreenTextToPb(text.InitPasswordDone), + EmailVerificationText: text_grpc.EmailVerificationScreenTextToPb(text.EmailVerification), + EmailVerificationDoneText: text_grpc.EmailVerificationDoneScreenTextToPb(text.EmailVerificationDone), + InitializeUserText: text_grpc.InitializeUserScreenTextToPb(text.InitUser), + InitializeDoneText: text_grpc.InitializeUserDoneScreenTextToPb(text.InitUserDone), + InitMfaPromptText: text_grpc.InitMFAPromptScreenTextToPb(text.InitMFAPrompt), + InitMfaOtpText: text_grpc.InitMFAOTPScreenTextToPb(text.InitMFAOTP), + InitMfaU2FText: text_grpc.InitMFAU2FScreenTextToPb(text.InitMFAU2F), + InitMfaDoneText: text_grpc.InitMFADoneScreenTextToPb(text.InitMFADone), + MfaProvidersText: text_grpc.MFAProvidersTextToPb(text.MFAProvider), + VerifyMfaOtpText: text_grpc.VerifyMFAOTPScreenTextToPb(text.VerifyMFAOTP), + VerifyMfaU2FText: text_grpc.VerifyMFAU2FScreenTextToPb(text.VerifyMFAU2F), + PasswordlessText: text_grpc.PasswordlessScreenTextToPb(text.Passwordless), + PasswordlessPromptText: text_grpc.PasswordlessPromptScreenTextToPb(text.PasswordlessPrompt), + PasswordlessRegistrationText: text_grpc.PasswordlessRegistrationScreenTextToPb(text.PasswordlessRegistration), + PasswordlessRegistrationDoneText: text_grpc.PasswordlessRegistrationDoneScreenTextToPb(text.PasswordlessRegistrationDone), + PasswordChangeText: text_grpc.PasswordChangeScreenTextToPb(text.PasswordChange), + PasswordChangeDoneText: text_grpc.PasswordChangeDoneScreenTextToPb(text.PasswordChangeDone), + PasswordResetDoneText: text_grpc.PasswordResetDoneScreenTextToPb(text.PasswordResetDone), + RegistrationOptionText: text_grpc.RegistrationOptionScreenTextToPb(text.RegisterOption), + RegistrationUserText: text_grpc.RegistrationUserScreenTextToPb(text.RegistrationUser), + ExternalRegistrationUserOverviewText: text_grpc.ExternalRegistrationUserOverviewScreenTextToPb(text.ExternalRegistrationUserOverview), + RegistrationOrgText: text_grpc.RegistrationOrgScreenTextToPb(text.RegistrationOrg), + LinkingUserDoneText: text_grpc.LinkingUserDoneScreenTextToPb(text.LinkingUsersDone), + ExternalUserNotFoundText: text_grpc.ExternalUserNotFoundScreenTextToPb(text.ExternalNotFoundOption), + SuccessLoginText: text_grpc.SuccessLoginScreenTextToPb(text.LoginSuccess), + LogoutText: text_grpc.LogoutDoneScreenTextToPb(text.LogoutDone), + FooterText: text_grpc.FooterTextToPb(text.Footer), + }) + } + } + + return customTexts, nil +} + +func (s *Server) getCustomInitMessageTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomInitMessageTextRequest, error) { + customTexts := make([]*management_pb.SetCustomInitMessageTextRequest, 0, len(languages)) + for _, lang := range languages { + text, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, org, domain.InitCodeMessageType, lang) + if err != nil { + return nil, err + } + + if !text.IsDefault { + customTexts = append(customTexts, &management_pb.SetCustomInitMessageTextRequest{ + Language: lang, + Title: text.Title, + PreHeader: text.PreHeader, + Subject: text.Subject, + Greeting: text.Greeting, + Text: text.Text, + ButtonText: text.ButtonText, + FooterText: text.Footer, + }) + } + } + + return customTexts, nil +} + +func (s *Server) getCustomPasswordResetMessageTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomPasswordResetMessageTextRequest, error) { + customTexts := make([]*management_pb.SetCustomPasswordResetMessageTextRequest, 0, len(languages)) + for _, lang := range languages { + text, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, org, domain.PasswordResetMessageType, lang) + if err != nil { + return nil, err + } + + if !text.IsDefault { + customTexts = append(customTexts, &management_pb.SetCustomPasswordResetMessageTextRequest{ + Language: lang, + Title: text.Title, + PreHeader: text.PreHeader, + Subject: text.Subject, + Greeting: text.Greeting, + Text: text.Text, + ButtonText: text.ButtonText, + FooterText: text.Footer, + }) + } + } + + return customTexts, nil +} + +func (s *Server) getCustomVerifyEmailMessageTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomVerifyEmailMessageTextRequest, error) { + customTexts := make([]*management_pb.SetCustomVerifyEmailMessageTextRequest, 0, len(languages)) + for _, lang := range languages { + text, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, org, domain.VerifyEmailMessageType, lang) + if err != nil { + return nil, err + } + + if !text.IsDefault { + customTexts = append(customTexts, &management_pb.SetCustomVerifyEmailMessageTextRequest{ + Language: lang, + Title: text.Title, + PreHeader: text.PreHeader, + Subject: text.Subject, + Greeting: text.Greeting, + Text: text.Text, + ButtonText: text.ButtonText, + FooterText: text.Footer, + }) + } + } + + return customTexts, nil +} + +func (s *Server) getCustomVerifyPhoneMessageTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomVerifyPhoneMessageTextRequest, error) { + customTexts := make([]*management_pb.SetCustomVerifyPhoneMessageTextRequest, 0, len(languages)) + for _, lang := range languages { + text, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, org, domain.VerifyPhoneMessageType, lang) + if err != nil { + return nil, err + } + + if !text.IsDefault { + customTexts = append(customTexts, &management_pb.SetCustomVerifyPhoneMessageTextRequest{ + Language: lang, + Title: text.Title, + PreHeader: text.PreHeader, + Subject: text.Subject, + Greeting: text.Greeting, + Text: text.Text, + ButtonText: text.ButtonText, + FooterText: text.Footer, + }) + } + } + + return customTexts, nil +} + +func (s *Server) getCustomDomainClaimedMessageTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomDomainClaimedMessageTextRequest, error) { + customTexts := make([]*management_pb.SetCustomDomainClaimedMessageTextRequest, 0, len(languages)) + for _, lang := range languages { + text, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, org, domain.DomainClaimedMessageType, lang) + if err != nil { + return nil, err + } + + if !text.IsDefault { + customTexts = append(customTexts, &management_pb.SetCustomDomainClaimedMessageTextRequest{ + Language: lang, + Title: text.Title, + PreHeader: text.PreHeader, + Subject: text.Subject, + Greeting: text.Greeting, + Text: text.Text, + ButtonText: text.ButtonText, + FooterText: text.Footer, + }) + } + } + + return customTexts, nil +} + +func (s *Server) getCustomPasswordlessRegistrationMessageTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomPasswordlessRegistrationMessageTextRequest, error) { + customTexts := make([]*management_pb.SetCustomPasswordlessRegistrationMessageTextRequest, 0, len(languages)) + for _, lang := range languages { + text, err := s.query.CustomMessageTextByTypeAndLanguage(ctx, org, domain.DomainClaimedMessageType, lang) + if err != nil { + return nil, err + } + + if !text.IsDefault { + customTexts = append(customTexts, &management_pb.SetCustomPasswordlessRegistrationMessageTextRequest{ + Language: lang, + Title: text.Title, + PreHeader: text.PreHeader, + Subject: text.Subject, + Greeting: text.Greeting, + Text: text.Text, + ButtonText: text.ButtonText, + FooterText: text.Footer, + }) + } + } + + return customTexts, nil +} diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go new file mode 100644 index 0000000000..66765bf6f4 --- /dev/null +++ b/internal/api/grpc/admin/import.go @@ -0,0 +1,898 @@ +package admin + +import ( + "cloud.google.com/go/storage" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/management" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" + management_pb "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/policy" + v1_pb "github.com/zitadel/zitadel/pkg/grpc/v1" + "google.golang.org/api/option" + "google.golang.org/protobuf/types/known/durationpb" + "io/ioutil" + "strconv" + "time" +) + +type importResponse struct { + ret *admin_pb.ImportDataResponse + count *count + err error +} +type count struct { + humanUserCount int + humanUserLen int + machineUserCount int + machineUserLen int + userMetadataCount int + userMetadataLen int + userLinksCount int + userLinksLen int + projectCount int + projectLen int + oidcAppCount int + oidcAppLen int + apiAppCount int + apiAppLen int + actionCount int + actionLen int + projectRolesCount int + projectRolesLen int + projectGrantCount int + projectGrantLen int + userGrantCount int + userGrantLen int + projectMembersCount int + projectMembersLen int + orgMemberCount int + orgMemberLen int + projectGrantMemberCount int + projectGrantMemberLen int +} + +func (c *count) getProgress() string { + return "progress:" + + "human_users " + strconv.Itoa(c.humanUserCount) + "/" + strconv.Itoa(c.humanUserLen) + ", " + + "machine_users " + strconv.Itoa(c.machineUserCount) + "/" + strconv.Itoa(c.machineUserLen) + ", " + + "user_metadata " + strconv.Itoa(c.userMetadataCount) + "/" + strconv.Itoa(c.userMetadataLen) + ", " + + "user_links " + strconv.Itoa(c.userLinksCount) + "/" + strconv.Itoa(c.userLinksLen) + ", " + + "projects " + strconv.Itoa(c.projectCount) + "/" + strconv.Itoa(c.projectLen) + ", " + + "oidc_apps " + strconv.Itoa(c.oidcAppCount) + "/" + strconv.Itoa(c.oidcAppLen) + ", " + + "api_apps " + strconv.Itoa(c.apiAppCount) + "/" + strconv.Itoa(c.apiAppLen) + ", " + + "actions " + strconv.Itoa(c.actionCount) + "/" + strconv.Itoa(c.actionLen) + ", " + + "project_roles " + strconv.Itoa(c.projectRolesCount) + "/" + strconv.Itoa(c.projectRolesLen) + ", " + + "project_grant " + strconv.Itoa(c.projectGrantCount) + "/" + strconv.Itoa(c.projectGrantLen) + ", " + + "user_grants " + strconv.Itoa(c.userGrantCount) + "/" + strconv.Itoa(c.userGrantLen) + ", " + + "project_members " + strconv.Itoa(c.projectMembersCount) + "/" + strconv.Itoa(c.projectMembersLen) + ", " + + "org_members " + strconv.Itoa(c.orgMemberCount) + "/" + strconv.Itoa(c.orgMemberLen) + ", " + + "project_grant_members " + strconv.Itoa(c.projectGrantMemberCount) + "/" + strconv.Itoa(c.projectGrantMemberLen) +} + +func Detach(ctx context.Context) context.Context { return detachedContext{ctx} } + +type detachedContext struct { + parent context.Context +} + +func (v detachedContext) Deadline() (time.Time, bool) { return time.Time{}, false } +func (v detachedContext) Done() <-chan struct{} { return nil } +func (v detachedContext) Err() error { return nil } +func (v detachedContext) Value(key interface{}) interface{} { return v.parent.Value(key) } + +func (s *Server) ImportData(ctx context.Context, req *admin_pb.ImportDataRequest) (_ *admin_pb.ImportDataResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if req.GetDataOrgs() != nil || req.GetDataOrgsv1() != nil { + timeoutDuration, err := time.ParseDuration(req.Timeout) + if err != nil { + return nil, err + } + ch := make(chan importResponse, 1) + ctxTimeout, cancel := context.WithTimeout(ctx, timeoutDuration) + defer cancel() + + go func() { + orgs := make([]*admin_pb.DataOrg, 0) + if req.GetDataOrgsv1() != nil { + dataOrgs, err := s.dataOrgsV1ToDataOrgs(ctx, req.GetDataOrgsv1()) + if err != nil { + ch <- importResponse{ret: nil, err: err} + return + } + orgs = dataOrgs.GetOrgs() + } else { + orgs = req.GetDataOrgs().GetOrgs() + } + + ret, count, err := s.importData(ctx, orgs) + ch <- importResponse{ret: ret, count: count, err: err} + }() + + select { + case <-ctxTimeout.Done(): + logging.Errorf("Import to response timeout: %v", ctxTimeout.Err()) + return nil, ctxTimeout.Err() + case result := <-ch: + logging.OnError(result.err).Errorf("error while importing: %v", result.err) + logging.Infof("Import done: %s", result.count.getProgress()) + return result.ret, result.err + } + } else { + v1Transformation := false + var gcsInput *admin_pb.ImportDataRequest_GCSInput + var s3Input *admin_pb.ImportDataRequest_S3Input + var localInput *admin_pb.ImportDataRequest_LocalInput + if req.GetDataOrgsGcs() != nil { + gcsInput = req.GetDataOrgsGcs() + } + if req.GetDataOrgsv1Gcs() != nil { + gcsInput = req.GetDataOrgsv1Gcs() + v1Transformation = true + } + if req.GetDataOrgsS3() != nil { + s3Input = req.GetDataOrgsS3() + } + if req.GetDataOrgsv1S3() != nil { + s3Input = req.GetDataOrgsv1S3() + v1Transformation = true + } + if req.GetDataOrgsLocal() != nil { + localInput = req.GetDataOrgsLocal() + } + if req.GetDataOrgsv1Local() != nil { + localInput = req.GetDataOrgsv1Local() + v1Transformation = true + } + + timeoutDuration, err := time.ParseDuration(req.Timeout) + if err != nil { + return nil, err + } + dctx := Detach(ctx) + go func() { + ch := make(chan importResponse, 1) + ctxTimeout, cancel := context.WithTimeout(dctx, timeoutDuration) + defer cancel() + go func() { + dataOrgs, err := s.transportDataFromFile(ctxTimeout, v1Transformation, gcsInput, s3Input, localInput) + if err != nil { + ch <- importResponse{nil, nil, err} + return + } + resp, count, err := s.importData(ctxTimeout, dataOrgs) + if err != nil { + ch <- importResponse{nil, count, err} + return + } + ch <- importResponse{resp, count, nil} + }() + + select { + case <-ctxTimeout.Done(): + logging.Errorf("Export to response timeout: %v", ctxTimeout.Err()) + return + case result := <-ch: + logging.OnError(result.err).Errorf("error while importing: %v", err) + if result.count != nil { + logging.Infof("Import done: %s", result.count.getProgress()) + } + } + }() + } + return &admin_pb.ImportDataResponse{}, nil +} + +func (s *Server) transportDataFromFile(ctx context.Context, v1Transformation bool, gcsInput *admin_pb.ImportDataRequest_GCSInput, s3Input *admin_pb.ImportDataRequest_S3Input, localInput *admin_pb.ImportDataRequest_LocalInput) (_ []*admin_pb.DataOrg, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + dataOrgs := make([]*admin_pb.DataOrg, 0) + data := make([]byte, 0) + if gcsInput != nil { + gcsData, err := getFileFromGCS(ctx, gcsInput) + if err != nil { + return nil, err + } + data = gcsData + } + if s3Input != nil { + s3Data, err := getFileFromS3(ctx, s3Input) + if err != nil { + return nil, err + } + data = s3Data + } + if localInput != nil { + localData, err := ioutil.ReadFile(localInput.Path) + if err != nil { + return nil, err + } + data = localData + } + + if v1Transformation { + dataImportV1 := new(v1_pb.ImportDataOrg) + if err := json.Unmarshal(data, dataImportV1); err != nil { + return nil, err + } + + dataImport, err := s.dataOrgsV1ToDataOrgs(ctx, dataImportV1) + if err != nil { + return nil, err + } + dataOrgs = dataImport.Orgs + } else { + dataImport := new(admin_pb.ImportDataOrg) + if err := json.Unmarshal(data, dataImport); err != nil { + return nil, err + } + dataOrgs = dataImport.Orgs + } + + return dataOrgs, nil +} + +func getFileFromS3(ctx context.Context, input *admin_pb.ImportDataRequest_S3Input) ([]byte, error) { + minioClient, err := minio.New(input.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(input.AccessKeyId, input.SecretAccessKey, ""), + Secure: input.Ssl, + }) + if err != nil { + return nil, err + } + + exists, err := minioClient.BucketExists(ctx, input.Bucket) + if err != nil { + return nil, err + } + if !exists { + return nil, fmt.Errorf("bucket not existing: %v", err) + } + + object, err := minioClient.GetObject(ctx, input.Bucket, input.Path, minio.GetObjectOptions{}) + if err != nil { + return nil, err + } + + defer object.Close() + return ioutil.ReadAll(object) +} + +func getFileFromGCS(ctx context.Context, input *admin_pb.ImportDataRequest_GCSInput) ([]byte, error) { + saJson, err := base64.StdEncoding.DecodeString(input.ServiceaccountJson) + if err != nil { + return nil, err + } + + client, err := storage.NewClient(ctx, option.WithCredentialsJSON(saJson)) + if err != nil { + return nil, err + } + + bucket := client.Bucket(input.Bucket) + reader, err := bucket.Object(input.Path).NewReader(ctx) + if err != nil { + return nil, err + } + defer reader.Close() + return ioutil.ReadAll(reader) +} + +func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*admin_pb.ImportDataResponse, *count, error) { + errors := make([]*admin_pb.ImportDataError, 0) + success := &admin_pb.ImportDataSuccess{} + count := &count{} + + appSecretGenerator, err := s.query.InitHashGenerator(ctx, domain.SecretGeneratorTypeAppSecret, s.passwordHashAlg) + if err != nil { + return nil, nil, err + } + initCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeInitCode, s.userCodeAlg) + if err != nil { + return nil, nil, err + } + phoneCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, s.userCodeAlg) + if err != nil { + return nil, nil, err + } + passwordlessInitCode, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordlessInitCode, s.userCodeAlg) + if err != nil { + return nil, nil, err + } + + ctxData := authz.GetCtxData(ctx) + for _, org := range orgs { + count.humanUserLen += len(org.GetHumanUsers()) + count.machineUserLen += len(org.GetMachineUsers()) + count.userMetadataLen += len(org.GetUserMetadata()) + count.userLinksLen += len(org.GetUserLinks()) + count.projectLen += len(org.GetProjects()) + count.oidcAppLen += len(org.GetOidcApps()) + count.apiAppLen += len(org.GetApiApps()) + count.actionLen += len(org.GetActions()) + count.projectRolesLen += len(org.GetProjectRoles()) + count.projectGrantLen += len(org.GetProjectGrants()) + count.userGrantLen += len(org.GetUserGrants()) + count.projectMembersLen += len(org.GetProjectMembers()) + count.orgMemberLen += len(org.GetOrgMembers()) + count.projectGrantMemberLen += len(org.GetProjectGrantMembers()) + } + + for _, org := range orgs { + _, err := s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) + } + successOrg := &admin_pb.ImportDataSuccessOrg{ + OrgId: org.GetOrgId(), + ProjectIds: []string{}, + OidcAppIds: []string{}, + ApiAppIds: []string{}, + HumanUserIds: []string{}, + MachineUserIds: []string{}, + ActionIds: []string{}, + ProjectGrants: []*admin_pb.ImportDataSuccessProjectGrant{}, + UserGrants: []*admin_pb.ImportDataSuccessUserGrant{}, + OrgMembers: []string{}, + ProjectMembers: []*admin_pb.ImportDataSuccessProjectMember{}, + ProjectGrantMembers: []*admin_pb.ImportDataSuccessProjectGrantMember{}, + } + logging.Debugf("successful org: %s", successOrg.OrgId) + success.Orgs = append(success.Orgs, successOrg) + + domainPolicy := org.GetDomainPolicy() + if org.DomainPolicy != nil { + _, err := s.command.AddOrgDomainPolicy(ctx, org.GetOrgId(), DomainPolicyToDomain(domainPolicy.UserLoginMustBeDomain, domainPolicy.ValidateOrgDomains, domainPolicy.SmtpSenderAddressMatchesInstanceDomain)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "domain_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + if org.Domains != nil { + for _, domainR := range org.Domains { + orgDomain := &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: org.GetOrgId(), + }, + Domain: domainR.DomainName, + Verified: domainR.IsVerified, + Primary: domainR.IsPrimary, + } + _, err := s.command.AddOrgDomain(ctx, org.GetOrgId(), domainR.DomainName, []string{}) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "domain", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + logging.Debugf("successful domain: %s", domainR.DomainName) + successOrg.Domains = append(successOrg.Domains, domainR.DomainName) + + if domainR.IsVerified { + if _, err := s.command.VerifyOrgDomain(ctx, org.GetOrgId(), domainR.DomainName); err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "domain_isverified", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + } + } + if domainR.IsPrimary { + if _, err := s.command.SetPrimaryOrgDomain(ctx, orgDomain); err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "domain_isprimary", Id: org.GetOrgId() + "_" + domainR.DomainName, Message: err.Error()}) + } + } + } + } + if org.LabelPolicy != nil { + _, err = s.command.AddLabelPolicy(ctx, org.GetOrgId(), management.AddLabelPolicyToDomain(org.GetLabelPolicy())) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "label_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + if org.LockoutPolicy != nil { + _, err = s.command.AddLockoutPolicy(ctx, org.GetOrgId(), management.AddLockoutPolicyToDomain(org.GetLockoutPolicy())) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "lockout_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + if org.OidcIdps != nil { + for _, idp := range org.OidcIdps { + logging.Debugf("import oidcidp: %s", idp.IdpId) + _, err := s.command.ImportIDPConfig(ctx, management.AddOIDCIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "oidc_idp", Id: idp.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + logging.Debugf("successful oidcidp: %s", idp.GetIdpId()) + successOrg.OidcIpds = append(successOrg.OidcIpds, idp.GetIdpId()) + } + } + if org.JwtIdps != nil { + for _, idp := range org.JwtIdps { + logging.Debugf("import jwtidp: %s", idp.IdpId) + _, err := s.command.ImportIDPConfig(ctx, management.AddJWTIDPRequestToDomain(idp.Idp), idp.IdpId, org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "jwt_idp", Id: idp.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + logging.Debugf("successful jwtidp: %s", idp.GetIdpId()) + successOrg.JwtIdps = append(successOrg.JwtIdps, idp.GetIdpId()) + } + } + if org.LoginPolicy != nil { + _, err = s.command.AddLoginPolicy(ctx, org.GetOrgId(), management.AddLoginPolicyToDomain(org.GetLoginPolicy())) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "login_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + if org.PasswordComplexityPolicy != nil { + _, err = s.command.AddPasswordComplexityPolicy(ctx, org.GetOrgId(), management.AddPasswordComplexityPolicyToDomain(org.GetPasswordComplexityPolicy())) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "password_complexity_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + if org.PrivacyPolicy != nil { + _, err = s.command.AddPrivacyPolicy(ctx, org.GetOrgId(), management.AddPrivacyPolicyToDomain(org.GetPrivacyPolicy())) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "privacy_policy", Id: org.GetOrgId(), Message: err.Error()}) + } + } + if org.LoginTexts != nil { + for _, text := range org.GetLoginTexts() { + _, err := s.command.SetOrgLoginText(ctx, org.GetOrgId(), management.SetLoginCustomTextToDomain(text)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "login_texts", Id: org.GetOrgId() + "_" + text.Language, Message: err.Error()}) + } + } + } + if org.InitMessages != nil { + for _, message := range org.GetInitMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetInitCustomTextToDomain(message)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "init_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } + } + if org.PasswordResetMessages != nil { + for _, message := range org.GetPasswordResetMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordResetCustomTextToDomain(message)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "password_reset_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } + } + if org.VerifyEmailMessages != nil { + for _, message := range org.GetVerifyEmailMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyEmailCustomTextToDomain(message)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "verify_email_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } + } + if org.VerifyPhoneMessages != nil { + for _, message := range org.GetVerifyPhoneMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetVerifyPhoneCustomTextToDomain(message)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "verify_phone_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } + } + if org.DomainClaimedMessages != nil { + for _, message := range org.GetDomainClaimedMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetDomainClaimedCustomTextToDomain(message)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "domain_claimed_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } + } + if org.PasswordlessRegistrationMessages != nil { + for _, message := range org.GetPasswordlessRegistrationMessages() { + _, err := s.command.SetOrgMessageText(ctx, authz.GetCtxData(ctx).OrgID, management.SetPasswordlessRegistrationCustomTextToDomain(message)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "passwordless_registration_message", Id: org.GetOrgId() + "_" + message.Language, Message: err.Error()}) + } + } + } + + if org.HumanUsers != nil { + for _, user := range org.GetHumanUsers() { + logging.Debugf("import user: %s", user.GetUserId()) + human, passwordless := management.ImportHumanUserRequestToDomain(user.User) + human.AggregateID = user.UserId + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, initCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + } else { + count.humanUserCount += 1 + logging.Debugf("successful user %d: %s", count.humanUserCount, user.GetUserId()) + successOrg.HumanUserIds = append(successOrg.HumanUserIds, user.GetUserId()) + } + + if user.User.OtpCode != "" { + logging.Debugf("import user otp: %s", user.GetUserId()) + if err := s.command.ImportHumanOTP(ctx, user.UserId, "", org.GetOrgId(), user.User.OtpCode); err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "human_user_otp", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + } else { + logging.Debugf("successful user otp: %s", user.GetUserId()) + } + } + } + } + if org.MachineUsers != nil { + for _, user := range org.GetMachineUsers() { + logging.Debugf("import user: %s", user.GetUserId()) + _, err := s.command.AddMachineWithID(ctx, org.GetOrgId(), user.GetUserId(), management.AddMachineUserRequestToDomain(user.GetUser())) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.machineUserCount += 1 + logging.Debugf("successful user %d: %s", count.machineUserCount, user.GetUserId()) + successOrg.MachineUserIds = append(successOrg.MachineUserIds, user.GetUserId()) + } + } + if org.UserMetadata != nil { + for _, userMetadata := range org.GetUserMetadata() { + logging.Debugf("import usermetadata: %s", userMetadata.GetId()+"_"+userMetadata.GetKey()) + _, err := s.command.SetUserMetadata(ctx, &domain.Metadata{Key: userMetadata.GetKey(), Value: userMetadata.GetValue()}, userMetadata.GetId(), org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "user_metadata", Id: userMetadata.GetId() + "_" + userMetadata.GetKey(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.userMetadataCount += 1 + logging.Debugf("successful usermetadata %d: %s", count.userMetadataCount, userMetadata.GetId()+"_"+userMetadata.GetKey()) + successOrg.UserMetadata = append(successOrg.UserMetadata, &admin_pb.ImportDataSuccessUserMetadata{UserId: userMetadata.GetId(), Key: userMetadata.GetKey()}) + } + } + if org.UserLinks != nil { + for _, userLinks := range org.GetUserLinks() { + logging.Debugf("import userlink: %s", userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) + externalIDP := &domain.UserIDPLink{ + ObjectRoot: es_models.ObjectRoot{AggregateID: userLinks.UserId}, + IDPConfigID: userLinks.IdpId, + ExternalUserID: userLinks.ProvidedUserId, + DisplayName: userLinks.ProvidedUserName, + } + if err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "user_link", Id: userLinks.UserId + "_" + userLinks.IdpId, Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.userLinksCount += 1 + logging.Debugf("successful userlink %d: %s", count.userLinksCount, userLinks.GetUserId()+"_"+userLinks.GetIdpId()+"_"+userLinks.GetProvidedUserId()+"_"+userLinks.GetProvidedUserName()) + successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) + } + } + if org.Projects != nil { + for _, project := range org.GetProjects() { + logging.Debugf("import project: %s", project.GetProjectId()) + _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.projectCount += 1 + logging.Debugf("successful project %d: %s", count.projectCount, project.GetProjectId()) + successOrg.ProjectIds = append(successOrg.ProjectIds, project.GetProjectId()) + } + } + if org.OidcApps != nil { + for _, app := range org.GetOidcApps() { + logging.Debugf("import oidcapplication: %s", app.GetAppId()) + _, err := s.command.AddOIDCApplicationWithID(ctx, management.AddOIDCAppRequestToDomain(app.App), org.GetOrgId(), app.GetAppId(), appSecretGenerator) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "oidc_app", Id: app.GetAppId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.oidcAppCount += 1 + logging.Debugf("successful oidcapplication %d: %s", count.oidcAppCount, app.GetAppId()) + successOrg.OidcAppIds = append(successOrg.OidcAppIds, app.GetAppId()) + } + } + if org.ApiApps != nil { + for _, app := range org.GetApiApps() { + logging.Debugf("import apiapplication: %s", app.GetAppId()) + _, err := s.command.AddAPIApplicationWithID(ctx, management.AddAPIAppRequestToDomain(app.GetApp()), org.GetOrgId(), app.GetAppId(), appSecretGenerator) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "api_app", Id: app.GetAppId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.apiAppCount += 1 + logging.Debugf("successful apiapplication %d: %s", count.apiAppCount, app.GetAppId()) + successOrg.ApiAppIds = append(successOrg.ApiAppIds, app.GetAppId()) + } + } + if org.Actions != nil { + for _, action := range org.GetActions() { + logging.Debugf("import action: %s", action.GetActionId()) + _, _, err := s.command.AddActionWithID(ctx, management.CreateActionRequestToDomain(action.GetAction()), org.GetOrgId(), action.GetActionId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "action", Id: action.GetActionId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.actionCount += 1 + logging.Debugf("successful action %d: %s", count.actionCount, action.GetActionId()) + successOrg.ActionIds = append(successOrg.ActionIds, action.ActionId) + } + } + if org.ProjectRoles != nil { + for _, role := range org.GetProjectRoles() { + logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) + _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.projectRolesCount += 1 + logging.Debugf("successful projectroles %d: %s", count.projectRolesCount, role.ProjectId+"_"+role.RoleKey) + successOrg.ProjectRoles = append(successOrg.ActionIds, role.ProjectId+"_"+role.RoleKey) + } + } + } + + for _, org := range orgs { + var successOrg *admin_pb.ImportDataSuccessOrg + for _, oldOrd := range success.Orgs { + if org.OrgId == oldOrd.OrgId { + successOrg = oldOrd + } + } + if org.TriggerActions != nil { + for _, triggerAction := range org.GetTriggerActions() { + _, err := s.command.SetTriggerActions(ctx, domain.FlowType(triggerAction.FlowType), domain.TriggerType(triggerAction.TriggerType), triggerAction.ActionIds, org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "trigger_action", Id: triggerAction.FlowType.String() + "_" + triggerAction.TriggerType.String(), Message: err.Error()}) + continue + } + successOrg.TriggerActions = append(successOrg.TriggerActions, &management_pb.SetTriggerActionsRequest{FlowType: triggerAction.FlowType, TriggerType: triggerAction.TriggerType, ActionIds: triggerAction.GetActionIds()}) + } + } + if org.ProjectGrants != nil { + for _, grant := range org.GetProjectGrants() { + logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) + _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.projectGrantCount += 1 + logging.Debugf("successful projectgrant %d: %s", count.projectGrantCount, grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) + successOrg.ProjectGrants = append(successOrg.ProjectGrants, &admin_pb.ImportDataSuccessProjectGrant{GrantId: grant.GetGrantId(), ProjectId: grant.GetProjectGrant().GetProjectId(), OrgId: grant.GetProjectGrant().GetGrantedOrgId()}) + } + } + if org.UserGrants != nil { + for _, grant := range org.GetUserGrants() { + logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId()) + _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.userGrantCount += 1 + logging.Debugf("successful usergrant %d: %s", count.userGrantCount, grant.GetProjectId()+"_"+grant.GetUserId()) + successOrg.UserGrants = append(successOrg.UserGrants, &admin_pb.ImportDataSuccessUserGrant{ProjectId: grant.GetProjectId(), UserId: grant.GetUserId()}) + } + } + } + + if success != nil && success.Orgs != nil { + for _, org := range orgs { + var successOrg *admin_pb.ImportDataSuccessOrg + for _, oldOrd := range success.Orgs { + if org.OrgId == oldOrd.OrgId { + successOrg = oldOrd + } + } + if successOrg == nil { + continue + } + + if org.OrgMembers != nil { + for _, member := range org.GetOrgMembers() { + logging.Debugf("import orgmember: %s", member.GetUserId()) + _, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.orgMemberCount += 1 + logging.Debugf("successful orgmember %d: %s", count.orgMemberCount, member.GetUserId()) + successOrg.OrgMembers = append(successOrg.OrgMembers, member.GetUserId()) + } + } + if org.ProjectGrantMembers != nil { + for _, member := range org.GetProjectGrantMembers() { + logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) + _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member)) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.projectGrantMemberCount += 1 + logging.Debugf("successful projectgrantmember %d: %s", count.projectGrantMemberCount, member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) + successOrg.ProjectGrantMembers = append(successOrg.ProjectGrantMembers, &admin_pb.ImportDataSuccessProjectGrantMember{ProjectId: member.GetProjectId(), GrantId: member.GetGrantId(), UserId: member.GetUserId()}) + } + } + if org.ProjectMembers != nil { + for _, member := range org.GetProjectMembers() { + logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId()) + _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId()) + if err != nil { + errors = append(errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()}) + if isCtxTimeout(ctx) { + return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err + } + continue + } + count.projectMembersCount += 1 + logging.Debugf("successful orgmember %d: %s", count.projectMembersCount, member.GetProjectId()+"_"+member.GetUserId()) + successOrg.ProjectMembers = append(successOrg.ProjectMembers, &admin_pb.ImportDataSuccessProjectMember{ProjectId: member.GetProjectId(), UserId: member.GetUserId()}) + } + } + } + } + return &admin_pb.ImportDataResponse{ + Errors: errors, + Success: success, + }, count, nil +} + +func (s *Server) dataOrgsV1ToDataOrgs(ctx context.Context, dataOrgs *v1_pb.ImportDataOrg) (_ *admin_pb.ImportDataOrg, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + orgs := make([]*admin_pb.DataOrg, 0) + for _, orgV1 := range dataOrgs.Orgs { + org := &admin_pb.DataOrg{ + OrgId: orgV1.GetOrgId(), + Org: orgV1.GetOrg(), + DomainPolicy: nil, + LabelPolicy: orgV1.GetLabelPolicy(), + LockoutPolicy: orgV1.GetLockoutPolicy(), + LoginPolicy: orgV1.GetLoginPolicy(), + PasswordComplexityPolicy: orgV1.GetPasswordComplexityPolicy(), + PrivacyPolicy: orgV1.GetPrivacyPolicy(), + Projects: orgV1.GetProjects(), + ProjectRoles: orgV1.GetProjectRoles(), + ApiApps: orgV1.GetApiApps(), + OidcApps: orgV1.GetOidcApps(), + HumanUsers: orgV1.GetHumanUsers(), + MachineUsers: orgV1.GetMachineUsers(), + TriggerActions: orgV1.GetTriggerActions(), + Actions: orgV1.GetActions(), + ProjectGrants: orgV1.GetProjectGrants(), + UserGrants: orgV1.GetUserGrants(), + OrgMembers: orgV1.GetOrgMembers(), + ProjectMembers: orgV1.GetProjectMembers(), + ProjectGrantMembers: orgV1.GetProjectGrantMembers(), + UserMetadata: orgV1.GetUserMetadata(), + LoginTexts: orgV1.GetLoginTexts(), + InitMessages: orgV1.GetInitMessages(), + PasswordResetMessages: orgV1.GetPasswordResetMessages(), + VerifyEmailMessages: orgV1.GetVerifyEmailMessages(), + VerifyPhoneMessages: orgV1.GetVerifyPhoneMessages(), + DomainClaimedMessages: orgV1.GetDomainClaimedMessages(), + PasswordlessRegistrationMessages: orgV1.GetPasswordlessRegistrationMessages(), + OidcIdps: orgV1.GetOidcIdps(), + JwtIdps: orgV1.GetJwtIdps(), + UserLinks: orgV1.GetUserLinks(), + Domains: orgV1.GetDomains(), + } + if orgV1.IamPolicy != nil { + defaultDomainPolicy, err := s.query.DefaultDomainPolicy(ctx) + if err != nil { + return nil, err + } + + org.DomainPolicy = &admin_pb.AddCustomDomainPolicyRequest{ + UserLoginMustBeDomain: orgV1.IamPolicy.UserLoginMustBeDomain, + ValidateOrgDomains: defaultDomainPolicy.ValidateOrgDomains, + SmtpSenderAddressMatchesInstanceDomain: defaultDomainPolicy.SMTPSenderAddressMatchesInstanceDomain, + } + } + if org.LoginPolicy != nil { + defaultLoginPolicy, err := s.query.DefaultLoginPolicy(ctx) + if err != nil { + return nil, err + } + org.LoginPolicy.ExternalLoginCheckLifetime = durationpb.New(defaultLoginPolicy.ExternalLoginCheckLifetime) + org.LoginPolicy.MultiFactorCheckLifetime = durationpb.New(defaultLoginPolicy.MultiFactorCheckLifetime) + org.LoginPolicy.SecondFactorCheckLifetime = durationpb.New(defaultLoginPolicy.SecondFactorCheckLifetime) + org.LoginPolicy.PasswordCheckLifetime = durationpb.New(defaultLoginPolicy.PasswordCheckLifetime) + org.LoginPolicy.MfaInitSkipLifetime = durationpb.New(defaultLoginPolicy.MFAInitSkipLifetime) + + if orgV1.SecondFactors != nil { + org.LoginPolicy.SecondFactors = make([]policy.SecondFactorType, len(orgV1.SecondFactors)) + for i, factor := range orgV1.SecondFactors { + org.LoginPolicy.SecondFactors[i] = factor.GetType() + } + } + if orgV1.MultiFactors != nil { + org.LoginPolicy.MultiFactors = make([]policy.MultiFactorType, len(orgV1.MultiFactors)) + for i, factor := range orgV1.MultiFactors { + org.LoginPolicy.MultiFactors[i] = factor.GetType() + } + } + if orgV1.Idps != nil { + org.LoginPolicy.Idps = make([]*management_pb.AddCustomLoginPolicyRequest_IDP, len(orgV1.Idps)) + for i, idpR := range orgV1.Idps { + org.LoginPolicy.Idps[i] = &management_pb.AddCustomLoginPolicyRequest_IDP{ + IdpId: idpR.GetIdpId(), + OwnerType: idpR.GetOwnerType(), + } + } + } + } + orgs = append(orgs, org) + } + + return &admin_pb.ImportDataOrg{ + Orgs: orgs, + }, nil +} + +func isCtxTimeout(ctx context.Context) bool { + select { + case <-ctx.Done(): + return true + default: + return false + } +} diff --git a/internal/api/grpc/admin/server.go b/internal/api/grpc/admin/server.go index 1665895847..60808f5b90 100644 --- a/internal/api/grpc/admin/server.go +++ b/internal/api/grpc/admin/server.go @@ -2,7 +2,6 @@ package admin import ( "context" - "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/admin/repository" @@ -11,6 +10,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -30,6 +30,7 @@ type Server struct { administrator repository.AdministratorRepository assetsAPIDomain func(context.Context) string userCodeAlg crypto.EncryptionAlgorithm + passwordHashAlg crypto.HashAlgorithm } type Config struct { @@ -40,6 +41,7 @@ func CreateServer( database string, command *command.Commands, query *query.Queries, + sd systemdefaults.SystemDefaults, repo repository.Repository, externalSecure bool, userCodeAlg crypto.EncryptionAlgorithm, @@ -51,6 +53,7 @@ func CreateServer( administrator: repo, assetsAPIDomain: assets.AssetAPI(externalSecure), userCodeAlg: userCodeAlg, + passwordHashAlg: crypto.NewBCrypt(sd.SecretGenerators.PasswordSaltCost), } } diff --git a/internal/api/grpc/management/actions.go b/internal/api/grpc/management/actions.go index 9c479155c9..67a25d16e4 100644 --- a/internal/api/grpc/management/actions.go +++ b/internal/api/grpc/management/actions.go @@ -35,7 +35,7 @@ func (s *Server) GetAction(ctx context.Context, req *mgmt_pb.GetActionRequest) ( } func (s *Server) CreateAction(ctx context.Context, req *mgmt_pb.CreateActionRequest) (*mgmt_pb.CreateActionResponse, error) { - id, details, err := s.command.AddAction(ctx, createActionRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + id, details, err := s.command.AddAction(ctx, CreateActionRequestToDomain(req), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/actions_converter.go b/internal/api/grpc/management/actions_converter.go index 873a36072a..d3e08a8bbe 100644 --- a/internal/api/grpc/management/actions_converter.go +++ b/internal/api/grpc/management/actions_converter.go @@ -9,7 +9,7 @@ import ( mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) -func createActionRequestToDomain(req *mgmt_pb.CreateActionRequest) *domain.Action { +func CreateActionRequestToDomain(req *mgmt_pb.CreateActionRequest) *domain.Action { return &domain.Action{ Name: req.Name, Script: req.Script, diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index 87041826d8..01f430c342 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -34,7 +34,7 @@ func (s *Server) ListOrgIDPs(ctx context.Context, req *mgmt_pb.ListOrgIDPsReques } func (s *Server) AddOrgOIDCIDP(ctx context.Context, req *mgmt_pb.AddOrgOIDCIDPRequest) (*mgmt_pb.AddOrgOIDCIDPResponse, error) { - config, err := s.command.AddIDPConfig(ctx, addOIDCIDPRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + config, err := s.command.AddIDPConfig(ctx, AddOIDCIDPRequestToDomain(req), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -49,7 +49,7 @@ func (s *Server) AddOrgOIDCIDP(ctx context.Context, req *mgmt_pb.AddOrgOIDCIDPRe } func (s *Server) AddOrgJWTIDP(ctx context.Context, req *mgmt_pb.AddOrgJWTIDPRequest) (*mgmt_pb.AddOrgJWTIDPResponse, error) { - config, err := s.command.AddIDPConfig(ctx, addJWTIDPRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + config, err := s.command.AddIDPConfig(ctx, AddJWTIDPRequestToDomain(req), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/idp_converter.go b/internal/api/grpc/management/idp_converter.go index 653375ed3c..b8dc6ba046 100644 --- a/internal/api/grpc/management/idp_converter.go +++ b/internal/api/grpc/management/idp_converter.go @@ -14,7 +14,7 @@ import ( mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) -func addOIDCIDPRequestToDomain(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.IDPConfig { +func AddOIDCIDPRequestToDomain(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.IDPConfig { return &domain.IDPConfig{ Name: req.Name, OIDCConfig: addOIDCIDPRequestToDomainOIDCIDPConfig(req), @@ -35,7 +35,7 @@ func addOIDCIDPRequestToDomainOIDCIDPConfig(req *mgmt_pb.AddOrgOIDCIDPRequest) * } } -func addJWTIDPRequestToDomain(req *mgmt_pb.AddOrgJWTIDPRequest) *domain.IDPConfig { +func AddJWTIDPRequestToDomain(req *mgmt_pb.AddOrgJWTIDPRequest) *domain.IDPConfig { return &domain.IDPConfig{ Name: req.Name, JWTConfig: addJWTIDPRequestToDomainJWTIDPConfig(req), diff --git a/internal/api/grpc/management/idp_converter_test.go b/internal/api/grpc/management/idp_converter_test.go index 7e5bbf0cb5..999be2ea06 100644 --- a/internal/api/grpc/management/idp_converter_test.go +++ b/internal/api/grpc/management/idp_converter_test.go @@ -35,7 +35,7 @@ func Test_addOIDCIDPRequestToDomain(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := addOIDCIDPRequestToDomain(tt.args.req) + got := AddOIDCIDPRequestToDomain(tt.args.req) test.AssertFieldsMapped(t, got, "ObjectRoot", "OIDCConfig.ClientSecret", diff --git a/internal/api/grpc/management/policy_label.go b/internal/api/grpc/management/policy_label.go index 3eaec40ebd..e76f97fb00 100644 --- a/internal/api/grpc/management/policy_label.go +++ b/internal/api/grpc/management/policy_label.go @@ -34,7 +34,7 @@ func (s *Server) GetDefaultLabelPolicy(ctx context.Context, req *mgmt_pb.GetDefa } func (s *Server) AddCustomLabelPolicy(ctx context.Context, req *mgmt_pb.AddCustomLabelPolicyRequest) (*mgmt_pb.AddCustomLabelPolicyResponse, error) { - policy, err := s.command.AddLabelPolicy(ctx, authz.GetCtxData(ctx).OrgID, addLabelPolicyToDomain(req)) + policy, err := s.command.AddLabelPolicy(ctx, authz.GetCtxData(ctx).OrgID, AddLabelPolicyToDomain(req)) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/policy_label_converter.go b/internal/api/grpc/management/policy_label_converter.go index 9918d2ea93..3ad0b3329e 100644 --- a/internal/api/grpc/management/policy_label_converter.go +++ b/internal/api/grpc/management/policy_label_converter.go @@ -5,7 +5,7 @@ import ( mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) -func addLabelPolicyToDomain(p *mgmt_pb.AddCustomLabelPolicyRequest) *domain.LabelPolicy { +func AddLabelPolicyToDomain(p *mgmt_pb.AddCustomLabelPolicyRequest) *domain.LabelPolicy { return &domain.LabelPolicy{ PrimaryColor: p.PrimaryColor, BackgroundColor: p.BackgroundColor, diff --git a/internal/api/grpc/management/policy_login.go b/internal/api/grpc/management/policy_login.go index 25273b81de..78029c79a5 100644 --- a/internal/api/grpc/management/policy_login.go +++ b/internal/api/grpc/management/policy_login.go @@ -30,7 +30,7 @@ func (s *Server) GetDefaultLoginPolicy(ctx context.Context, req *mgmt_pb.GetDefa } func (s *Server) AddCustomLoginPolicy(ctx context.Context, req *mgmt_pb.AddCustomLoginPolicyRequest) (*mgmt_pb.AddCustomLoginPolicyResponse, error) { - policy, err := s.command.AddLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, addLoginPolicyToDomain(req)) + policy, err := s.command.AddLoginPolicy(ctx, authz.GetCtxData(ctx).OrgID, AddLoginPolicyToDomain(req)) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/policy_login_converter.go b/internal/api/grpc/management/policy_login_converter.go index 6b2c6cb88a..8f7a632ece 100644 --- a/internal/api/grpc/management/policy_login_converter.go +++ b/internal/api/grpc/management/policy_login_converter.go @@ -9,7 +9,7 @@ import ( mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management" ) -func addLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.LoginPolicy { +func AddLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.LoginPolicy { return &domain.LoginPolicy{ AllowUsernamePassword: p.AllowUsernamePassword, AllowRegister: p.AllowRegister, diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 7aa0e1a3e0..507fcce199 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -184,6 +184,21 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe } func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) { + details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToAddHuman(req)) + if err != nil { + return nil, err + } + return &mgmt_pb.AddHumanUserResponse{ + UserId: details.ID, + Details: obj_grpc.AddToDetailsPb( + details.Sequence, + details.EventDate, + details.ResourceOwner, + ), + }, nil +} + +func AddHumanUserRequestToAddHuman(req *mgmt_pb.AddHumanUserRequest) *command.AddHuman { lang, err := language.Parse(req.Profile.PreferredLanguage) logging.OnError(err).Debug("unable to parse language") @@ -211,18 +226,7 @@ func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequ Verified: req.Phone.IsPhoneVerified, } } - details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human) - if err != nil { - return nil, err - } - return &mgmt_pb.AddHumanUserResponse{ - UserId: details.ID, - Details: obj_grpc.AddToDetailsPb( - details.Sequence, - details.EventDate, - details.ResourceOwner, - ), - }, nil + return human } func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUserRequest) (*mgmt_pb.ImportHumanUserResponse, error) { diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 77f1d7c09b..97ff4ec443 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -142,11 +142,16 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human IsPhoneVerified: req.Phone.IsPhoneVerified, } } + if req.Password != "" { - human.Password = &domain.Password{SecretString: req.Password} + human.Password = domain.NewPassword(req.Password) human.Password.ChangeRequired = req.PasswordChangeRequired } + if req.HashedPassword != nil && req.HashedPassword.Value != "" && req.HashedPassword.Algorithm != "" { + human.HashedPassword = domain.NewHashedPassword(req.HashedPassword.Value, req.HashedPassword.Algorithm) + } + return human, req.RequestPasswordlessRegistration } diff --git a/internal/command/org.go b/internal/command/org.go index 6eb3aa6a8c..d40c7eec36 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -21,17 +21,19 @@ type OrgSetup struct { Roles []string } -func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string) (string, *domain.ObjectDetails, error) { - orgID, err := c.idGenerator.Next() +func (c *Commands) SetUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, *domain.ObjectDetails, error) { + existingOrg, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return "", nil, err } - - userID, err := c.idGenerator.Next() - if err != nil { - return "", nil, err + if existingOrg != nil { + return "", nil, errors.ThrowPreconditionFailed(nil, "COMMAND-poaj2", "Errors.Org.AlreadyExisting") } + return c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) +} + +func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(orgID) userAgg := user_repo.NewAggregate(userID, orgID) @@ -65,6 +67,20 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string) }, nil } +func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string) (string, *domain.ObjectDetails, error) { + orgID, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + + userID, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + + return c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) +} + //AddOrgCommand defines the commands to create a new org, // this includes the verified default domain func AddOrgCommand(ctx context.Context, a *org.Aggregate, name string, userIDs ...string) preparation.Validation { @@ -106,8 +122,33 @@ func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error { return nil } +func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (*domain.Org, error) { + existingOrg, err := c.getOrgWriteModelByID(ctx, orgID) + if err != nil { + return nil, err + } + if existingOrg.State != domain.OrgStateUnspecified { + return nil, caos_errs.ThrowNotFound(nil, "ORG-lapo2m", "Errors.Org.AlreadyExisting") + } + + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) +} + func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner string, claimedUserIDs []string) (*domain.Org, error) { - orgAgg, addedOrg, events, err := c.addOrg(ctx, &domain.Org{Name: name}, claimedUserIDs) + if name == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-Mf9sd", "Errors.Org.Invalid") + } + + orgID, err := c.idGenerator.Next() + if err != nil { + return nil, caos_errs.ThrowInternal(err, "COMMA-OwciI", "Errors.Internal") + } + + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) +} + +func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (*domain.Org, error) { + orgAgg, addedOrg, events, err := c.addOrgWithID(ctx, &domain.Org{Name: name}, orgID, claimedUserIDs) if err != nil { return nil, err } @@ -136,6 +177,7 @@ func (c *Commands) ChangeOrg(ctx context.Context, orgID, name string) (*domain.O if orgID == "" || name == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "EVENT-Mf9sd", "Errors.Org.Invalid") } + orgWriteModel, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return nil, err @@ -242,15 +284,12 @@ func ExistsOrg(ctx context.Context, filter preparation.FilterToQueryReducer, id return exists, nil } -func (c *Commands) addOrg(ctx context.Context, organisation *domain.Org, claimedUserIDs []string) (_ *eventstore.Aggregate, _ *OrgWriteModel, _ []eventstore.Command, err error) { +func (c *Commands) addOrgWithID(ctx context.Context, organisation *domain.Org, orgID string, claimedUserIDs []string) (_ *eventstore.Aggregate, _ *OrgWriteModel, _ []eventstore.Command, err error) { if !organisation.IsValid() { return nil, nil, nil, caos_errs.ThrowInvalidArgument(nil, "COMM-deLSk", "Errors.Org.Invalid") } - organisation.AggregateID, err = c.idGenerator.Next() - if err != nil { - return nil, nil, nil, caos_errs.ThrowInternal(err, "COMMA-OwciI", "Errors.Internal") - } + organisation.AggregateID = orgID organisation.AddIAMDomain(authz.GetInstance(ctx).RequestedDomain()) addedOrg := NewOrgWriteModel(organisation.AggregateID) diff --git a/internal/command/org_action.go b/internal/command/org_action.go index aeeb5f5879..45cf13acfa 100644 --- a/internal/command/org_action.go +++ b/internal/command/org_action.go @@ -11,14 +11,33 @@ import ( "github.com/zitadel/zitadel/internal/repository/org" ) +func (c *Commands) AddActionWithID(ctx context.Context, addAction *domain.Action, resourceOwner, actionID string) (_ string, _ *domain.ObjectDetails, err error) { + existingAction, err := c.getActionWriteModelByID(ctx, actionID, resourceOwner) + if err != nil { + return "", nil, err + } + if existingAction.State != domain.ActionStateUnspecified { + return "", nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-nau2k", "Errors.Action.AlreadyExisting") + } + + return c.addActionWithID(ctx, addAction, resourceOwner, actionID) +} + func (c *Commands) AddAction(ctx context.Context, addAction *domain.Action, resourceOwner string) (_ string, _ *domain.ObjectDetails, err error) { if !addAction.IsValid() { return "", nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-eg2gf", "Errors.Action.Invalid") } - addAction.AggregateID, err = c.idGenerator.Next() + + actionID, err := c.idGenerator.Next() if err != nil { return "", nil, err } + + return c.addActionWithID(ctx, addAction, resourceOwner, actionID) +} + +func (c *Commands) addActionWithID(ctx context.Context, addAction *domain.Action, resourceOwner, actionID string) (_ string, _ *domain.ObjectDetails, err error) { + addAction.AggregateID = actionID actionModel := NewActionWriteModel(addAction.AggregateID, resourceOwner) actionAgg := ActionAggregateFromWriteModel(&actionModel.WriteModel) diff --git a/internal/command/org_domain.go b/internal/command/org_domain.go index 5270bbc19b..dc108001ba 100644 --- a/internal/command/org_domain.go +++ b/internal/command/org_domain.go @@ -51,7 +51,7 @@ func (c *Commands) prepareAddOrgDomain(a *org.Aggregate, addDomain string, userI } } -func VerifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation { +func verifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation { return func() (preparation.CreateCommands, error) { if domain = strings.TrimSpace(domain); domain == "" { return nil, errors.ThrowInvalidArgument(nil, "ORG-yqlVQ", "Errors.Invalid.Argument") @@ -63,7 +63,7 @@ func VerifyOrgDomain(a *org.Aggregate, domain string) preparation.Validation { } } -func SetPrimaryOrgDomain(a *org.Aggregate, domain string) preparation.Validation { +func setPrimaryOrgDomain(a *org.Aggregate, domain string) preparation.Validation { return func() (preparation.CreateCommands, error) { if domain = strings.TrimSpace(domain); domain == "" { return nil, errors.ThrowInvalidArgument(nil, "ORG-gmNqY", "Errors.Invalid.Argument") @@ -101,6 +101,19 @@ func orgDomain(ctx context.Context, filter preparation.FilterToQueryReducer, org return wm, nil } +func (c *Commands) VerifyOrgDomain(ctx context.Context, orgID, domain string) (*domain.ObjectDetails, error) { + orgAgg := org.NewAggregate(orgID) + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, verifyOrgDomain(orgAgg, domain)) + if err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, cmds...) + if err != nil { + return nil, err + } + return pushedEventsToObjectDetails(pushedEvents), nil +} + func (c *Commands) AddOrgDomain(ctx context.Context, orgID, domain string, claimedUserIDs []string) (*domain.ObjectDetails, error) { orgAgg := org.NewAggregate(orgID) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareAddOrgDomain(orgAgg, domain, claimedUserIDs)) diff --git a/internal/command/org_domain_test.go b/internal/command/org_domain_test.go index 001992406e..230fc5601a 100644 --- a/internal/command/org_domain_test.go +++ b/internal/command/org_domain_test.go @@ -178,7 +178,7 @@ func TestVerifyDomain(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, context.Background(), VerifyOrgDomain(tt.args.a, tt.args.domain), nil, tt.want) + AssertValidation(t, context.Background(), verifyOrgDomain(tt.args.a, tt.args.domain), nil, tt.want) }) } } @@ -273,7 +273,7 @@ func TestSetDomainPrimary(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, context.Background(), SetPrimaryOrgDomain(tt.args.a, tt.args.domain), tt.args.filter, tt.want) + AssertValidation(t, context.Background(), setPrimaryOrgDomain(tt.args.a, tt.args.domain), tt.args.filter, tt.want) }) } } diff --git a/internal/command/org_idp_config.go b/internal/command/org_idp_config.go index 21647263ac..f135a4109a 100644 --- a/internal/command/org_idp_config.go +++ b/internal/command/org_idp_config.go @@ -11,6 +11,17 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) +func (c *Commands) ImportIDPConfig(ctx context.Context, config *domain.IDPConfig, idpConfigID, resourceOwner string) (*domain.IDPConfig, error) { + existingIDP, err := c.orgIDPConfigWriteModelByID(ctx, idpConfigID, resourceOwner) + if err != nil { + return nil, err + } + if existingIDP.State != domain.IDPConfigStateRemoved && existingIDP.State != domain.IDPConfigStateUnspecified { + return nil, errors.ThrowNotFound(nil, "Org-1J8fs", "Errors.Org.IDPConfig.AlreadyExisting") + } + return c.addIDPConfig(ctx, config, idpConfigID, resourceOwner) +} + func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, resourceOwner string) (*domain.IDPConfig, error) { if resourceOwner == "" { return nil, errors.ThrowInvalidArgument(nil, "Org-0j8gs", "Errors.ResourceOwnerMissing") @@ -18,11 +29,16 @@ func (c *Commands) AddIDPConfig(ctx context.Context, config *domain.IDPConfig, r if config.OIDCConfig == nil && config.JWTConfig == nil { return nil, errors.ThrowInvalidArgument(nil, "Org-eUpQU", "Errors.idp.config.notset") } - idpConfigID, err := c.idGenerator.Next() if err != nil { return nil, err } + + return c.addIDPConfig(ctx, config, idpConfigID, resourceOwner) +} + +func (c *Commands) addIDPConfig(ctx context.Context, config *domain.IDPConfig, idpConfigID, resourceOwner string) (*domain.IDPConfig, error) { + addedConfig := NewOrgIDPConfigWriteModel(idpConfigID, resourceOwner) orgAgg := OrgAggregateFromWriteModel(&addedConfig.WriteModel) diff --git a/internal/command/project.go b/internal/command/project.go index 07a8bbc478..8b795ed07b 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -13,11 +13,46 @@ import ( "github.com/zitadel/zitadel/internal/repository/project" ) -func (c *Commands) AddProject(ctx context.Context, project *domain.Project, resourceOwner, ownerUserID string) (_ *domain.Project, err error) { - events, addedProject, err := c.addProject(ctx, project, resourceOwner, ownerUserID) +func (c *Commands) AddProjectWithID(ctx context.Context, project *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { + existingProject, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) if err != nil { return nil, err } + if existingProject.State != domain.ProjectStateUnspecified { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-opamwu", "Errors.Project.AlreadyExisting") + } + return c.addProjectWithID(ctx, project, resourceOwner, projectID) +} + +func (c *Commands) AddProject(ctx context.Context, project *domain.Project, resourceOwner, ownerUserID string) (_ *domain.Project, err error) { + if !project.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") + } + + projectID, err := c.idGenerator.Next() + if err != nil { + return nil, err + } + + return c.addProjectWithIDWithOwner(ctx, project, resourceOwner, ownerUserID, projectID) +} + +func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { + projectAdd.AggregateID = projectID + addedProject := NewProjectWriteModel(projectAdd.AggregateID, resourceOwner) + projectAgg := ProjectAggregateFromWriteModel(&addedProject.WriteModel) + + events := []eventstore.Command{ + project.NewProjectAddedEvent( + ctx, + projectAgg, + projectAdd.Name, + projectAdd.ProjectRoleAssertion, + projectAdd.ProjectRoleCheck, + projectAdd.HasProjectCheck, + projectAdd.PrivateLabelingSetting), + } + pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err @@ -29,14 +64,11 @@ func (c *Commands) AddProject(ctx context.Context, project *domain.Project, reso return projectWriteModelToProject(addedProject), nil } -func (c *Commands) addProject(ctx context.Context, projectAdd *domain.Project, resourceOwner, ownerUserID string) (_ []eventstore.Command, _ *ProjectWriteModel, err error) { +func (c *Commands) addProjectWithIDWithOwner(ctx context.Context, projectAdd *domain.Project, resourceOwner, ownerUserID, projectID string) (_ *domain.Project, err error) { if !projectAdd.IsValid() { - return nil, nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") - } - projectAdd.AggregateID, err = c.idGenerator.Next() - if err != nil { - return nil, nil, err + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") } + projectAdd.AggregateID = projectID addedProject := NewProjectWriteModel(projectAdd.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedProject.WriteModel) @@ -52,7 +84,16 @@ func (c *Commands) addProject(ctx context.Context, projectAdd *domain.Project, r projectAdd.PrivateLabelingSetting), project.NewProjectMemberAddedEvent(ctx, projectAgg, ownerUserID, projectRole), } - return events, addedProject, nil + + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(addedProject, pushedEvents...) + if err != nil { + return nil, err + } + return projectWriteModelToProject(addedProject), nil } func AddProjectCommand( diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index 87a1dabec2..2046d3dc62 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -70,21 +70,70 @@ func (c *Commands) AddAPIAppCommand(app *addAPIApp, clientSecretAlg crypto.HashA } } -func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.APIApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.APIApp, err error) { - if application == nil || application.AggregateID == "" { - return nil, errors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Application.Invalid") - } - project, err := c.getProjectByID(ctx, application.AggregateID, resourceOwner) - if err != nil { - return nil, errors.ThrowPreconditionFailed(err, "PROJECT-9fnsf", "Errors.Project.NotFound") - } - addedApplication := NewAPIApplicationWriteModel(application.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) - events, stringPw, err := c.addAPIApplication(ctx, projectAgg, project, application, resourceOwner, appSecretGenerator) +func (c *Commands) AddAPIApplicationWithID(ctx context.Context, apiApp *domain.APIApp, resourceOwner, appID string, appSecretGenerator crypto.Generator) (_ *domain.APIApp, err error) { + existingAPI, err := c.getAPIAppWriteModel(ctx, apiApp.AggregateID, appID, resourceOwner) if err != nil { return nil, err } - addedApplication.AppID = application.AppID + if existingAPI.State != domain.AppStateUnspecified { + return nil, errors.ThrowPreconditionFailed(nil, "PROJECT-mabu12", "Errors.Application.AlreadyExisting") + } + project, err := c.getProjectByID(ctx, apiApp.AggregateID, resourceOwner) + if err != nil { + return nil, errors.ThrowPreconditionFailed(err, "PROJECT-9fnsa", "Errors.Project.NotFound") + } + + return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, project, appID, appSecretGenerator) +} + +func (c *Commands) AddAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.APIApp, err error) { + if apiApp == nil || apiApp.AggregateID == "" { + return nil, errors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Application.Invalid") + } + project, err := c.getProjectByID(ctx, apiApp.AggregateID, resourceOwner) + if err != nil { + return nil, errors.ThrowPreconditionFailed(err, "PROJECT-9fnsf", "Errors.Project.NotFound") + } + + if !apiApp.IsValid() { + return nil, errors.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") + } + + appID, err := c.idGenerator.Next() + if err != nil { + return nil, err + } + + return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, project, appID, appSecretGenerator) +} + +func (c *Commands) addAPIApplicationWithID(ctx context.Context, apiApp *domain.APIApp, resourceOwner string, project *domain.Project, appID string, appSecretGenerator crypto.Generator) (_ *domain.APIApp, err error) { + apiApp.AppID = appID + + addedApplication := NewAPIApplicationWriteModel(apiApp.AggregateID, resourceOwner) + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) + + events := []eventstore.Command{ + project_repo.NewApplicationAddedEvent(ctx, projectAgg, apiApp.AppID, apiApp.AppName), + } + + var stringPw string + err = domain.SetNewClientID(apiApp, c.idGenerator, project) + if err != nil { + return nil, err + } + stringPw, err = domain.SetNewClientSecretIfNeeded(apiApp, appSecretGenerator) + if err != nil { + return nil, err + } + events = append(events, project_repo.NewAPIConfigAddedEvent(ctx, + projectAgg, + apiApp.AppID, + apiApp.ClientID, + apiApp.ClientSecret, + apiApp.AuthMethodType)) + + addedApplication.AppID = apiApp.AppID pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err @@ -98,38 +147,6 @@ func (c *Commands) AddAPIApplication(ctx context.Context, application *domain.AP return result, nil } -func (c *Commands) addAPIApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, apiAppApp *domain.APIApp, resourceOwner string, appSecretGenerator crypto.Generator) (events []eventstore.Command, stringPW string, err error) { - if !apiAppApp.IsValid() { - return nil, "", errors.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Application.Invalid") - } - apiAppApp.AppID, err = c.idGenerator.Next() - if err != nil { - return nil, "", err - } - - events = []eventstore.Command{ - project_repo.NewApplicationAddedEvent(ctx, projectAgg, apiAppApp.AppID, apiAppApp.AppName), - } - - var stringPw string - err = domain.SetNewClientID(apiAppApp, c.idGenerator, proj) - if err != nil { - return nil, "", err - } - stringPw, err = domain.SetNewClientSecretIfNeeded(apiAppApp, appSecretGenerator) - if err != nil { - return nil, "", err - } - events = append(events, project_repo.NewAPIConfigAddedEvent(ctx, - projectAgg, - apiAppApp.AppID, - apiAppApp.ClientID, - apiAppApp.ClientSecret, - apiAppApp.AuthMethodType)) - - return events, stringPw, nil -} - func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) { if apiApp.AppID == "" || apiApp.AggregateID == "" { return nil, errors.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid") diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index aabb310448..4deb39653c 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -116,56 +116,63 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp, clientSecretAlg crypto.Has } } -func (c *Commands) AddOIDCApplication(ctx context.Context, application *domain.OIDCApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.OIDCApp, err error) { - if application == nil || application.AggregateID == "" { +func (c *Commands) AddOIDCApplicationWithID(ctx context.Context, oidcApp *domain.OIDCApp, resourceOwner, appID string, appSecretGenerator crypto.Generator) (_ *domain.OIDCApp, err error) { + existingApp, err := c.getOIDCAppWriteModel(ctx, oidcApp.AggregateID, appID, resourceOwner) + if err != nil { + return nil, err + } + if existingApp.State != domain.AppStateUnspecified { + return nil, caos_errs.ThrowPreconditionFailed(nil, "PROJECT-lxowmp", "Errors.Application.AlreadyExisting") + } + + project, err := c.getProjectByID(ctx, oidcApp.AggregateID, resourceOwner) + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-3m9s2", "Errors.Project.NotFound") + } + + return c.addOIDCApplicationWithID(ctx, oidcApp, resourceOwner, project, appID, appSecretGenerator) +} + +func (c *Commands) AddOIDCApplication(ctx context.Context, oidcApp *domain.OIDCApp, resourceOwner string, appSecretGenerator crypto.Generator) (_ *domain.OIDCApp, err error) { + if oidcApp == nil || oidcApp.AggregateID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Application.Invalid") } - project, err := c.getProjectByID(ctx, application.AggregateID, resourceOwner) + project, err := c.getProjectByID(ctx, oidcApp.AggregateID, resourceOwner) if err != nil { return nil, caos_errs.ThrowPreconditionFailed(err, "PROJECT-3m9ss", "Errors.Project.NotFound") } - addedApplication := NewOIDCApplicationWriteModel(application.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) - events, stringPw, err := c.addOIDCApplication(ctx, projectAgg, project, application, resourceOwner, appSecretGenerator) + + if oidcApp.AppName == "" || !oidcApp.IsValid() { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-1n8df", "Errors.Application.Invalid") + } + + appID, err := c.idGenerator.Next() if err != nil { return nil, err } - addedApplication.AppID = application.AppID - pushedEvents, err := c.eventstore.Push(ctx, events...) - if err != nil { - return nil, err - } - err = AppendAndReduce(addedApplication, pushedEvents...) - if err != nil { - return nil, err - } - result := oidcWriteModelToOIDCConfig(addedApplication) - result.ClientSecretString = stringPw - result.FillCompliance() - return result, nil + + return c.addOIDCApplicationWithID(ctx, oidcApp, resourceOwner, project, appID, appSecretGenerator) } -func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstore.Aggregate, proj *domain.Project, oidcApp *domain.OIDCApp, resourceOwner string, appSecretGenerator crypto.Generator) (events []eventstore.Command, stringPW string, err error) { - if oidcApp.AppName == "" || !oidcApp.IsValid() { - return nil, "", caos_errs.ThrowInvalidArgument(nil, "PROJECT-1n8df", "Errors.Application.Invalid") - } - oidcApp.AppID, err = c.idGenerator.Next() - if err != nil { - return nil, "", err - } +func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain.OIDCApp, resourceOwner string, project *domain.Project, appID string, appSecretGenerator crypto.Generator) (_ *domain.OIDCApp, err error) { - events = []eventstore.Command{ + addedApplication := NewOIDCApplicationWriteModel(oidcApp.AggregateID, resourceOwner) + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) + + oidcApp.AppID = appID + + events := []eventstore.Command{ project_repo.NewApplicationAddedEvent(ctx, projectAgg, oidcApp.AppID, oidcApp.AppName), } var stringPw string - err = domain.SetNewClientID(oidcApp, c.idGenerator, proj) + err = domain.SetNewClientID(oidcApp, c.idGenerator, project) if err != nil { - return nil, "", err + return nil, err } stringPw, err = domain.SetNewClientSecretIfNeeded(oidcApp, appSecretGenerator) if err != nil { - return nil, "", err + return nil, err } events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx, projectAgg, @@ -187,7 +194,19 @@ func (c *Commands) addOIDCApplication(ctx context.Context, projectAgg *eventstor oidcApp.ClockSkew, oidcApp.AdditionalOrigins)) - return events, stringPw, nil + addedApplication.AppID = oidcApp.AppID + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(addedApplication, pushedEvents...) + if err != nil { + return nil, err + } + result := oidcWriteModelToOIDCConfig(addedApplication) + result.ClientSecretString = stringPw + result.FillCompliance() + return result, nil } func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) { diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index e484f33cfc..9e83044e3d 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -12,6 +12,18 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) +func (c *Commands) AddProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { + existingMember, err := c.projectGrantWriteModelByID(ctx, grantID, grant.AggregateID, resourceOwner) + if err != nil && !caos_errs.IsNotFound(err) { + return nil, err + } + if existingMember != nil && existingMember.State != domain.ProjectGrantStateUnspecified { + return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-2b8fs", "Errors.Project.Grant.AlreadyExisting") + } + + return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) +} + func (c *Commands) AddProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string) (_ *domain.ProjectGrant, err error) { if !grant.IsValid() { return nil, caos_errs.ThrowInvalidArgument(nil, "PROJECT-3b8fs", "Errors.Project.Grant.Invalid") @@ -20,10 +32,18 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *domain.ProjectGra if err != nil { return nil, err } - grant.GrantID, err = c.idGenerator.Next() + + grantID, err := c.idGenerator.Next() if err != nil { return nil, err } + + return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) +} + +func (c *Commands) addProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { + grant.GrantID = grantID + addedGrant := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, resourceOwner) projectAgg := ProjectAggregateFromWriteModel(&addedGrant.WriteModel) pushedEvents, err := c.eventstore.Push( diff --git a/internal/command/user_human.go b/internal/command/user_human.go index dfed12d4e6..b2efd78d42 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -47,6 +47,8 @@ type AddHuman struct { Phone Phone //Password is optional Password string + //BcryptedPassword is optional + BcryptedPassword string //PasswordChangeRequired is used if the `Password`-field is set PasswordChangeRequired bool Passwordless bool @@ -54,14 +56,19 @@ type AddHuman struct { Register bool } -func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) { - if resourceOwner == "" { - return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal") - } - userID, err := c.idGenerator.Next() +func (c *Commands) AddHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) { + existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceOwner) if err != nil { return nil, err } + if isUserStateExists(existingHuman.UserState) { + return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting") + } + + return c.addHumanWithID(ctx, resourceOwner, userID, human) +} + +func (c *Commands) addHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) { agg := user.NewAggregate(userID, resourceOwner) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddHumanCommand(agg, human, c.userPasswordAlg, c.userEncryption)) if err != nil { @@ -83,6 +90,18 @@ func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *Ad }, nil } +func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) { + if resourceOwner == "" { + return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal") + } + userID, err := c.idGenerator.Next() + if err != nil { + return nil, err + } + + return c.addHumanWithID(ctx, resourceOwner, userID, human) +} + type humanCreationCommand interface { eventstore.Command AddPhoneData(phoneNumber string) @@ -167,6 +186,10 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash createCmd.AddPasswordData(secret, human.PasswordChangeRequired) } + if human.BcryptedPassword != "" { + createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired) + } + cmds := make([]eventstore.Command, 0, 3) cmds = append(cmds, createCmd) @@ -274,6 +297,18 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. if err != nil { return nil, nil, errors.ThrowPreconditionFailed(err, "COMMAND-4N8gs", "Errors.Org.PasswordComplexityPolicy.NotFound") } + + if human.AggregateID != "" { + existing, err := c.getHumanWriteModelByID(ctx, human.AggregateID, human.ResourceOwner) + if err != nil { + return nil, nil, err + } + + if existing.UserState != domain.UserStateUnspecified { + return nil, nil, errors.ThrowPreconditionFailed(nil, "COMMAND-ziuna", "Errors.User.AlreadyExisting") + } + } + events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err @@ -355,8 +390,8 @@ func (c *Commands) addHuman(ctx context.Context, orgID string, human *domain.Hum if orgID == "" || !human.IsValid() { return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-67Ms8", "Errors.User.Invalid") } - if human.Password != nil && human.SecretString != "" { - human.ChangeRequired = true + if human.Password != nil && human.Password.SecretString != "" { + human.Password.ChangeRequired = true } return c.createHuman(ctx, orgID, human, nil, false, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) } @@ -384,11 +419,11 @@ func (c *Commands) registerHuman(ctx context.Context, orgID string, human *domai if human != nil && human.Username == "" { human.Username = human.EmailAddress } - if orgID == "" || !human.IsValid() || link == nil && (human.Password == nil || human.SecretString == "") { + if orgID == "" || !human.IsValid() || link == nil && (human.Password == nil || human.Password.SecretString == "") { return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-9dk45", "Errors.User.Invalid") } - if human.Password != nil && human.SecretString != "" { - human.ChangeRequired = false + if human.Password != nil && human.Password.SecretString != "" { + human.Password.ChangeRequired = false } return c.createHuman(ctx, orgID, human, link, true, false, domainPolicy, pwPolicy, initCodeGenerator, phoneCodeGenerator) } @@ -410,14 +445,18 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. return nil, nil, errors.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername") } } - userID, err := c.idGenerator.Next() - if err != nil { - return nil, nil, err + + if human.AggregateID == "" { + userID, err := c.idGenerator.Next() + if err != nil { + return nil, nil, err + } + human.AggregateID = userID } - human.AggregateID = userID + human.SetNamesAsDisplayname() if human.Password != nil { - if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg, human.ChangeRequired); err != nil { + if err := human.HashPasswordIfExisting(pwPolicy, c.userPasswordAlg, human.Password.ChangeRequired); err != nil { return nil, nil, err } } @@ -510,7 +549,10 @@ func createAddHumanEvent(ctx context.Context, aggregate *eventstore.Aggregate, h human.StreetAddress) } if human.Password != nil { - addEvent.AddPasswordData(human.SecretCrypto, human.ChangeRequired) + addEvent.AddPasswordData(human.Password.SecretCrypto, human.Password.ChangeRequired) + } + if human.HashedPassword != nil { + addEvent.AddPasswordData(human.HashedPassword.SecretCrypto, false) } return addEvent } @@ -541,7 +583,10 @@ func createRegisterHumanEvent(ctx context.Context, aggregate *eventstore.Aggrega human.StreetAddress) } if human.Password != nil { - addEvent.AddPasswordData(human.SecretCrypto, human.ChangeRequired) + addEvent.AddPasswordData(human.Password.SecretCrypto, human.Password.ChangeRequired) + } + if human.HashedPassword != nil { + addEvent.AddPasswordData(human.HashedPassword.SecretCrypto, false) } return addEvent } diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 960c346ae0..725a0b10e7 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -2,6 +2,7 @@ package command import ( "context" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/domain" @@ -11,6 +12,28 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) +func (c *Commands) ImportHumanOTP(ctx context.Context, userID, userAgentID, resourceowner string, key string) error { + encryptedSecret, err := crypto.Encrypt([]byte(key), c.multifactors.OTP.CryptoMFA) + if err != nil { + return err + } + + otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner) + if err != nil { + return err + } + if otpWriteModel.State == domain.MFAStateReady { + return caos_errs.ThrowAlreadyExists(nil, "COMMAND-do9se", "Errors.User.MFA.OTP.AlreadyReady") + } + userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel) + + _, err = c.eventstore.Push(ctx, + user.NewHumanOTPAddedEvent(ctx, userAgg, encryptedSecret), + user.NewHumanOTPVerifiedEvent(ctx, userAgg, userAgentID), + ) + return err +} + func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string) (*domain.OTP, error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-5M0sd", "Errors.User.UserIDMissing") @@ -30,6 +53,7 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string logging.Log("COMMAND-y5zv9").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to get org policy for loginname") return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-8ugTs", "Errors.Org.DomainPolicy.NotFound") } + otpWriteModel, err := c.otpWriteModelByID(ctx, userID, resourceowner) if err != nil { return nil, err @@ -38,6 +62,7 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string return nil, caos_errs.ThrowAlreadyExists(nil, "COMMAND-do9se", "Errors.User.MFA.OTP.AlreadyReady") } userAgg := UserAggregateFromWriteModel(&otpWriteModel.WriteModel) + accountName := domain.GenerateLoginName(human.GetUsername(), org.PrimaryDomain, orgPolicy.UserLoginMustBeDomain) if accountName == "" { accountName = human.EmailAddress @@ -46,8 +71,8 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string if err != nil { return nil, err } - _, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret)) + _, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret)) if err != nil { return nil, err } diff --git a/internal/command/user_idp_link.go b/internal/command/user_idp_link.go index 266911aa25..4440b64927 100644 --- a/internal/command/user_idp_link.go +++ b/internal/command/user_idp_link.go @@ -11,6 +11,23 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) +func (c *Commands) AddUserIDPLink(ctx context.Context, userID, resourceOwner string, link *domain.UserIDPLink) (err error) { + if userID == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing") + } + + linkWriteModel := NewUserIDPLinkWriteModel(userID, link.IDPConfigID, link.ExternalUserID, resourceOwner) + userAgg := UserAggregateFromWriteModel(&linkWriteModel.WriteModel) + + event, err := c.addUserIDPLink(ctx, userAgg, link) + if err != nil { + return err + } + + _, err = c.eventstore.Push(ctx, event) + return err +} + func (c *Commands) BulkAddedUserIDPLinks(ctx context.Context, userID, resourceOwner string, links []*domain.UserIDPLink) (err error) { if userID == "" { return caos_errs.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing") diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index c7fae09fc4..38e8cff2a6 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -2,7 +2,6 @@ package command import ( "context" - "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/repository/user" @@ -24,6 +23,29 @@ func (c *Commands) AddMachine(ctx context.Context, orgID string, machine *domain if err != nil { return nil, err } + return c.addMachineWithID(ctx, orgID, userID, machine, domainPolicy) +} + +func (c *Commands) AddMachineWithID(ctx context.Context, orgID string, userID string, machine *domain.Machine) (*domain.Machine, error) { + existingMachine, err := c.machineWriteModelByID(ctx, userID, orgID) + if err != nil { + return nil, err + } + if isUserStateExists(existingMachine.UserState) { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2una", "Errors.User.AlreadyExisting") + } + domainPolicy, err := c.getOrgDomainPolicy(ctx, orgID) + if err != nil { + return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-3M9fs", "Errors.Org.DomainPolicy.NotFound") + } + if !domainPolicy.UserLoginMustBeDomain { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-6M0dd", "Errors.User.Invalid") + } + return c.addMachineWithID(ctx, orgID, userID, machine, domainPolicy) +} + +func (c *Commands) addMachineWithID(ctx context.Context, orgID string, userID string, machine *domain.Machine, domainPolicy *domain.DomainPolicy) (*domain.Machine, error) { + machine.AggregateID = userID addedMachine := NewMachineWriteModel(machine.AggregateID, orgID) userAgg := UserAggregateFromWriteModel(&addedMachine.WriteModel) diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 36de8d6155..9799faea60 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -124,3 +124,11 @@ func CompareHash(value *CryptoValue, comparer []byte, alg HashAlgorithm) error { } return alg.CompareHash(value.Crypted, comparer) } + +func FillHash(value []byte, alg HashAlgorithm) *CryptoValue { + return &CryptoValue{ + CryptoType: TypeHash, + Algorithm: alg.Algorithm(), + Crypted: value, + } +} diff --git a/internal/domain/human.go b/internal/domain/human.go index ce3c2bf42b..e83797a12e 100644 --- a/internal/domain/human.go +++ b/internal/domain/human.go @@ -19,6 +19,7 @@ type Human struct { Username string State UserState *Password + *HashedPassword *Profile *Email *Phone @@ -88,7 +89,7 @@ func (u *Human) HashPasswordIfExisting(policy *PasswordComplexityPolicy, passwor } func (u *Human) IsInitialState(passwordless, externalIDPs bool) bool { - return u.Email == nil || !u.IsEmailVerified || !externalIDPs && !passwordless && (u.Password == nil || u.SecretString == "") + return u.Email == nil || !u.IsEmailVerified || !externalIDPs && !passwordless && (u.Password == nil || u.Password.SecretString == "") && (u.HashedPassword == nil || u.HashedPassword.SecretString == "") } func NewInitUserCode(generator crypto.Generator) (*InitUserCode, error) { diff --git a/internal/domain/human_hashed_password.go b/internal/domain/human_hashed_password.go new file mode 100644 index 0000000000..5aa144fe52 --- /dev/null +++ b/internal/domain/human_hashed_password.go @@ -0,0 +1,24 @@ +package domain + +import ( + "github.com/zitadel/zitadel/internal/crypto" + es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" +) + +type HashedPassword struct { + es_models.ObjectRoot + + SecretString string + SecretCrypto *crypto.CryptoValue +} + +func NewHashedPassword(password, algorithm string) *HashedPassword { + return &HashedPassword{ + SecretString: password, + SecretCrypto: &crypto.CryptoValue{ + CryptoType: crypto.TypeHash, + Algorithm: algorithm, + Crypted: []byte(password), + }, + } +} diff --git a/internal/eventstore/handler/handler_projection.go b/internal/eventstore/handler/handler_projection.go index e495caf71a..ae2c4c2c80 100644 --- a/internal/eventstore/handler/handler_projection.go +++ b/internal/eventstore/handler/handler_projection.go @@ -168,7 +168,7 @@ func (h *ProjectionHandler) subscribe(ctx context.Context) { index, err := h.Process(ctx, events...) if err != nil || index < len(events)-1 { - logging.WithFields("projection", h.ProjectionName).WithError(err).Error("unable to process all events from subscription") + logging.WithFields("projection", h.ProjectionName).WithError(err).Warn("unable to process all events from subscription") } } } diff --git a/internal/query/idp.go b/internal/query/idp.go index 2e6ea3e7e6..dee1c5e6cf 100644 --- a/internal/query/idp.go +++ b/internal/query/idp.go @@ -502,3 +502,15 @@ func prepareIDPsQuery() (sq.SelectBuilder, func(*sql.Rows) (*IDPs, error)) { }, nil } } + +func (q *Queries) GetOIDCIDPClientSecret(ctx context.Context, shouldRealTime bool, resourceowner, idpID string) (string, error) { + idp, err := q.IDPByIDAndResourceOwner(ctx, shouldRealTime, idpID, resourceowner) + if err != nil { + return "", err + } + + if idp.ClientSecret != nil && idp.ClientSecret.Crypted != nil { + return crypto.DecryptString(idp.ClientSecret, q.idpConfigEncryption) + } + return "", errors.ThrowNotFound(nil, "QUERY-bsm2o", "Errors.Query.NotFound") +} diff --git a/internal/query/query.go b/internal/query/query.go index 328eec42fa..75dca49d66 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -4,6 +4,8 @@ import ( "context" "database/sql" "fmt" + sd "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" "net/http" "sync" @@ -27,6 +29,8 @@ type Queries struct { eventstore *eventstore.Eventstore client *sql.DB + idpConfigEncryption crypto.EncryptionAlgorithm + DefaultLanguage language.Tag LoginDir http.FileSystem NotificationDir http.FileSystem @@ -35,9 +39,10 @@ type Queries struct { NotificationTranslationFileContents map[string][]byte supportedLangs []language.Tag zitadelRoles []authz.RoleMapping + multifactors domain.MultifactorConfigs } -func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *sql.DB, projections projection.Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, zitadelRoles []authz.RoleMapping) (repo *Queries, err error) { +func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *sql.DB, projections projection.Config, defaults sd.SystemDefaults, idpConfigEncryption, otpEncryption, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, zitadelRoles []authz.RoleMapping) (repo *Queries, err error) { statikLoginFS, err := fs.NewWithNamespace("login") if err != nil { return nil, fmt.Errorf("unable to start login statik dir") @@ -66,6 +71,14 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *sql keypair.RegisterEventMappers(repo.eventstore) usergrant.RegisterEventMappers(repo.eventstore) + repo.idpConfigEncryption = idpConfigEncryption + repo.multifactors = domain.MultifactorConfigs{ + OTP: domain.OTPConfig{ + CryptoMFA: otpEncryption, + Issuer: defaults.Multifactors.OTP.Issuer, + }, + } + err = projection.Start(ctx, sqlClient, es, projections, keyEncryptionAlgorithm) if err != nil { return nil, err diff --git a/internal/query/user_otp.go b/internal/query/user_otp.go new file mode 100644 index 0000000000..9e1029a511 --- /dev/null +++ b/internal/query/user_otp.go @@ -0,0 +1,91 @@ +package query + +import ( + "context" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +func (q *Queries) GetHumanOTPSecret(ctx context.Context, userID, resourceowner string) (string, error) { + if userID == "" { + return "", caos_errs.ThrowPreconditionFailed(nil, "QUERY-8N9ds", "Errors.User.UserIDMissing") + } + existingOTP, err := q.otpWriteModelByID(ctx, userID, resourceowner) + if err != nil { + return "", err + } + if existingOTP.State != domain.MFAStateReady { + return "", caos_errs.ThrowNotFound(nil, "QUERY-01982h", "Errors.User.NotFound") + } + + return crypto.DecryptString(existingOTP.Secret, q.multifactors.OTP.CryptoMFA) +} + +func (q *Queries) otpWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *HumanOTPWriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + writeModel = NewHumanOTPWriteModel(userID, resourceOwner) + err = q.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return writeModel, nil +} + +type HumanOTPWriteModel struct { + eventstore.WriteModel + + State domain.MFAState + Secret *crypto.CryptoValue +} + +func NewHumanOTPWriteModel(userID, resourceOwner string) *HumanOTPWriteModel { + return &HumanOTPWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + } +} + +func (wm *HumanOTPWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *user.HumanOTPAddedEvent: + wm.Secret = e.Secret + wm.State = domain.MFAStateNotReady + case *user.HumanOTPVerifiedEvent: + wm.State = domain.MFAStateReady + case *user.HumanOTPRemovedEvent: + wm.State = domain.MFAStateRemoved + case *user.UserRemovedEvent: + wm.State = domain.MFAStateRemoved + } + } + return wm.WriteModel.Reduce() +} + +func (wm *HumanOTPWriteModel) Query() *eventstore.SearchQueryBuilder { + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(user.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes(user.HumanMFAOTPAddedType, + user.HumanMFAOTPVerifiedType, + user.HumanMFAOTPRemovedType, + user.UserRemovedType, + user.UserV1MFAOTPAddedType, + user.UserV1MFAOTPVerifiedType, + user.UserV1MFAOTPRemovedType). + Builder() + + if wm.ResourceOwner != "" { + query.ResourceOwner(wm.ResourceOwner) + } + return query +} diff --git a/internal/query/user_password.go b/internal/query/user_password.go new file mode 100644 index 0000000000..7fd48564bc --- /dev/null +++ b/internal/query/user_password.go @@ -0,0 +1,140 @@ +package query + +import ( + "context" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "time" +) + +type HumanPasswordWriteModel struct { + eventstore.WriteModel + + Secret *crypto.CryptoValue + SecretChangeRequired bool + + Code *crypto.CryptoValue + CodeCreationDate time.Time + CodeExpiry time.Duration + PasswordCheckFailedCount uint64 + + UserState domain.UserState +} + +func (q *Queries) GetHumanPassword(ctx context.Context, orgID, userID string) (passwordHash []byte, algorithm string, err error) { + if userID == "" { + return nil, "", caos_errs.ThrowInvalidArgument(nil, "QUERY-4Mfsf", "Errors.User.UserIDMissing") + } + existingPassword, err := q.passwordWriteModel(ctx, userID, orgID) + if err != nil { + return nil, "", caos_errs.ThrowInternal(nil, "QUERY-p1k1n2i", "Errors.User.NotFound") + } + if existingPassword.UserState == domain.UserStateUnspecified || existingPassword.UserState == domain.UserStateDeleted { + return nil, "", caos_errs.ThrowPreconditionFailed(nil, "QUERY-3n77z", "Errors.User.NotFound") + } + + if existingPassword.Secret != nil && existingPassword.Secret.Crypted != nil { + return existingPassword.Secret.Crypted, existingPassword.Secret.Algorithm, nil + } + + return nil, "", nil +} + +func (q *Queries) passwordWriteModel(ctx context.Context, userID, resourceOwner string) (writeModel *HumanPasswordWriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + writeModel = NewHumanPasswordWriteModel(userID, resourceOwner) + err = q.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + return writeModel, nil +} + +func NewHumanPasswordWriteModel(userID, resourceOwner string) *HumanPasswordWriteModel { + return &HumanPasswordWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: userID, + ResourceOwner: resourceOwner, + }, + } +} + +func (wm *HumanPasswordWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *user.HumanAddedEvent: + wm.Secret = e.Secret + wm.SecretChangeRequired = e.ChangeRequired + wm.UserState = domain.UserStateActive + case *user.HumanRegisteredEvent: + wm.Secret = e.Secret + wm.SecretChangeRequired = e.ChangeRequired + wm.UserState = domain.UserStateActive + case *user.HumanInitialCodeAddedEvent: + wm.UserState = domain.UserStateInitial + case *user.HumanInitializedCheckSucceededEvent: + wm.UserState = domain.UserStateActive + case *user.HumanPasswordChangedEvent: + wm.Secret = e.Secret + wm.SecretChangeRequired = e.ChangeRequired + wm.Code = nil + wm.PasswordCheckFailedCount = 0 + case *user.HumanPasswordCodeAddedEvent: + wm.Code = e.Code + wm.CodeCreationDate = e.CreationDate() + wm.CodeExpiry = e.Expiry + case *user.HumanEmailVerifiedEvent: + if wm.UserState == domain.UserStateInitial { + wm.UserState = domain.UserStateActive + } + case *user.HumanPasswordCheckFailedEvent: + wm.PasswordCheckFailedCount += 1 + case *user.HumanPasswordCheckSucceededEvent: + wm.PasswordCheckFailedCount = 0 + case *user.UserUnlockedEvent: + wm.PasswordCheckFailedCount = 0 + case *user.UserRemovedEvent: + wm.UserState = domain.UserStateDeleted + } + } + return wm.WriteModel.Reduce() +} + +func (wm *HumanPasswordWriteModel) Query() *eventstore.SearchQueryBuilder { + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(user.AggregateType). + AggregateIDs(wm.AggregateID). + EventTypes(user.HumanAddedType, + user.HumanRegisteredType, + user.HumanInitialCodeAddedType, + user.HumanInitializedCheckSucceededType, + user.HumanPasswordChangedType, + user.HumanPasswordCodeAddedType, + user.HumanEmailVerifiedType, + user.HumanPasswordCheckFailedType, + user.HumanPasswordCheckSucceededType, + user.UserRemovedType, + user.UserUnlockedType, + user.UserV1AddedType, + user.UserV1RegisteredType, + user.UserV1InitialCodeAddedType, + user.UserV1InitializedCheckSucceededType, + user.UserV1PasswordChangedType, + user.UserV1PasswordCodeAddedType, + user.UserV1EmailVerifiedType, + user.UserV1PasswordCheckFailedType, + user.UserV1PasswordCheckSucceededType). + Builder() + + if wm.ResourceOwner != "" { + query.ResourceOwner(wm.ResourceOwner) + } + return query +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index fceb2a2848..21e44a2ed9 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -10,6 +10,8 @@ import "zitadel/policy.proto"; import "zitadel/settings.proto"; import "zitadel/text.proto"; import "zitadel/member.proto"; +import "zitadel/management.proto"; +import "zitadel/v1.proto"; import "google/api/annotations.proto"; import "google/protobuf/timestamp.proto"; @@ -460,7 +462,7 @@ service AdminService { }; } - //Checks whether an organisation exists by the given parameters + //Checks whether an organisation exists by the given parameters rpc IsOrgUnique(IsOrgUniqueRequest) returns (IsOrgUniqueResponse) { option (google.api.http) = { get: "/orgs/_is_unique"; @@ -553,7 +555,7 @@ service AdminService { }; } - //Creates a new org and user + //Creates a new org and user // and adds the user to the orgs members as ORG_OWNER rpc SetUpOrg(SetUpOrgRequest) returns (SetUpOrgResponse) { option (google.api.http) = { @@ -1471,7 +1473,7 @@ service AdminService { }; }; } - + //Updates the default login policy of ZITADEL // it impacts all organisations without a customised policy rpc UpdateLoginPolicy(UpdateLoginPolicyRequest) returns (UpdateLoginPolicyResponse) { @@ -1536,7 +1538,7 @@ service AdminService { post: "/policies/login/idps"; body: "*"; }; - + option (zitadel.v1.auth_option) = { permission: "iam.policy.write"; }; @@ -1887,7 +1889,7 @@ service AdminService { }; }; } - + //Updates the default password age policy of ZITADEL // it impacts all organisations without a customised policy rpc UpdatePasswordAgePolicy(UpdatePasswordAgePolicyRequest) returns (UpdatePasswordAgePolicyResponse) { @@ -1945,7 +1947,7 @@ service AdminService { }; }; } - + //Updates the default lockout policy of ZITADEL // it impacts all organisations without a customised policy rpc UpdateLockoutPolicy(UpdateLockoutPolicyRequest) returns (UpdateLockoutPolicyResponse) { @@ -2362,7 +2364,7 @@ service AdminService { }; } - //Returns the IAM roles visible for the requested user + //Returns the IAM roles visible for the requested user rpc ListIAMMemberRoles(ListIAMMemberRolesRequest) returns (ListIAMMemberRolesResponse) { option (google.api.http) = { post: "/members/roles/_search"; @@ -2564,7 +2566,7 @@ service AdminService { } //Returns event descriptions which cannot be processed. - // It's possible that some events need some retries. + // 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 rpc ListFailedEvents(ListFailedEventsRequest) returns (ListFailedEventsResponse) { option (google.api.http) = { @@ -2629,6 +2631,29 @@ service AdminService { }; }; } + + // Imports data into instance and creates different objects + rpc ImportData(ImportDataRequest) returns (ImportDataResponse) { + option (google.api.http) = { + post: "/import"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.write"; + }; + } + // Exports data from instance + rpc ExportData(ExportDataRequest) returns (ExportDataResponse) { + option (google.api.http) = { + post: "/export"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "iam.read"; + }; + } } @@ -3070,7 +3095,7 @@ message SetUpOrgRequest { ]; bool is_phone_verified = 2; } - + string user_name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -3079,7 +3104,7 @@ message SetUpOrgRequest { example: "\"mr_long_neck\""; } ]; - + Profile profile = 2 [(validate.rules).message.required = true]; Email email = 3 [(validate.rules).message.required = true]; Phone phone = 4; @@ -3905,7 +3930,7 @@ message RemoveIDPFromLoginPolicyRequest { required: ["idp_id"] }; }; - + string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -3934,7 +3959,7 @@ message AddSecondFactorToLoginPolicyRequest { required: ["type"] }; }; - + zitadel.policy.v1.SecondFactorType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; } @@ -3984,7 +4009,7 @@ message RemoveMultiFactorFromLoginPolicyRequest { required: ["type"] }; }; - + zitadel.policy.v1.MultiFactorType type = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; } @@ -4621,3 +4646,187 @@ message FailedEvent { } ]; } + +message ImportDataRequest { + message LocalInput{ + string path = 1; + } + message S3Input{ + string path = 1; + string endpoint = 2; + string access_key_id =3; + string secret_access_key = 4; + bool ssl = 5; + string bucket = 6; + } + message GCSInput{ + string bucket = 1; + string serviceaccount_json = 2; + string path = 3; + } + + oneof data { + ImportDataOrg data_orgs = 1; + zitadel.v1.v1.ImportDataOrg data_orgsv1 = 2; + LocalInput data_orgs_local = 3; + LocalInput data_orgsv1_local = 4; + S3Input data_orgs_s3 = 5; + S3Input data_orgsv1_s3 = 6; + GCSInput data_orgs_gcs = 7; + GCSInput data_orgsv1_gcs = 8; + } + string timeout = 9; +} + +message ImportDataOrg { + repeated DataOrg orgs = 1; +} + +message DataOrg { + string org_id = 1; + zitadel.management.v1.AddOrgRequest org = 3; + AddCustomDomainPolicyRequest domain_policy = 4; + zitadel.management.v1.AddCustomLabelPolicyRequest label_policy = 5; + zitadel.management.v1.AddCustomLockoutPolicyRequest lockout_policy = 6; + zitadel.management.v1.AddCustomLoginPolicyRequest login_policy = 7; + zitadel.management.v1.AddCustomPasswordComplexityPolicyRequest password_complexity_policy = 8; + zitadel.management.v1.AddCustomPrivacyPolicyRequest privacy_policy = 9; + + repeated zitadel.v1.v1.DataProject projects = 10; + repeated zitadel.management.v1.AddProjectRoleRequest project_roles = 11; + repeated zitadel.v1.v1.DataAPIApplication api_apps = 12; + repeated zitadel.v1.v1.DataOIDCApplication oidc_apps = 13; + repeated zitadel.v1.v1.DataHumanUser human_users = 14; + repeated zitadel.v1.v1.DataMachineUser machine_users = 15; + repeated zitadel.management.v1.SetTriggerActionsRequest trigger_actions = 16; + repeated zitadel.v1.v1.DataAction actions = 17; + + repeated zitadel.v1.v1.DataProjectGrant project_grants = 18; + repeated zitadel.management.v1.AddUserGrantRequest user_grants = 19; + + repeated zitadel.management.v1.AddOrgMemberRequest org_members = 20; + repeated zitadel.management.v1.AddProjectMemberRequest project_members = 21; + repeated zitadel.management.v1.AddProjectGrantMemberRequest project_grant_members = 22; + + repeated zitadel.management.v1.SetUserMetadataRequest user_metadata = 23; + + repeated zitadel.management.v1.SetCustomLoginTextsRequest login_texts = 24; + + repeated zitadel.management.v1.SetCustomInitMessageTextRequest init_messages = 25; + repeated zitadel.management.v1.SetCustomPasswordResetMessageTextRequest password_reset_messages = 26; + repeated zitadel.management.v1.SetCustomVerifyEmailMessageTextRequest verify_email_messages = 27; + repeated zitadel.management.v1.SetCustomVerifyPhoneMessageTextRequest verify_phone_messages = 28; + repeated zitadel.management.v1.SetCustomDomainClaimedMessageTextRequest domain_claimed_messages = 29; + repeated zitadel.management.v1.SetCustomPasswordlessRegistrationMessageTextRequest passwordless_registration_messages = 30; + + repeated zitadel.v1.v1.DataOIDCIDP oidc_idps = 31; + repeated zitadel.v1.v1.DataJWTIDP jwt_idps = 32; + + repeated zitadel.idp.v1.IDPUserLink user_links = 33; + repeated zitadel.org.v1.Domain domains = 34; +} + +message ImportDataResponse{ + repeated ImportDataError errors = 1; + ImportDataSuccess success = 2; +} + +message ImportDataError{ + string type = 1; + string id = 2; + string message = 3; +} + +message ImportDataSuccess { + repeated ImportDataSuccessOrg orgs = 1; +} + +message ImportDataSuccessOrg{ + string org_id = 1; + repeated string project_ids = 2; + repeated string project_roles = 3; + repeated string oidc_app_ids = 4; + repeated string api_app_ids = 5; + repeated string human_user_ids = 6; + repeated string machine_user_ids = 7; + repeated string action_ids = 8; + repeated zitadel.management.v1.SetTriggerActionsRequest trigger_actions = 9; + repeated ImportDataSuccessProjectGrant project_grants = 10; + repeated ImportDataSuccessUserGrant user_grants = 11; + repeated string org_members = 12; + repeated ImportDataSuccessProjectMember project_members = 13; + repeated ImportDataSuccessProjectGrantMember project_grant_members = 14; + repeated string oidc_ipds = 15; + repeated string jwt_idps = 16; + repeated string idp_links = 17; + repeated ImportDataSuccessUserLinks user_links = 18; + repeated ImportDataSuccessUserMetadata user_metadata = 19; + repeated string domains = 20; +} + +message ImportDataSuccessProjectGrant{ + string grant_id = 1; + string project_id = 2; + string org_id = 3; +} + +message ImportDataSuccessUserGrant{ + string project_id = 1; + string user_id = 2; +} + +message ImportDataSuccessProjectMember{ + string project_id = 1; + string user_id = 2; +} + +message ImportDataSuccessProjectGrantMember{ + string project_id = 1; + string grant_id = 2; + string user_id = 3; +} + +message ImportDataSuccessUserLinks { + string user_id = 1; + string external_user_id = 2; + string display_name = 3; + string idp_id = 4; +} + +message ImportDataSuccessUserMetadata { + string user_id = 1; + string key = 2; +} + +message ExportDataRequest { + message LocalOutput{ + string path = 1; + } + message S3Output{ + string path = 1; + string endpoint = 2; + string access_key_id =3; + string secret_access_key = 4; + bool ssl = 5; + string bucket = 6; + } + message GCSOutput{ + string bucket = 1; + string serviceaccount_json = 2; + string path = 3; + } + + repeated string org_ids = 1; + repeated string excluded_org_ids = 2; + bool with_passwords = 3; + bool with_otp = 4; + bool response_output = 5; + LocalOutput local_output = 6; + S3Output s3_output = 7; + GCSOutput gcs_output = 8; + string timeout = 9; +} + +message ExportDataResponse { + repeated DataOrg orgs = 1; +} \ No newline at end of file diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 11da17b0d5..c49651b309 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -3007,6 +3007,10 @@ message ImportHumanUserRequest { string phone = 1 [(validate.rules).string = {min_len: 1, max_len: 50, prefix: "+"}]; bool is_phone_verified = 2; } + message HashedPassword{ + string value = 1; + string algorithm = 2; + } string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; @@ -3014,8 +3018,11 @@ message ImportHumanUserRequest { Email email = 3 [(validate.rules).message.required = true]; Phone phone = 4; string password = 5; - bool password_change_required = 6; - bool request_passwordless_registration = 7; + HashedPassword hashed_password = 6; + bool password_change_required = 7; + bool request_passwordless_registration = 8; + + string otp_code = 9; } message ImportHumanUserResponse { diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 6123249abb..2d9e47a279 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -11,324 +11,323 @@ import "validate/validate.proto"; package zitadel.system.v1; -option go_package ="github.com/zitadel/zitadel/pkg/grpc/system"; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/system"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { - info: { - title: "System API"; - version: "1.0"; - description: "This API is intended to configure and manage the different tenants whithin ZITADEL."; - contact:{ - name: "ZITADEL" - url: "https://zitadel.com" - email: "hi@zitadel.com" - } - license: { - name: "Apache 2.0", - url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; - }; + info: { + title: "System API"; + version: "1.0"; + description: "This API is intended to configure and manage the different tenants whithin ZITADEL."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; }; + }; - schemes: HTTPS; - schemes: HTTP; + schemes: HTTPS; + schemes: HTTP; - consumes: "application/json"; - consumes: "application/grpc"; + consumes: "application/json"; + consumes: "application/grpc"; - produces: "application/json"; - produces: "application/grpc"; + produces: "application/json"; + produces: "application/grpc"; - consumes: "application/grpc-web+proto"; - produces: "application/grpc-web+proto"; + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; - host: "api.zitadel.ch"; - base_path: "/system/v1"; + host: "api.zitadel.ch"; + base_path: "/system/v1"; - external_docs: { - description: "Detailed information about ZITADEL", - url: "https://docs.zitadel.com" - } + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://docs.zitadel.com" + } - responses: { - key: "403"; - value: { - description: "Returned when the user does not have permission to access the resource."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; } + } } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; } + } } + } }; service SystemService { - //Indicates if ZITADEL is running. - // It respondes as soon as ZITADEL started - rpc Healthz(HealthzRequest) returns (HealthzResponse) { - option (google.api.http) = { - get: "/healthz"; + //Indicates if ZITADEL is running. + // It respondes as soon as ZITADEL started + rpc Healthz(HealthzRequest) returns (HealthzResponse) { + option (google.api.http) = { + get: "/healthz"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "probes"; + responses: { + key: "200"; + value: { + description: "ZITADEL started"; }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "probes"; - responses: { - key: "200"; - value: { - description: "ZITADEL started"; - }; - } - responses: { - key: "default"; - value: { - description: "ZITADEL NOT started yet"; - }; - } + } + responses: { + key: "default"; + value: { + description: "ZITADEL NOT started yet"; }; - } + } + }; + } - // Returns a list of ZITADEL instances - rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { - option (google.api.http) = { - post: "/instances/_search" - body: "*" + // Returns a list of ZITADEL instances + rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { + option (google.api.http) = { + post: "/instances/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Returns the detail of an instance + rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { + option (google.api.http) = { + get: "/instances/{instance_id}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Creates a new instance with all needed setup data + // This might take some time + rpc AddInstance(AddInstanceRequest) returns (AddInstanceResponse) { + option (google.api.http) = { + post: "/instances" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Removes a instances + // This might take some time + rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { + option (google.api.http) = { + delete: "/instances/{instance_id}" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Checks if a domain exists + rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { + option (google.api.http) = { + post: "/domains/{domain}/_exists"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Returns the custom domains of an instance + rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { + option (google.api.http) = { + post: "/instances/{instance_id}/domains/_search"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Returns the domain of an instance + rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) { + option (google.api.http) = { + post: "/instances/{instance_id}/domains"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Returns the domain of an instance + rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) { + option (google.api.http) = { + delete: "/instances/{instance_id}/domains/{domain}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + // Returns the domain of an instance + rpc SetPrimaryDomain(SetPrimaryDomainRequest) returns (SetPrimaryDomainResponse) { + option (google.api.http) = { + post: "/instances/{instance_id}/domains/_set_primary"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + } + + //Returns all stored read models of ZITADEL + // views are used for search optimisation and optimise request latencies + // they represent the delta of the event happend on the objects + rpc ListViews(ListViewsRequest) returns (ListViewsResponse) { + option (google.api.http) = { + post: "/views/_search"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "views"; + external_docs: { + url: "https://docs.zitadel.com/concepts#Software_Architecture"; + description: "details of ZITADEL's event driven software concepts"; + }; + responses: { + key: "200"; + value: { + description: "Views for query operations"; }; + }; + }; + } - option (zitadel.v1.auth_option) = { - permission: "authenticated"; + //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: "authenticated"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "views"; + external_docs: { + url: "https://docs.zitadel.com/concepts#Software_Architecture"; + description: "details of ZITADEL's event driven software concepts"; + }; + responses: { + key: "200"; + value: { + description: "View cleared"; }; - } + }; + }; + } - // Returns the detail of an instance - rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { - option (google.api.http) = { - get: "/instances/{instance_id}"; + //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 + rpc ListFailedEvents(ListFailedEventsRequest) returns (ListFailedEventsResponse) { + option (google.api.http) = { + post: "/failedevents/_search"; + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "failed events"; + external_docs: { + url: "https://docs.zitadel.com/concepts#Software_Architecture"; + description: "details of ZITADEL's event driven software concepts"; + }; + responses: { + key: "200"; + value: { + description: "Events which were not processed by the views"; }; + }; + }; + } - option (zitadel.v1.auth_option) = { - permission: "authenticated"; + //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. + // 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) { + option (google.api.http) = { + delete: "/failedevents/{database}/{view_name}/{failed_sequence}"; + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated"; + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "failed events"; + external_docs: { + url: "https://docs.zitadel.com/concepts#Software_Architecture"; + description: "details of ZITADEL's event driven software concepts"; + }; + responses: { + key: "200"; + value: { + description: "Events removed from the list"; }; - } - - // Creates a new instance with all needed setup data - // This might take some time - rpc AddInstance(AddInstanceRequest) returns (AddInstanceResponse) { - option (google.api.http) = { - post: "/instances" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - } - - // Removes a instances - // This might take some time - rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { - option (google.api.http) = { - delete: "/instances/{instance_id}" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - } - - // Checks if a domain exists - rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { - option (google.api.http) = { - post: "/domains/{domain}/_exists"; - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - } - - // Returns the custom domains of an instance - rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { - option (google.api.http) = { - post: "/instances/{instance_id}/domains/_search"; - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - } - - // Returns the domain of an instance - rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) { - option (google.api.http) = { - post: "/instances/{instance_id}/domains"; - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - } - - // Returns the domain of an instance - rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) { - option (google.api.http) = { - delete: "/instances/{instance_id}/domains/{domain}"; - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - } - - // Returns the domain of an instance - rpc SetPrimaryDomain(SetPrimaryDomainRequest) returns (SetPrimaryDomainResponse) { - option (google.api.http) = { - post: "/instances/{instance_id}/domains/_set_primary"; - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - } - - - //Returns all stored read models of ZITADEL - // views are used for search optimisation and optimise request latencies - // they represent the delta of the event happend on the objects - rpc ListViews(ListViewsRequest) returns (ListViewsResponse) { - option (google.api.http) = { - post: "/views/_search"; - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "views"; - external_docs: { - url: "https://docs.zitadel.com/concepts#Software_Architecture"; - description: "details of ZITADEL's event driven software concepts"; - }; - responses: { - key: "200"; - value: { - description: "Views for query operations"; - }; + }; + responses: { + key: "400"; + value: { + description: "failed event not found"; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; }; + }; }; - } - - //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: "authenticated"; - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "views"; - external_docs: { - url: "https://docs.zitadel.com/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 - rpc ListFailedEvents(ListFailedEventsRequest) returns (ListFailedEventsResponse) { - option (google.api.http) = { - post: "/failedevents/_search"; - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "failed events"; - external_docs: { - url: "https://docs.zitadel.com/concepts#Software_Architecture"; - description: "details of ZITADEL's event driven software concepts"; - }; - responses: { - key: "200"; - value: { - description: "Events which were not processed by the views"; - }; - }; - }; - } - - //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. - // 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) { - option (google.api.http) = { - delete: "/failedevents/{database}/{view_name}/{failed_sequence}"; - }; - - option (zitadel.v1.auth_option) = { - permission: "authenticated"; - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "failed events"; - external_docs: { - url: "https://docs.zitadel.com/concepts#Software_Architecture"; - description: "details of ZITADEL's event driven software concepts"; - }; - responses: { - key: "200"; - value: { - description: "Events removed from the list"; - }; - }; - responses: { - key: "400"; - value: { - description: "failed event not found"; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - }; - }; - }; - }; - }; - } + }; + }; + } } @@ -339,175 +338,175 @@ message HealthzRequest {} message HealthzResponse {} message ListInstancesRequest { - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - description: "Search query for lists"; - required: ["query"] - }; - }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + description: "Search query for lists"; + required: ["query"] + }; + }; - //list limitations and ordering - zitadel.v1.ListQuery query = 1; - // the field the result is sorted - zitadel.instance.v1.FieldName sorting_column = 2; - //criterias the client is looking for - repeated zitadel.instance.v1.Query queries = 3; + //list limitations and ordering + zitadel.v1.ListQuery query = 1; + // the field the result is sorted + zitadel.instance.v1.FieldName sorting_column = 2; + //criterias the client is looking for + repeated zitadel.instance.v1.Query queries = 3; } message ListInstancesResponse { - zitadel.v1.ListDetails details = 1; - zitadel.instance.v1.FieldName sorting_column = 2; - repeated zitadel.instance.v1.Instance result = 3; + zitadel.v1.ListDetails details = 1; + zitadel.instance.v1.FieldName sorting_column = 2; + repeated zitadel.instance.v1.Instance result = 3; } message GetInstanceRequest { - string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message GetInstanceResponse { - zitadel.instance.v1.InstanceDetail instance = 1; + zitadel.instance.v1.InstanceDetail instance = 1; } message AddInstanceRequest { - message Profile { - string first_name = 1 [(validate.rules).string = {max_len: 200}]; - string last_name = 2 [(validate.rules).string = {max_len: 200}]; - string preferred_language = 5 [(validate.rules).string = {max_len: 10}]; - } - message Email { - string email = 1[(validate.rules).string = {min_len: 1, max_len: 200}]; - bool is_email_verified = 2; - } - message Password { - string password = 1 [(validate.rules).string = {max_len: 200}]; - bool password_change_required = 2; - } + message Profile { + string first_name = 1 [(validate.rules).string = {max_len: 200}]; + string last_name = 2 [(validate.rules).string = {max_len: 200}]; + string preferred_language = 5 [(validate.rules).string = {max_len: 10}]; + } + message Email { + string email = 1[(validate.rules).string = {min_len: 1, max_len: 200}]; + bool is_email_verified = 2; + } + message Password { + string password = 1 [(validate.rules).string = {max_len: 200}]; + bool password_change_required = 2; + } - string instance_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string first_org_name = 2 [(validate.rules).string = {max_len: 200}]; - string custom_domain = 3 [(validate.rules).string = {max_len: 200}]; - string owner_user_name = 4 [(validate.rules).string = {max_len: 200}]; - Email owner_email = 5 [(validate.rules).message.required = true]; - Profile owner_profile = 6 [(validate.rules).message.required = false]; - Password owner_password = 7 [(validate.rules).message.required = false]; - string default_language = 8 [(validate.rules).string = {max_len: 10}]; + string instance_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string first_org_name = 2 [(validate.rules).string = {max_len: 200}]; + string custom_domain = 3 [(validate.rules).string = {max_len: 200}]; + string owner_user_name = 4 [(validate.rules).string = {max_len: 200}]; + Email owner_email = 5 [(validate.rules).message.required = true]; + Profile owner_profile = 6 [(validate.rules).message.required = false]; + Password owner_password = 7 [(validate.rules).message.required = false]; + string default_language = 8 [(validate.rules).string = {max_len: 10}]; } message AddInstanceResponse { - string instance_id = 1; - zitadel.v1.ObjectDetails details = 2; + string instance_id = 1; + zitadel.v1.ObjectDetails details = 2; } message RemoveInstanceRequest { - string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message RemoveInstanceResponse { - zitadel.v1.ObjectDetails details = 1; + zitadel.v1.ObjectDetails details = 1; } message GetUsageRequest { - string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message GetUsageResponse { - zitadel.v1.ObjectDetails details = 1; - uint64 executed_requests = 2; - uint64 executed_action_mins = 3; + zitadel.v1.ObjectDetails details = 1; + uint64 executed_requests = 2; + uint64 executed_action_mins = 3; } message ExistsDomainRequest { - string domain = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string domain = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message ExistsDomainResponse { - bool exists = 1; + bool exists = 1; } message ListDomainsRequest { - string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];//list limitations and ordering - zitadel.v1.ListQuery query = 2; - // the field the result is sorted - zitadel.instance.v1.DomainFieldName sorting_column = 3; - //criterias the client is looking for - repeated zitadel.instance.v1.DomainSearchQuery queries = 4; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];//list limitations and ordering + zitadel.v1.ListQuery query = 2; + // the field the result is sorted + zitadel.instance.v1.DomainFieldName sorting_column = 3; + //criterias the client is looking for + repeated zitadel.instance.v1.DomainSearchQuery queries = 4; } message ListDomainsResponse { - zitadel.v1.ListDetails details = 1; - zitadel.instance.v1.DomainFieldName sorting_column = 2; - repeated zitadel.instance.v1.Domain result = 3; + zitadel.v1.ListDetails details = 1; + zitadel.instance.v1.DomainFieldName sorting_column = 2; + repeated zitadel.instance.v1.Domain result = 3; } message AddDomainRequest { - string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message AddDomainResponse { - zitadel.v1.ObjectDetails details = 1; + zitadel.v1.ObjectDetails details = 1; } message RemoveDomainRequest { - string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message RemoveDomainResponse { - zitadel.v1.ObjectDetails details = 1; + zitadel.v1.ObjectDetails details = 1; } message SetPrimaryDomainRequest { - string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message SetPrimaryDomainResponse { - zitadel.v1.ObjectDetails details = 1; + zitadel.v1.ObjectDetails details = 1; } message ChangeSubscriptionRequest { - string domain = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string subscription_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; - uint64 request_limit = 3; - uint64 action_mins_limit = 4; + string domain = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string subscription_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + uint64 request_limit = 3; + uint64 action_mins_limit = 4; } message ChangeSubscriptionResponse { - zitadel.v1.ObjectDetails details = 1; + zitadel.v1.ObjectDetails details = 1; } //This is an empty request message ListViewsRequest {} message ListViewsResponse { - //TODO: list details - repeated View result = 1; + //TODO: list details + repeated View result = 1; } message ClearViewRequest { - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["database", "view_name"] - }; - }; + 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; - } - ]; + 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 @@ -517,101 +516,101 @@ message ClearViewResponse {} message ListFailedEventsRequest {} message ListFailedEventsResponse { - //TODO: list details - repeated FailedEvent result = 1; + //TODO: list details + repeated FailedEvent result = 1; } message RemoveFailedEventRequest { - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - json_schema: { - required: ["database", "view_name", "failed_sequence"] - }; - }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + required: ["database", "view_name", "failed_sequence"] + }; + }; - 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; - } - ]; - uint64 failed_sequence = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"9823758\""; - } - ]; + 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; + } + ]; + uint64 failed_sequence = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"9823758\""; + } + ]; } //This is an empty response message RemoveFailedEventResponse {} message View { - string database = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"adminapi\""; - } - ]; - string view_name = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"iam_members\""; - } - ]; - uint64 processed_sequence = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"9823758\""; - } - ]; - google.protobuf.Timestamp event_timestamp = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"2019-04-01T08:45:00.000000Z\""; - description: "The timestamp the event occured"; - } - ]; // The timestamp the event occured - google.protobuf.Timestamp last_successful_spooler_run = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "The timestamp the event occured"; - } - ]; - string instance = 6 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"840498034930840\""; - } - ]; + string database = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"adminapi\""; + } + ]; + string view_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"iam_members\""; + } + ]; + uint64 processed_sequence = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"9823758\""; + } + ]; + google.protobuf.Timestamp event_timestamp = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2019-04-01T08:45:00.000000Z\""; + description: "The timestamp the event occured"; + } + ]; // The timestamp the event occured + google.protobuf.Timestamp last_successful_spooler_run = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The timestamp the event occured"; + } + ]; + string instance = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"840498034930840\""; + } + ]; } message FailedEvent { - string database = 1 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"adminapi\""; - } - ]; - string view_name = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"iam_members\""; - } - ]; - uint64 failed_sequence = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"9823759\""; - } - ]; - uint64 failure_count = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"5\""; - } - ]; - string error_message = 5 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "\"ID=EXAMP-ID3ER Message=Example message\""; - } - ]; + string database = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"adminapi\""; + } + ]; + string view_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"iam_members\""; + } + ]; + uint64 failed_sequence = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"9823759\""; + } + ]; + uint64 failure_count = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"5\""; + } + ]; + string error_message = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ID=EXAMP-ID3ER Message=Example message\""; + } + ]; } diff --git a/proto/zitadel/v1.proto b/proto/zitadel/v1.proto new file mode 100644 index 0000000000..2887082679 --- /dev/null +++ b/proto/zitadel/v1.proto @@ -0,0 +1,161 @@ +syntax = "proto3"; + +import "zitadel/user.proto"; +import "zitadel/idp.proto"; +import "zitadel/org.proto"; +import "zitadel/management.proto"; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +import "validate/validate.proto"; + +package zitadel.v1.v1; + +option go_package ="github.com/zitadel/zitadel/pkg/grpc/v1"; + +message AddCustomOrgIAMPolicyRequest { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + json_schema: { + required: ["org_id"] + }; + }; + + string org_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"#69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; + bool user_login_must_be_domain = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the username has to end with the domain of it's organisation" + } + ]; // the username has to end with the domain of it's organisation (uniqueness is organisation based) +} +message ImportDataOrg { + repeated DataOrg orgs = 1; +} + +message DataOrg { + string org_id = 1; + zitadel.management.v1.AddOrgRequest org = 3; + AddCustomOrgIAMPolicyRequest iam_policy = 4; + zitadel.management.v1.AddCustomLabelPolicyRequest label_policy = 5; + zitadel.management.v1.AddCustomLockoutPolicyRequest lockout_policy = 6; + zitadel.management.v1.AddCustomLoginPolicyRequest login_policy = 7; + zitadel.management.v1.AddCustomPasswordComplexityPolicyRequest password_complexity_policy = 8; + zitadel.management.v1.AddCustomPrivacyPolicyRequest privacy_policy = 9; + + repeated DataProject projects = 10; + repeated zitadel.management.v1.AddProjectRoleRequest project_roles = 11; + repeated DataAPIApplication api_apps = 12; + repeated DataOIDCApplication oidc_apps = 13; + repeated DataHumanUser human_users = 14; + repeated DataMachineUser machine_users = 15; + repeated zitadel.management.v1.SetTriggerActionsRequest trigger_actions = 16; + repeated DataAction actions = 17; + + repeated DataProjectGrant project_grants = 18; + repeated zitadel.management.v1.AddUserGrantRequest user_grants = 19; + + repeated zitadel.management.v1.AddOrgMemberRequest org_members = 20; + repeated zitadel.management.v1.AddProjectMemberRequest project_members = 21; + repeated zitadel.management.v1.AddProjectGrantMemberRequest project_grant_members = 22; + + repeated zitadel.management.v1.SetUserMetadataRequest user_metadata = 23; + + repeated zitadel.management.v1.SetCustomLoginTextsRequest login_texts = 24; + + repeated zitadel.management.v1.SetCustomInitMessageTextRequest init_messages = 25; + repeated zitadel.management.v1.SetCustomPasswordResetMessageTextRequest password_reset_messages = 26; + repeated zitadel.management.v1.SetCustomVerifyEmailMessageTextRequest verify_email_messages = 27; + repeated zitadel.management.v1.SetCustomVerifyPhoneMessageTextRequest verify_phone_messages = 28; + repeated zitadel.management.v1.SetCustomDomainClaimedMessageTextRequest domain_claimed_messages = 29; + repeated zitadel.management.v1.SetCustomPasswordlessRegistrationMessageTextRequest passwordless_registration_messages = 30; + + repeated DataOIDCIDP oidc_idps = 31; + repeated DataJWTIDP jwt_idps = 32; + + repeated zitadel.management.v1.AddSecondFactorToLoginPolicyRequest second_factors = 33; + repeated zitadel.management.v1.AddMultiFactorToLoginPolicyRequest multi_factors = 34; + repeated zitadel.management.v1.AddIDPToLoginPolicyRequest idps = 35; + + repeated zitadel.idp.v1.IDPUserLink user_links = 36; + repeated zitadel.org.v1.Domain domains = 37; +} +message DataOIDCIDP{ + string idp_id = 1; + zitadel.management.v1.AddOrgOIDCIDPRequest idp = 2; +} +message DataJWTIDP{ + string idp_id = 1; + zitadel.management.v1.AddOrgJWTIDPRequest idp = 32; +} + +message ExportHumanUser { + message Profile { + string first_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string last_name = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string nick_name = 3 [(validate.rules).string = {max_len: 200}]; + string display_name = 4 [(validate.rules).string = {max_len: 200}]; + string preferred_language = 5 [(validate.rules).string = {max_len: 10}]; + zitadel.user.v1.Gender gender = 6; + } + message Email { + string email = 1 [(validate.rules).string.email = true]; //TODO: check if no value is allowed + bool is_email_verified = 2; + } + message Phone { + // has to be a global number + string phone = 1 [(validate.rules).string = {min_len: 1, max_len: 50, prefix: "+"}]; + bool is_phone_verified = 2; + } + message HashedPassword{ + string value = 1; + string algorithm = 2; + } + + string user_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + Profile profile = 2 [(validate.rules).message.required = true]; + Email email = 3 [(validate.rules).message.required = true]; + Phone phone = 4; + string password = 5; + HashedPassword hashed_password = 6; + bool password_change_required = 7; + bool request_passwordless_registration = 8; + + string otp_code = 9; +} + + +message DataProject { + string project_id = 1; + zitadel.management.v1.AddProjectRequest project = 2; +} +message DataAPIApplication { + string app_id = 1; + zitadel.management.v1.AddAPIAppRequest app = 2; +} +message DataOIDCApplication { + string app_id = 1; + zitadel.management.v1.AddOIDCAppRequest app = 2; +} +message DataHumanUser { + string user_id = 1; + zitadel.management.v1.ImportHumanUserRequest user = 2; +} +message DataMachineUser { + string user_id = 1; + zitadel.management.v1.AddMachineUserRequest user = 2; +} +message DataAction { + string action_id = 1; + zitadel.management.v1.CreateActionRequest action = 2; +} +message DataProjectGrant { + string grant_id = 1; + zitadel.management.v1.AddProjectGrantRequest project_grant = 2; +} \ No newline at end of file