Skip to content

Commit 93db672

Browse files
authored
add support for configuring default and optional client scopes per realm via dedicated resources (#1079)
Signed-off-by: Philipp Böhm <[email protected]>
1 parent 4d7f55a commit 93db672

10 files changed

+663
-21
lines changed

docs/resources/realm.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ Each of these attributes are blocks with the following attributes:
243243

244244
## Default Client Scopes
245245

246-
- `default_default_client_scopes` - (Optional) A list of default `default client scopes` to be used for client definitions. Defaults to `[]` or keycloak's built-in default `default client-scopes`.
247-
- `default_optional_client_scopes` - (Optional) A list of default `optional client scopes` to be used for client definitions. Defaults to `[]` or keycloak's built-in default `optional client-scopes`.
246+
- `default_default_client_scopes` - (Optional) A list of default `default client scopes` to be used for client definitions. Defaults to `[]` or keycloak's built-in default `default client-scopes`. For an alternative, please refer to the dedicated resource `keycloak_realm_default_client_scopes`.
247+
- `default_optional_client_scopes` - (Optional) A list of default `optional client scopes` to be used for client definitions. Defaults to `[]` or keycloak's built-in default `optional client-scopes`. For an alternative, please refer to the dedicated resource `keycloak_realm_optional_client_scopes`.
248248

249249
## Import
250250

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
page_title: "keycloak_realm_default_client_scopes Resource"
3+
---
4+
5+
# keycloak\_realm\_default\_client\_scopes Resource
6+
7+
Allows you to manage the set of default client scopes for a Keycloak realm, which are used when new clients are created.
8+
9+
Note that this resource attempts to be an **authoritative** source over the default client scopes for a Keycloak realm,
10+
so any Keycloak defaults and manual adjustments will be overwritten.
11+
12+
13+
## Example Usage
14+
15+
```hcl
16+
resource "keycloak_realm" "realm" {
17+
realm = "my-realm"
18+
enabled = true
19+
}
20+
21+
resource "keycloak_openid_client_scope" "client_scope" {
22+
realm_id = keycloak_realm.realm.id
23+
name = "test-client-scope"
24+
}
25+
26+
resource "keycloak_realm_default_client_scopes" "default_scopes" {
27+
realm_id = keycloak_realm.realm.id
28+
29+
default_scopes = [
30+
"profile",
31+
"email",
32+
"roles",
33+
"web-origins",
34+
keycloak_openid_client_scope.client_scope.name,
35+
]
36+
}
37+
```
38+
39+
## Argument Reference
40+
41+
- `realm_id` - (Required) The realm this client and scopes exists in.
42+
- `default_scopes` - (Required) An array of default client scope names that should be used when creating new Keycloak clients.
43+
44+
## Import
45+
46+
This resource does not support import. Instead of importing, feel free to create this resource
47+
as if it did not already exist on the server.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
page_title: "keycloak_realm_optional_client_scopes Resource"
3+
---
4+
5+
# keycloak\_realm\_optional\_client\_scopes Resource
6+
7+
Allows you to manage the set of optional client scopes for a Keycloak realm, which are used when new clients are created.
8+
9+
Note that this resource attempts to be an **authoritative** source over the optional client scopes for a Keycloak realm,
10+
so any Keycloak defaults and manual adjustments will be overwritten.
11+
12+
13+
## Example Usage
14+
15+
```hcl
16+
resource "keycloak_realm" "realm" {
17+
realm = "my-realm"
18+
enabled = true
19+
}
20+
21+
resource "keycloak_openid_client_scope" "client_scope" {
22+
realm_id = keycloak_realm.realm.id
23+
name = "test-client-scope"
24+
}
25+
26+
resource "keycloak_realm_optional_client_scopes" "optional_scopes" {
27+
realm_id = keycloak_realm.realm.id
28+
29+
optional_scopes = [
30+
"address",
31+
"phone",
32+
"offline_access",
33+
"microprofile-jwt",
34+
keycloak_openid_client_scope.client_scope.name
35+
]
36+
}
37+
```
38+
39+
## Argument Reference
40+
41+
- `realm_id` - (Required) The realm this client and scopes exists in.
42+
- `optional_scopes` - (Required) An array of optional client scope names that should be used when creating new Keycloak clients.
43+
44+
## Import
45+
46+
This resource does not support import. Instead of importing, feel free to create this resource
47+
as if it did not already exist on the server.

keycloak/openid_client.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -256,25 +256,6 @@ func (keycloakClient *KeycloakClient) GetOpenidClientOptionalScopes(ctx context.
256256
return keycloakClient.getOpenidClientScopes(ctx, realmId, clientId, "optional")
257257
}
258258

259-
func (keycloakClient *KeycloakClient) getRealmClientScopes(ctx context.Context, realmId, t string) ([]*OpenidClientScope, error) {
260-
var scopes []*OpenidClientScope
261-
262-
err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes", realmId, t), &scopes, nil)
263-
if err != nil {
264-
return nil, err
265-
}
266-
267-
return scopes, nil
268-
}
269-
270-
func (keycloakClient *KeycloakClient) GetRealmDefaultClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
271-
return keycloakClient.getRealmClientScopes(ctx, realmId, "default")
272-
}
273-
274-
func (keycloakClient *KeycloakClient) GetRealmOptionalClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
275-
return keycloakClient.getRealmClientScopes(ctx, realmId, "optional")
276-
}
277-
278259
func (keycloakClient *KeycloakClient) attachOpenidClientScopes(ctx context.Context, realmId, clientId, t string, scopeNames []string) error {
279260
openidClient, err := keycloakClient.GetOpenidClient(ctx, realmId, clientId)
280261
if err != nil && ErrorIs404(err) {

keycloak/realm_client_scope.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package keycloak
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
)
8+
9+
func (keycloakClient *KeycloakClient) getRealmClientScopesOfType(ctx context.Context, realmId, t string) ([]*OpenidClientScope, error) {
10+
var scopes []*OpenidClientScope
11+
12+
err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes", realmId, t), &scopes, nil)
13+
if err != nil {
14+
return nil, err
15+
}
16+
17+
return scopes, nil
18+
}
19+
20+
func (keycloakClient *KeycloakClient) GetRealmDefaultClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
21+
return keycloakClient.getRealmClientScopesOfType(ctx, realmId, "default")
22+
}
23+
24+
func (keycloakClient *KeycloakClient) GetRealmOptionalClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
25+
return keycloakClient.getRealmClientScopesOfType(ctx, realmId, "optional")
26+
}
27+
28+
func (keycloakClient *KeycloakClient) resolveClientScopeNamesIntoIds(ctx context.Context, realmId string, scopeNames []string) ([]string, error) {
29+
var scopeIds []string
30+
var clientScopes []OpenidClientScope
31+
32+
err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/client-scopes", realmId), &clientScopes, nil)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
ScopeNames:
38+
for _, scopeName := range scopeNames {
39+
for _, clientScope := range clientScopes {
40+
if clientScope.Name == scopeName {
41+
scopeIds = append(scopeIds, clientScope.Id)
42+
continue ScopeNames
43+
}
44+
}
45+
46+
return nil, errors.New(fmt.Sprintf("Client scope with name %s not found in realm %s", scopeName, realmId))
47+
}
48+
49+
return scopeIds, nil
50+
}
51+
52+
func (keycloakClient *KeycloakClient) resolveAndHandleClientScopes(ctx context.Context, realmId string, scopeNames []string, handler func(context.Context, string, string) error) error {
53+
scopeIds, err := keycloakClient.resolveClientScopeNamesIntoIds(ctx, realmId, scopeNames)
54+
if err != nil {
55+
return err
56+
}
57+
58+
for _, scopeId := range scopeIds {
59+
if err := handler(ctx, realmId, scopeId); err != nil {
60+
return err
61+
}
62+
}
63+
64+
return nil
65+
}
66+
67+
func (keycloakClient *KeycloakClient) markClientScopeAs(ctx context.Context, realmId, scopeId, t string) error {
68+
return keycloakClient.put(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes/%s", realmId, t, scopeId), nil)
69+
}
70+
71+
func (keycloakClient *KeycloakClient) MarkClientScopesAsRealmDefault(ctx context.Context, realmId string, scopeNames []string) error {
72+
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
73+
return keycloakClient.markClientScopeAs(ctx, realmId, scopeId, "default")
74+
})
75+
}
76+
77+
func (keycloakClient *KeycloakClient) MarkClientScopesAsRealmOptional(ctx context.Context, realmId string, scopeNames []string) error {
78+
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
79+
return keycloakClient.markClientScopeAs(ctx, realmId, scopeId, "optional")
80+
})
81+
}
82+
83+
func (keycloakClient *KeycloakClient) unmarkClientScopeAs(ctx context.Context, realmId, scopeId, t string) error {
84+
return keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes/%s", realmId, t, scopeId), nil)
85+
}
86+
87+
func (keycloakClient *KeycloakClient) UnmarkClientScopesAsRealmDefault(ctx context.Context, realmId string, scopeNames []string) error {
88+
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
89+
return keycloakClient.unmarkClientScopeAs(ctx, realmId, scopeId, "default")
90+
})
91+
}
92+
93+
func (keycloakClient *KeycloakClient) UnmarkClientScopesAsRealmOptional(ctx context.Context, realmId string, scopeNames []string) error {
94+
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
95+
return keycloakClient.unmarkClientScopeAs(ctx, realmId, scopeId, "optional")
96+
})
97+
}

provider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
3232
ResourcesMap: map[string]*schema.Resource{
3333
"keycloak_realm": resourceKeycloakRealm(),
3434
"keycloak_realm_events": resourceKeycloakRealmEvents(),
35+
"keycloak_realm_default_client_scopes": resourceKeycloakRealmDefaultClientScopes(),
36+
"keycloak_realm_optional_client_scopes": resourceKeycloakRealmOptionalClientScopes(),
3537
"keycloak_realm_keystore_aes_generated": resourceKeycloakRealmKeystoreAesGenerated(),
3638
"keycloak_realm_keystore_ecdsa_generated": resourceKeycloakRealmKeystoreEcdsaGenerated(),
3739
"keycloak_realm_keystore_hmac_generated": resourceKeycloakRealmKeystoreHmacGenerated(),
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
7+
"github.com/keycloak/terraform-provider-keycloak/keycloak"
8+
)
9+
10+
func resourceKeycloakRealmDefaultClientScopes() *schema.Resource {
11+
return &schema.Resource{
12+
CreateContext: resourceKeycloakRealmDefaultClientScopesReconcile,
13+
ReadContext: resourceKeycloakRealmDefaultClientScopesRead,
14+
DeleteContext: resourceKeycloakRealmDefaultClientScopesDelete,
15+
UpdateContext: resourceKeycloakRealmDefaultClientScopesReconcile,
16+
Schema: map[string]*schema.Schema{
17+
"realm_id": {
18+
Type: schema.TypeString,
19+
Required: true,
20+
ForceNew: true,
21+
},
22+
"default_scopes": {
23+
Type: schema.TypeSet,
24+
Elem: &schema.Schema{Type: schema.TypeString},
25+
Required: true,
26+
Set: schema.HashString,
27+
},
28+
},
29+
}
30+
}
31+
32+
func resourceKeycloakRealmDefaultClientScopesRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
33+
keycloakClient := meta.(*keycloak.KeycloakClient)
34+
35+
realmId := data.Get("realm_id").(string)
36+
37+
defaultClientScopes, err := keycloakClient.GetRealmDefaultClientScopes(ctx, realmId)
38+
if err != nil {
39+
return handleNotFoundError(ctx, err, data)
40+
}
41+
42+
var scopeNames []string
43+
for _, clientScope := range defaultClientScopes {
44+
scopeNames = append(scopeNames, clientScope.Name)
45+
}
46+
47+
data.Set("default_scopes", scopeNames)
48+
data.SetId(realmId)
49+
50+
return nil
51+
}
52+
53+
func resourceKeycloakRealmDefaultClientScopesReconcile(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
54+
keycloakClient := meta.(*keycloak.KeycloakClient)
55+
56+
realmId := data.Get("realm_id").(string)
57+
tfDefaultClientScopes := data.Get("default_scopes").(*schema.Set)
58+
59+
keycloakDefaultClientScopes, err := keycloakClient.GetRealmDefaultClientScopes(ctx, realmId)
60+
if err != nil {
61+
return diag.FromErr(err)
62+
}
63+
64+
var scopesToUnmark []string
65+
for _, keycloakDefaultClientScope := range keycloakDefaultClientScopes {
66+
// if this scope is a default client scope in keycloak and tf state, no update is required
67+
if tfDefaultClientScopes.Contains(keycloakDefaultClientScope.Name) {
68+
tfDefaultClientScopes.Remove(keycloakDefaultClientScope.Name)
69+
} else {
70+
// if this scope is marked as default in keycloak but not in tf state unmark it
71+
scopesToUnmark = append(scopesToUnmark, keycloakDefaultClientScope.Name)
72+
}
73+
}
74+
75+
// unmark scopes that aren't in tf state
76+
err = keycloakClient.UnmarkClientScopesAsRealmDefault(ctx, realmId, scopesToUnmark)
77+
if err != nil {
78+
return diag.FromErr(err)
79+
}
80+
81+
// mark scopes as default that exist in tf state but not in keycloak
82+
err = keycloakClient.MarkClientScopesAsRealmDefault(ctx, realmId, interfaceSliceToStringSlice(tfDefaultClientScopes.List()))
83+
if err != nil {
84+
return diag.FromErr(err)
85+
}
86+
87+
data.SetId(realmId)
88+
89+
return resourceKeycloakRealmDefaultClientScopesRead(ctx, data, meta)
90+
}
91+
92+
func resourceKeycloakRealmDefaultClientScopesDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
93+
keycloakClient := meta.(*keycloak.KeycloakClient)
94+
95+
realmId := data.Get("realm_id").(string)
96+
defaultClientScopes := data.Get("default_scopes").(*schema.Set)
97+
98+
return diag.FromErr(keycloakClient.UnmarkClientScopesAsRealmDefault(ctx, realmId, interfaceSliceToStringSlice(defaultClientScopes.List())))
99+
}

0 commit comments

Comments
 (0)