Skip to content

Commit b90d06e

Browse files
feat: add realm translation resource (#1123)
* feat: add realm translation resource Signed-off-by: Jonathan Davies <[email protected]> * feat: remove redundant code Signed-off-by: Jonathan Davies <[email protected]> * feat: add documentation, expand tests and rename language->locale Signed-off-by: Jonathan Davies <[email protected]> * fix: revert tflog Signed-off-by: Jonathan Davies <[email protected]> * feat: readd tflog Signed-off-by: Jonathan Davies <[email protected]> * feat: rename translations to localization texts Signed-off-by: Jonathan Davies <[email protected]> * fix: example tf Signed-off-by: Jonathan Davies <[email protected]> * fix: wrong resource Signed-off-by: Jonathan Davies <[email protected]> * fix: change back to 600 Signed-off-by: Jonathan Davies <[email protected]> --------- Signed-off-by: Jonathan Davies <[email protected]> Co-authored-by: Sebastian Schuster <[email protected]>
1 parent 3e11e03 commit b90d06e

8 files changed

+413
-18
lines changed

docs/resources/realm_translation.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
page_title: "keycloak_realm_localization Resource"
3+
---
4+
5+
# keycloak_realm_localization Resource
6+
7+
Allows for managing Realm Localization Text overrides within Keycloak.
8+
9+
A localization resource defines a schema for representing a locale with a map of key/value pairs and how they are managed within a realm.
10+
11+
Note: whilst you can provide localization texts for unsupported locales, they will not take effect until they are defined within the realm resource.
12+
13+
## Example Usage
14+
15+
```hcl
16+
resource "keycloak_realm" "realm" {
17+
realm = "my-realm"
18+
}
19+
20+
resource "keycloak_realm_localization" "german_texts" {
21+
realm_id = keycloak_realm.my_realm.id
22+
locale = "de"
23+
texts = {
24+
"Hello" : "Hallo"
25+
}
26+
}
27+
```
28+
29+
## Argument Reference
30+
31+
- `realm_id` - (Required) The ID of the realm the user profile applies to.
32+
- `locale` - (Required) The locale (language code) the texts apply to.
33+
- `texts` - (Optional) A map of translation keys to values.
34+
35+
36+
## Import
37+
38+
This resource does not currently support importing.

example/main.tf

+24-16
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ resource "keycloak_realm" "test" {
101101
}
102102
}
103103

104+
resource "keycloak_realm_localization" "test_translation" {
105+
realm_id = keycloak_realm.test.id
106+
locale = "en"
107+
texts = {
108+
"test" : "translation"
109+
}
110+
}
111+
104112
resource "keycloak_required_action" "custom-terms-and-conditions" {
105113
realm_id = keycloak_realm.test.realm
106114
alias = "TERMS_AND_CONDITIONS"
@@ -116,7 +124,7 @@ resource "keycloak_required_action" "update-password" {
116124
enabled = true
117125
name = "Update Password"
118126

119-
config {
127+
config = {
120128
max_auth_age = "600"
121129
}
122130
}
@@ -439,25 +447,25 @@ resource "keycloak_ldap_full_name_mapper" "full_name_mapper" {
439447
}
440448

441449
resource "keycloak_ldap_custom_mapper" "custom_mapper" {
442-
name = "custom-mapper"
443-
realm_id = keycloak_ldap_user_federation.openldap.realm_id
444-
ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id
450+
name = "custom-mapper"
451+
realm_id = keycloak_ldap_user_federation.openldap.realm_id
452+
ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id
445453

446-
provider_id = "msad-user-account-control-mapper"
447-
provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
454+
provider_id = "msad-user-account-control-mapper"
455+
provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
448456
}
449457

450458
resource "keycloak_ldap_custom_mapper" "custom_mapper_with_config" {
451-
name = "custom-mapper-with-config"
452-
realm_id = keycloak_ldap_user_federation.openldap.realm_id
453-
ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id
454-
455-
provider_id = "user-attribute-ldap-mapper"
456-
provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
457-
config = {
458-
"user.model.attribute" = "username"
459-
"ldap.attribute" = "cn"
460-
}
459+
name = "custom-mapper-with-config"
460+
realm_id = keycloak_ldap_user_federation.openldap.realm_id
461+
ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id
462+
463+
provider_id = "user-attribute-ldap-mapper"
464+
provider_type = "org.keycloak.storage.ldap.mappers.LDAPStorageMapper"
465+
config = {
466+
"user.model.attribute" = "username"
467+
"ldap.attribute" = "cn"
468+
}
461469
}
462470

463471

keycloak/keycloak_client.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"crypto/x509"
88
"encoding/json"
99
"fmt"
10-
"github.com/hashicorp/terraform-plugin-log/tflog"
1110
"io"
1211
"net/http"
1312
"net/http/cookiejar"
@@ -18,6 +17,7 @@ import (
1817
"time"
1918

2019
"github.com/hashicorp/go-version"
20+
"github.com/hashicorp/terraform-plugin-log/tflog"
2121

2222
"golang.org/x/net/publicsuffix"
2323

@@ -293,7 +293,7 @@ func (keycloakClient *KeycloakClient) addRequestHeaders(request *http.Request) {
293293
request.Header.Set("User-Agent", keycloakClient.userAgent)
294294
}
295295

296-
if request.Method == http.MethodPost || request.Method == http.MethodPut || request.Method == http.MethodDelete {
296+
if request.Header.Get("Content-type") == "" && (request.Method == http.MethodPost || request.Method == http.MethodPut || request.Method == http.MethodDelete) {
297297
request.Header.Set("Content-type", "application/json")
298298
}
299299
}

keycloak/realm_translation.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package keycloak
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
)
10+
11+
func (keycloakClient *KeycloakClient) UpdateRealmLocalizationTexts(ctx context.Context, realmId string, locale string, texts map[string]string) error {
12+
var existingtexts map[string]string
13+
14+
data, _ := keycloakClient.getRaw(ctx, fmt.Sprintf("/realms/%s/localization/%s", realmId, locale), nil)
15+
err := json.Unmarshal(data, &existingtexts)
16+
if err != nil {
17+
return nil
18+
}
19+
textsToDelete := make([]string, 0)
20+
for key := range existingtexts {
21+
if _, exists := texts[key]; !exists {
22+
textsToDelete = append(textsToDelete, key)
23+
}
24+
}
25+
for _, key := range textsToDelete {
26+
err := keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, locale, key), nil)
27+
if err != nil {
28+
return err
29+
}
30+
}
31+
for key, value := range texts {
32+
err := keycloakClient.putPlain(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, locale, key), value)
33+
if err != nil {
34+
return err
35+
}
36+
}
37+
return nil
38+
}
39+
40+
func (keycloakClient *KeycloakClient) putPlain(ctx context.Context, path string, requestBody string) error {
41+
resourceUrl := keycloakClient.baseUrl + apiUrl + path
42+
request, err := http.NewRequestWithContext(ctx, http.MethodPut, resourceUrl, bytes.NewReader([]byte(requestBody)))
43+
if err != nil {
44+
return err
45+
}
46+
request.Header.Set("Content-type", "text/plain")
47+
_, _, err = keycloakClient.sendRequest(ctx, request, []byte(requestBody))
48+
return err
49+
}
50+
51+
func (keycloakClient *KeycloakClient) GetRealmLocalizationTexts(ctx context.Context, realmId string, locale string) (*map[string]string, error) {
52+
keyValues := make(map[string]string)
53+
err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/localization/%s", realmId, locale), &keyValues, nil)
54+
if err != nil {
55+
return nil, err
56+
}
57+
return &keyValues, nil
58+
}
59+
60+
func (keycloakClient *KeycloakClient) DeleteRealmLocalizationTexts(ctx context.Context, realmId string, locale string, texts map[string]string) error {
61+
for key := range texts {
62+
err := keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/localization/%s/%s", realmId, locale, key), nil)
63+
if err != nil {
64+
return err
65+
}
66+
}
67+
return nil
68+
}

provider/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
4141
"keycloak_realm_keystore_rsa": resourceKeycloakRealmKeystoreRsa(),
4242
"keycloak_realm_keystore_rsa_generated": resourceKeycloakRealmKeystoreRsaGenerated(),
4343
"keycloak_realm_user_profile": resourceKeycloakRealmUserProfile(),
44+
"keycloak_realm_localization": resourceKeycloakRealmLocalization(),
4445
"keycloak_required_action": resourceKeycloakRequiredAction(),
4546
"keycloak_group": resourceKeycloakGroup(),
4647
"keycloak_group_memberships": resourceKeycloakGroupMemberships(),

provider/resource_keycloak_realm.go

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package provider
22

33
import (
44
"context"
5+
56
"github.com/hashicorp/go-version"
67
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
78
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
"github.com/keycloak/terraform-provider-keycloak/keycloak"
10+
)
11+
12+
func resourceKeycloakRealmLocalization() *schema.Resource {
13+
return &schema.Resource{
14+
CreateContext: resourceKeycloakRealmLocalizationTextsUpdate,
15+
ReadContext: resourceKeycloakRealmLocalizationTextsRead,
16+
DeleteContext: resourceKeycloakRealmLocalizationTextsDelete,
17+
UpdateContext: resourceKeycloakRealmLocalizationTextsUpdate,
18+
Description: "Manage realm-level localization texts.",
19+
Schema: map[string]*schema.Schema{
20+
"realm_id": {
21+
Type: schema.TypeString,
22+
Required: true,
23+
ForceNew: true,
24+
Description: "The realm in which the texts exists.",
25+
},
26+
"locale": {
27+
Type: schema.TypeString,
28+
Required: true,
29+
ForceNew: true,
30+
Description: "The locale for the localization texts.",
31+
},
32+
"texts": {
33+
Optional: true,
34+
Type: schema.TypeMap,
35+
Elem: &schema.Schema{
36+
Type: schema.TypeString,
37+
},
38+
Description: "The mapping of localization texts keys to values.",
39+
},
40+
},
41+
}
42+
}
43+
44+
func resourceKeycloakRealmLocalizationTextsRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
45+
keycloakClient := meta.(*keycloak.KeycloakClient)
46+
realmId := data.Get("realm_id").(string)
47+
locale := data.Get("locale").(string)
48+
realmLocaleTexts, err := keycloakClient.GetRealmLocalizationTexts(ctx, realmId, locale)
49+
if err != nil {
50+
return diag.FromErr(err)
51+
}
52+
data.Set("texts", realmLocaleTexts)
53+
return nil
54+
}
55+
56+
func resourceKeycloakRealmLocalizationTextsUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
57+
client := meta.(*keycloak.KeycloakClient)
58+
realm := d.Get("realm_id").(string)
59+
locale := d.Get("locale").(string)
60+
texts := d.Get("texts").(map[string]interface{})
61+
textsConverted := convertTexts(texts)
62+
63+
err := client.UpdateRealmLocalizationTexts(ctx, realm, locale, textsConverted)
64+
if err != nil {
65+
return diag.FromErr(err)
66+
}
67+
68+
d.SetId(fmt.Sprintf("%s/%s", realm, locale)) // Set resource ID as "realm/locale"
69+
return resourceKeycloakRealmLocalizationTextsRead(ctx, d, meta)
70+
}
71+
72+
func resourceKeycloakRealmLocalizationTextsDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
73+
client := meta.(*keycloak.KeycloakClient)
74+
realm := d.Get("realm_id").(string)
75+
locale := d.Get("locale").(string)
76+
texts := d.Get("texts").(map[string]interface{})
77+
textsConverted := convertTexts(texts)
78+
79+
err := client.DeleteRealmLocalizationTexts(ctx, realm, locale, textsConverted)
80+
if err != nil {
81+
return diag.FromErr(err)
82+
}
83+
84+
d.SetId("")
85+
return nil
86+
}
87+
88+
func convertTexts(texts map[string]interface{}) map[string]string {
89+
translionsConverted := make(map[string]string)
90+
for key, value := range texts {
91+
strValue, ok := value.(string)
92+
if !ok {
93+
panic(fmt.Sprintf("expected string, got %T for key %s", value, key))
94+
}
95+
translionsConverted[key] = strValue
96+
}
97+
98+
return translionsConverted
99+
}

0 commit comments

Comments
 (0)