Skip to content

Commit 226aa90

Browse files
authored
fix: handles json arrays and objects in client policy and profile (#1256) (#1338)
Unmarshals and marshals JSON strings in configuration fields of conditions and executors for client policies and profiles. Signed-off-by: Remco Jansen <[email protected]>
1 parent e70b201 commit 226aa90

File tree

4 files changed

+206
-19
lines changed

4 files changed

+206
-19
lines changed

provider/resource_keycloak_realm_client_policy_profile.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package provider
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"strings"
68

79
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
810
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -150,8 +152,22 @@ func mapFromDataToRealmClientPolicyProfile(data *schema.ResourceData) *keycloak.
150152
executorMap := executor.(map[string]interface{})
151153

152154
exec := keycloak.RealmClientPolicyProfileExecutor{
153-
Name: executorMap["name"].(string),
154-
Configuration: executorMap["configuration"].(map[string]interface{}),
155+
Name: executorMap["name"].(string),
156+
}
157+
158+
if v, ok := executorMap["configuration"]; ok {
159+
configurations := make(map[string]interface{})
160+
for key, value := range v.(map[string]interface{}) {
161+
// handle json objects and arrays
162+
if strings.HasPrefix(value.(string), "{") || strings.HasPrefix(value.(string), "[") {
163+
var t interface{}
164+
json.Unmarshal([]byte(value.(string)), &t)
165+
configurations[key] = t
166+
continue
167+
}
168+
configurations[key] = value
169+
}
170+
exec.Configuration = configurations
155171
}
156172

157173
executors = append(executors, exec)
@@ -174,8 +190,22 @@ func mapFromRealmClientPolicyProfileToData(data *schema.ResourceData, profile *k
174190
for _, ex := range profile.Executors {
175191

176192
executorMap := map[string]interface{}{
177-
"name": ex.Name,
178-
"configuration": ex.Configuration,
193+
"name": ex.Name,
194+
}
195+
196+
if ex.Configuration != nil {
197+
configurations := make(map[string]interface{})
198+
for k, v := range ex.Configuration {
199+
switch v.(type) {
200+
// handle json objects and arrays
201+
case map[string]interface{}, []interface{}:
202+
s, _ := json.Marshal(v)
203+
configurations[k] = string(s)
204+
default:
205+
configurations[k] = v
206+
}
207+
}
208+
executorMap["configuration"] = configurations
179209
}
180210
executors = append(executors, executorMap)
181211
}

provider/resource_keycloak_realm_client_policy_profile_policy.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package provider
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"strings"
68

79
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
810
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -169,7 +171,14 @@ func mapFromDataToRealmClientPolicyProfilePolicy(data *schema.ResourceData) *key
169171
if v, ok := conditionMap["configuration"]; ok {
170172
configurations := make(map[string]interface{})
171173
for key, value := range v.(map[string]interface{}) {
172-
configurations[key] = value.(string)
174+
// handle json objects and arrays
175+
if strings.HasPrefix(value.(string), "{") || strings.HasPrefix(value.(string), "[") {
176+
var t interface{}
177+
json.Unmarshal([]byte(value.(string)), &t)
178+
configurations[key] = t
179+
continue
180+
}
181+
configurations[key] = value
173182
}
174183
cond.Configuration = configurations
175184
}
@@ -208,7 +217,14 @@ func mapFromRealmClientPolicyProfilePolicyToData(data *schema.ResourceData, poli
208217
if cond.Configuration != nil {
209218
configurations := make(map[string]interface{})
210219
for k, v := range cond.Configuration {
211-
configurations[k] = v
220+
switch v.(type) {
221+
// handle json objects and arrays
222+
case map[string]interface{}, []interface{}:
223+
s, _ := json.Marshal(v)
224+
configurations[k] = string(s)
225+
default:
226+
configurations[k] = v
227+
}
212228
}
213229
conditionMap["configuration"] = configurations
214230
}

provider/resource_keycloak_realm_client_policy_profile_test.go

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package provider
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"reflect"
57
"testing"
68

79
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
@@ -31,39 +33,114 @@ func TestAccKeycloakRealmClientPolicyProfile_basicWithExecutor(t *testing.T) {
3133
resourceName := "test-profile-with-executor"
3234
description := "Test description with executor"
3335
executorName := "pkce-enforcer"
36+
configuration := map[string]interface{}{
37+
"auto-configure": "true",
38+
}
3439

3540
resource.Test(t, resource.TestCase{
3641
ProviderFactories: testAccProviderFactories,
3742
PreCheck: func() { testAccPreCheck(t) },
3843
Steps: []resource.TestStep{
3944
{
40-
Config: testKeycloakRealmClientPolicyProfile_basicWithExecutor(realmName, resourceName, description, executorName),
45+
Config: testKeycloakRealmClientPolicyProfile_basicWithExecutor(realmName, resourceName, description, executorName, testKeycloakRealmClientPolicyProfile_mapConfig(configuration)),
4146
Check: testAccCheckKeycloakRealmClientPolicyProfileWithExecutorExists(realmName, resourceName, executorName),
4247
},
4348
},
4449
})
4550
}
4651

52+
func TestAccKeycloakRealmClientPolicyProfile_basicWithExecutorAndJSON(t *testing.T) {
53+
realmName := acctest.RandomWithPrefix("tf-acc")
54+
resourceName := "test-profile-with-executor-and-configuration"
55+
description := "Test description with executor and configuration"
56+
executorName := "secure-client-authenticator"
57+
configuration := map[string]interface{}{
58+
"allowed-client-authenticators": []string{"client-secret", "client-secret-jwt"},
59+
"default-client-authenticator": "client-secret",
60+
}
61+
62+
resource.Test(t, resource.TestCase{
63+
ProviderFactories: testAccProviderFactories,
64+
PreCheck: func() { testAccPreCheck(t) },
65+
Steps: []resource.TestStep{
66+
{
67+
Config: testKeycloakRealmClientPolicyProfile_basicWithExecutor(realmName, resourceName, description, executorName, testKeycloakRealmClientPolicyProfile_mapConfig(configuration)),
68+
Check: testAccCheckKeycloakRealmClientPolicyProfileWithExecutorMatches(realmName, resourceName, executorName, configuration),
69+
},
70+
},
71+
})
72+
}
73+
4774
func TestAccKeycloakRealmClientPolicyProfile_basicWithPolicy(t *testing.T) {
4875
realmName := acctest.RandomWithPrefix("tf-acc")
4976
profileName := "test-profile"
5077
profileDescription := "Test profile description"
5178
policyName := "test-policy"
5279
policyDescription := "Test policy description"
5380
conditionName := "client-updater-source-roles"
81+
configuration := map[string]interface{}{
82+
"is_negative_logic": false,
83+
"attributes": []map[string]string{
84+
{
85+
"key": "test-key",
86+
"value": "test-value",
87+
},
88+
},
89+
}
5490

5591
resource.Test(t, resource.TestCase{
5692
ProviderFactories: testAccProviderFactories,
5793
PreCheck: func() { testAccPreCheck(t) },
5894
Steps: []resource.TestStep{
5995
{
60-
Config: testKeycloakRealmClientPolicyProfile_basicWithPolicy(realmName, profileName, profileDescription, policyName, policyDescription, conditionName),
96+
Config: testKeycloakRealmClientPolicyProfile_basicWithPolicy(realmName, profileName, profileDescription, policyName, policyDescription, conditionName, testKeycloakRealmClientPolicyProfile_mapConfig(configuration)),
6197
Check: testAccCheckKeycloakRealmClientPolicyProfilePolicyExists(realmName, policyName),
6298
},
6399
},
64100
})
65101
}
66102

103+
func TestAccKeycloakRealmClientPolicyProfile_basicWithPolicyAndJSON(t *testing.T) {
104+
realmName := acctest.RandomWithPrefix("tf-acc")
105+
profileName := "test-profile"
106+
profileDescription := "Test profile description"
107+
policyName := "test-policy"
108+
policyDescription := "Test policy description"
109+
conditionName := "client-updater-context"
110+
configuration := map[string]interface{}{
111+
"is_negative_logic": false,
112+
"update-client-source": []string{"ByInitialAccessToken", "ByRegistrationAccessToken"},
113+
}
114+
115+
resource.Test(t, resource.TestCase{
116+
ProviderFactories: testAccProviderFactories,
117+
PreCheck: func() { testAccPreCheck(t) },
118+
Steps: []resource.TestStep{
119+
{
120+
Config: testKeycloakRealmClientPolicyProfile_basicWithPolicy(realmName, profileName, profileDescription, policyName, policyDescription, conditionName, testKeycloakRealmClientPolicyProfile_mapConfig(configuration)),
121+
Check: testAccCheckKeycloakRealmClientPolicyProfilePolicyMatches(realmName, policyName, conditionName, configuration),
122+
},
123+
},
124+
})
125+
}
126+
127+
func testKeycloakRealmClientPolicyProfile_mapConfig(configuration map[string]interface{}) string {
128+
var s string = "{"
129+
for k, v := range configuration {
130+
switch reflect.TypeOf(v).Kind() {
131+
case reflect.Map, reflect.Slice:
132+
jsonStr, _ := json.Marshal(v)
133+
s += fmt.Sprintf("%s = jsonencode(%s)\n", k, string(jsonStr))
134+
case reflect.String:
135+
s += fmt.Sprintf("%s = \"%v\"\n", k, v)
136+
default:
137+
s += fmt.Sprintf("%s = %v\n", k, v)
138+
}
139+
}
140+
s += "}"
141+
return s
142+
}
143+
67144
func testKeycloakRealmClientPolicyProfile_basic(realm string, name string, description string) string {
68145
return fmt.Sprintf(`
69146
resource "keycloak_realm" "realm" {
@@ -78,7 +155,7 @@ resource "keycloak_realm_client_policy_profile" "profile" {
78155
`, realm, name, description)
79156
}
80157

81-
func testKeycloakRealmClientPolicyProfile_basicWithExecutor(realm string, name string, description string, executorName string) string {
158+
func testKeycloakRealmClientPolicyProfile_basicWithExecutor(realm string, name string, description string, executorName string, configuration string) string {
82159
return fmt.Sprintf(`
83160
resource "keycloak_realm" "realm" {
84161
realm = "%s"
@@ -91,15 +168,13 @@ resource "keycloak_realm_client_policy_profile" "profile" {
91168
92169
executor {
93170
name = "%s"
94-
configuration = {
95-
auto-configure = "true"
96-
}
171+
configuration = %s
97172
}
98173
}
99-
`, realm, name, description, executorName)
174+
`, realm, name, description, executorName, configuration)
100175
}
101176

102-
func testKeycloakRealmClientPolicyProfile_basicWithPolicy(realm string, profileName string, profileDescription string, policyName string, policyDescription string, conditionName string) string {
177+
func testKeycloakRealmClientPolicyProfile_basicWithPolicy(realm string, profileName string, profileDescription string, policyName string, policyDescription string, conditionName string, configuration string) string {
103178
return fmt.Sprintf(`
104179
resource "keycloak_realm" "realm" {
105180
realm = "%s"
@@ -122,13 +197,10 @@ resource "keycloak_realm_client_policy_profile_policy" "policy" {
122197
123198
condition {
124199
name = "%s"
125-
configuration = {
126-
is_negative_logic = false
127-
attributes = jsonencode([{"key": "test-key", "value": "test-value"}])
128-
}
200+
configuration = %s
129201
}
130202
}
131-
`, realm, profileName, profileDescription, policyName, policyDescription, conditionName)
203+
`, realm, profileName, profileDescription, policyName, policyDescription, conditionName, configuration)
132204
}
133205

134206
func testAccCheckKeycloakRealmClientPolicyProfileExists(realm string, profileName string) resource.TestCheckFunc {
@@ -157,6 +229,29 @@ func testAccCheckKeycloakRealmClientPolicyProfileWithExecutorExists(realm string
157229
}
158230
}
159231

232+
func testAccCheckKeycloakRealmClientPolicyProfileWithExecutorMatches(realm string, profileName string, executorName string, configuration map[string]interface{}) resource.TestCheckFunc {
233+
return func(s *terraform.State) error {
234+
profile, err := keycloakClient.GetRealmClientPolicyProfileByName(testCtx, realm, profileName)
235+
if err != nil {
236+
return fmt.Errorf("Client policy profile not found: %s", profileName)
237+
}
238+
239+
if profile.Executors[0].Name != executorName {
240+
return fmt.Errorf("Client policy profile executor not found: %s", executorName)
241+
}
242+
243+
for k, got := range profile.Executors[0].Configuration {
244+
want := configuration[k]
245+
246+
if !equalsIgnoreType(got, want) {
247+
return fmt.Errorf("Client policy profile executor configuration does not match: want %v, got %v", want, got)
248+
}
249+
}
250+
251+
return nil
252+
}
253+
}
254+
160255
func testAccCheckKeycloakRealmClientPolicyProfilePolicyExists(realm string, policyName string) resource.TestCheckFunc {
161256
return func(s *terraform.State) error {
162257
_, err := keycloakClient.GetRealmClientPolicyProfilePolicyByName(testCtx, realm, policyName)
@@ -167,3 +262,26 @@ func testAccCheckKeycloakRealmClientPolicyProfilePolicyExists(realm string, poli
167262
return nil
168263
}
169264
}
265+
266+
func testAccCheckKeycloakRealmClientPolicyProfilePolicyMatches(realm string, policyName string, conditionName string, configuration map[string]interface{}) resource.TestCheckFunc {
267+
return func(s *terraform.State) error {
268+
policy, err := keycloakClient.GetRealmClientPolicyProfilePolicyByName(testCtx, realm, policyName)
269+
if err != nil {
270+
return fmt.Errorf("Client policy profile policy not found: %s", policyName)
271+
}
272+
273+
if policy.Conditions[0].Name != conditionName {
274+
return fmt.Errorf("Client policy profile policy condition not found: %s", conditionName)
275+
}
276+
277+
for k, got := range policy.Conditions[0].Configuration {
278+
want := configuration[k]
279+
280+
if !equalsIgnoreType(got, want) {
281+
return fmt.Errorf("Client policy profile policy condition configuration does not match: want %v, got %v", want, got)
282+
}
283+
}
284+
285+
return nil
286+
}
287+
}

provider/test_utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package provider
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"math/rand"
78
"os"
9+
"reflect"
810
"strings"
911
"testing"
1012
"time"
@@ -111,3 +113,24 @@ func TestCheckResourceAttrNot(name, key, value string) resource.TestCheckFunc {
111113
return nil
112114
}
113115
}
116+
117+
func equalsIgnoreType(want, got interface{}) bool {
118+
if reflect.DeepEqual(want, got) {
119+
return true
120+
}
121+
122+
// TODO this should be replaced with an actual comparison the json == json is a quick fix.
123+
wantJSON, _ := json.Marshal(want)
124+
gotJSON, _ := json.Marshal(got)
125+
126+
if string(wantJSON) == string(gotJSON) {
127+
return true
128+
}
129+
130+
// compare as strings (this handles "false" == false and "0" == 0)
131+
if fmt.Sprintf("%v", want) == fmt.Sprintf("%v", got) {
132+
return true
133+
}
134+
135+
return false
136+
}

0 commit comments

Comments
 (0)