feat: Hosted login translation API (#10011)

# Which Problems Are Solved

This PR implements https://github.com/zitadel/zitadel/issues/9850

# How the Problems Are Solved

  - New protobuf definition
  - Implementation of retrieval of system translations
- Implementation of retrieval and persistence of organization and
instance level translations

# Additional Context

- Closes #9850

# TODO

- [x] Integration tests for Get and Set hosted login translation
endpoints
- [x] DB migration test
- [x] Command function tests
- [x] Command util functions tests
- [x] Query function test
- [x] Query util functions tests
This commit is contained in:
Marco A.
2025-06-18 13:24:39 +02:00
committed by GitHub
parent cddbd3dd47
commit 28f7218ea1
23 changed files with 3613 additions and 527 deletions

View File

@@ -10,4 +10,4 @@ enum ResourceOwnerType {
RESOURCE_OWNER_TYPE_UNSPECIFIED = 0;
RESOURCE_OWNER_TYPE_INSTANCE = 1;
RESOURCE_OWNER_TYPE_ORG = 2;
}
}

View File

@@ -15,6 +15,8 @@ import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/struct.proto";
import "zitadel/settings/v2/settings.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings";
@@ -362,6 +364,69 @@ service SettingsService {
description: "Set the security settings of the ZITADEL instance."
};
}
// Get Hosted Login Translation
//
// Returns the translations in the requested locale for the hosted login.
// The translations returned are based on the input level specified (system, instance or organization).
//
// If the requested level doesn't contain all translations, and ignore_inheritance is set to false,
// a merging process fallbacks onto the higher levels ensuring all keys in the file have a translation,
// which could be in the default language if the one of the locale is missing on all levels.
//
// The etag returned in the response represents the hash of the translations as they are stored on DB
// and its reliable only if ignore_inheritance = true.
//
// Required permissions:
// - `iam.policy.read`
rpc GetHostedLoginTranslation(GetHostedLoginTranslationRequest) returns (GetHostedLoginTranslationResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "The localized translations.";
}
};
};
option (google.api.http) = {
get: "/v2/settings/hosted_login_translation"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.policy.read"
}
};
}
// Set Hosted Login Translation
//
// Sets the input translations at the specified level (instance or organization) for the input language.
//
// Required permissions:
// - `iam.policy.write`
rpc SetHostedLoginTranslation(SetHostedLoginTranslationRequest) returns (SetHostedLoginTranslationResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
responses: {
key: "200";
value: {
description: "The translations was successfully set.";
}
};
};
option (google.api.http) = {
put: "/v2/settings/hosted_login_translation";
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.policy.write"
}
};
}
}
message GetLoginSettingsRequest {
@@ -480,4 +545,76 @@ message SetSecuritySettingsRequest{
message SetSecuritySettingsResponse{
zitadel.object.v2.Details details = 1;
}
message GetHostedLoginTranslationRequest {
oneof level {
bool system = 1 [(validate.rules).bool = {const: true}];
bool instance = 2 [(validate.rules).bool = {const: true}];
string organization_id = 3;
}
string locale = 4 [
(validate.rules).string = {min_len: 2},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 2;
example: "\"fr-FR\"";
}
];
// if set to true, higher levels are ignored, if false higher levels are merged into the file
bool ignore_inheritance = 5;
}
message GetHostedLoginTranslationResponse {
// hash of the payload
string etag = 1 [
(validate.rules).string = {min_len: 32, max_len: 32},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 32;
max_length: 32;
example: "\"42a1ba123e6ea6f0c93e286ed97c7018\"";
}
];
google.protobuf.Struct translations = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}";
description: "Translations contains the translations in the request language.";
}
];
}
message SetHostedLoginTranslationRequest {
oneof level {
bool instance = 1 [(validate.rules).bool = {const: true}];
string organization_id = 2;
}
string locale = 3 [
(validate.rules).string = {min_len: 2},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 2;
example: "\"fr-FR\"";
}
];
google.protobuf.Struct translations = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}";
description: "Translations should contain the translations in the specified locale.";
}
];
}
message SetHostedLoginTranslationResponse {
// hash of the saved translation. Valid only when ignore_inheritance = true
string etag = 1 [
(validate.rules).string = {min_len: 32, max_len: 32},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 32;
max_length: 32;
example: "\"42a1ba123e6ea6f0c93e286ed97c7018\"";
}
];
}