Skip to content

Commit

Permalink
add support for configuring default and optional client scopes per re…
Browse files Browse the repository at this point in the history
…alm via dedicated resources

Signed-off-by: Philipp Böhm <[email protected]>
  • Loading branch information
pboehm committed Jan 20, 2025
1 parent e8ccbc6 commit 425c754
Show file tree
Hide file tree
Showing 10 changed files with 663 additions and 21 deletions.
4 changes: 2 additions & 2 deletions docs/resources/realm.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,8 @@ Each of these attributes are blocks with the following attributes:

## Default Client Scopes

- `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`.
- `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`.
- `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`.
- `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`.

## Import

Expand Down
47 changes: 47 additions & 0 deletions docs/resources/realm_default_client_scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
page_title: "keycloak_realm_default_client_scopes Resource"
---

# keycloak\_realm\_default\_client\_scopes Resource

Allows you to manage the set of default client scopes for a Keycloak realm, which are used when new clients are created.

Note that this resource attempts to be an **authoritative** source over the default client scopes for a Keycloak realm,
so any Keycloak defaults and manual adjustments will be overwritten.


## Example Usage

```hcl
resource "keycloak_realm" "realm" {
realm = "my-realm"
enabled = true
}
resource "keycloak_openid_client_scope" "client_scope" {
realm_id = keycloak_realm.realm.id
name = "test-client-scope"
}
resource "keycloak_realm_default_client_scopes" "default_scopes" {
realm_id = keycloak_realm.realm.id
default_scopes = [
"profile",
"email",
"roles",
"web-origins",
keycloak_openid_client_scope.client_scope.name,
]
}
```

## Argument Reference

- `realm_id` - (Required) The realm this client and scopes exists in.
- `default_scopes` - (Required) An array of default client scope names that should be used when creating new Keycloak clients.

## Import

This resource does not support import. Instead of importing, feel free to create this resource
as if it did not already exist on the server.
47 changes: 47 additions & 0 deletions docs/resources/realm_optional_client_scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
page_title: "keycloak_realm_optional_client_scopes Resource"
---

# keycloak\_realm\_optional\_client\_scopes Resource

Allows you to manage the set of optional client scopes for a Keycloak realm, which are used when new clients are created.

Note that this resource attempts to be an **authoritative** source over the optional client scopes for a Keycloak realm,
so any Keycloak defaults and manual adjustments will be overwritten.


## Example Usage

```hcl
resource "keycloak_realm" "realm" {
realm = "my-realm"
enabled = true
}
resource "keycloak_openid_client_scope" "client_scope" {
realm_id = keycloak_realm.realm.id
name = "test-client-scope"
}
resource "keycloak_realm_optional_client_scopes" "optional_scopes" {
realm_id = keycloak_realm.realm.id
optional_scopes = [
"address",
"phone",
"offline_access",
"microprofile-jwt",
keycloak_openid_client_scope.client_scope.name
]
}
```

## Argument Reference

- `realm_id` - (Required) The realm this client and scopes exists in.
- `optional_scopes` - (Required) An array of optional client scope names that should be used when creating new Keycloak clients.

## Import

This resource does not support import. Instead of importing, feel free to create this resource
as if it did not already exist on the server.
19 changes: 0 additions & 19 deletions keycloak/openid_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,25 +256,6 @@ func (keycloakClient *KeycloakClient) GetOpenidClientOptionalScopes(ctx context.
return keycloakClient.getOpenidClientScopes(ctx, realmId, clientId, "optional")
}

func (keycloakClient *KeycloakClient) getRealmClientScopes(ctx context.Context, realmId, t string) ([]*OpenidClientScope, error) {
var scopes []*OpenidClientScope

err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes", realmId, t), &scopes, nil)
if err != nil {
return nil, err
}

return scopes, nil
}

func (keycloakClient *KeycloakClient) GetRealmDefaultClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
return keycloakClient.getRealmClientScopes(ctx, realmId, "default")
}

func (keycloakClient *KeycloakClient) GetRealmOptionalClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
return keycloakClient.getRealmClientScopes(ctx, realmId, "optional")
}

func (keycloakClient *KeycloakClient) attachOpenidClientScopes(ctx context.Context, realmId, clientId, t string, scopeNames []string) error {
openidClient, err := keycloakClient.GetOpenidClient(ctx, realmId, clientId)
if err != nil && ErrorIs404(err) {
Expand Down
97 changes: 97 additions & 0 deletions keycloak/realm_client_scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package keycloak

import (
"context"
"errors"
"fmt"
)

func (keycloakClient *KeycloakClient) getRealmClientScopesOfType(ctx context.Context, realmId, t string) ([]*OpenidClientScope, error) {
var scopes []*OpenidClientScope

err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes", realmId, t), &scopes, nil)
if err != nil {
return nil, err
}

return scopes, nil
}

func (keycloakClient *KeycloakClient) GetRealmDefaultClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
return keycloakClient.getRealmClientScopesOfType(ctx, realmId, "default")
}

func (keycloakClient *KeycloakClient) GetRealmOptionalClientScopes(ctx context.Context, realmId string) ([]*OpenidClientScope, error) {
return keycloakClient.getRealmClientScopesOfType(ctx, realmId, "optional")
}

func (keycloakClient *KeycloakClient) resolveClientScopeNamesIntoIds(ctx context.Context, realmId string, scopeNames []string) ([]string, error) {
var scopeIds []string
var clientScopes []OpenidClientScope

err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/client-scopes", realmId), &clientScopes, nil)
if err != nil {
return nil, err
}

ScopeNames:
for _, scopeName := range scopeNames {
for _, clientScope := range clientScopes {
if clientScope.Name == scopeName {
scopeIds = append(scopeIds, clientScope.Id)
continue ScopeNames
}
}

return nil, errors.New(fmt.Sprintf("Client scope with name %s not found in realm %s", scopeName, realmId))
}

return scopeIds, nil
}

func (keycloakClient *KeycloakClient) resolveAndHandleClientScopes(ctx context.Context, realmId string, scopeNames []string, handler func(context.Context, string, string) error) error {
scopeIds, err := keycloakClient.resolveClientScopeNamesIntoIds(ctx, realmId, scopeNames)
if err != nil {
return err
}

for _, scopeId := range scopeIds {
if err := handler(ctx, realmId, scopeId); err != nil {
return err
}
}

return nil
}

func (keycloakClient *KeycloakClient) markClientScopeAs(ctx context.Context, realmId, scopeId, t string) error {
return keycloakClient.put(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes/%s", realmId, t, scopeId), nil)
}

func (keycloakClient *KeycloakClient) MarkClientScopesAsRealmDefault(ctx context.Context, realmId string, scopeNames []string) error {
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
return keycloakClient.markClientScopeAs(ctx, realmId, scopeId, "default")
})
}

func (keycloakClient *KeycloakClient) MarkClientScopesAsRealmOptional(ctx context.Context, realmId string, scopeNames []string) error {
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
return keycloakClient.markClientScopeAs(ctx, realmId, scopeId, "optional")
})
}

func (keycloakClient *KeycloakClient) unmarkClientScopeAs(ctx context.Context, realmId, scopeId, t string) error {
return keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/default-%s-client-scopes/%s", realmId, t, scopeId), nil)
}

func (keycloakClient *KeycloakClient) UnmarkClientScopesAsRealmDefault(ctx context.Context, realmId string, scopeNames []string) error {
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
return keycloakClient.unmarkClientScopeAs(ctx, realmId, scopeId, "default")
})
}

func (keycloakClient *KeycloakClient) UnmarkClientScopesAsRealmOptional(ctx context.Context, realmId string, scopeNames []string) error {
return keycloakClient.resolveAndHandleClientScopes(ctx, realmId, scopeNames, func(ctx context.Context, realmId, scopeId string) error {
return keycloakClient.unmarkClientScopeAs(ctx, realmId, scopeId, "optional")
})
}
2 changes: 2 additions & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
ResourcesMap: map[string]*schema.Resource{
"keycloak_realm": resourceKeycloakRealm(),
"keycloak_realm_events": resourceKeycloakRealmEvents(),
"keycloak_realm_default_client_scopes": resourceKeycloakRealmDefaultClientScopes(),
"keycloak_realm_optional_client_scopes": resourceKeycloakRealmOptionalClientScopes(),
"keycloak_realm_keystore_aes_generated": resourceKeycloakRealmKeystoreAesGenerated(),
"keycloak_realm_keystore_ecdsa_generated": resourceKeycloakRealmKeystoreEcdsaGenerated(),
"keycloak_realm_keystore_hmac_generated": resourceKeycloakRealmKeystoreHmacGenerated(),
Expand Down
99 changes: 99 additions & 0 deletions provider/resource_keycloak_realm_default_client_scopes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package provider

import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/keycloak/terraform-provider-keycloak/keycloak"
)

func resourceKeycloakRealmDefaultClientScopes() *schema.Resource {
return &schema.Resource{
CreateContext: resourceKeycloakRealmDefaultClientScopesReconcile,
ReadContext: resourceKeycloakRealmDefaultClientScopesRead,
DeleteContext: resourceKeycloakRealmDefaultClientScopesDelete,
UpdateContext: resourceKeycloakRealmDefaultClientScopesReconcile,
Schema: map[string]*schema.Schema{
"realm_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"default_scopes": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Required: true,
Set: schema.HashString,
},
},
}
}

func resourceKeycloakRealmDefaultClientScopesRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)

defaultClientScopes, err := keycloakClient.GetRealmDefaultClientScopes(ctx, realmId)
if err != nil {
return handleNotFoundError(ctx, err, data)
}

var scopeNames []string
for _, clientScope := range defaultClientScopes {
scopeNames = append(scopeNames, clientScope.Name)
}

data.Set("default_scopes", scopeNames)
data.SetId(realmId)

return nil
}

func resourceKeycloakRealmDefaultClientScopesReconcile(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)
tfDefaultClientScopes := data.Get("default_scopes").(*schema.Set)

keycloakDefaultClientScopes, err := keycloakClient.GetRealmDefaultClientScopes(ctx, realmId)
if err != nil {
return diag.FromErr(err)
}

var scopesToUnmark []string
for _, keycloakDefaultClientScope := range keycloakDefaultClientScopes {
// if this scope is a default client scope in keycloak and tf state, no update is required
if tfDefaultClientScopes.Contains(keycloakDefaultClientScope.Name) {
tfDefaultClientScopes.Remove(keycloakDefaultClientScope.Name)
} else {
// if this scope is marked as default in keycloak but not in tf state unmark it
scopesToUnmark = append(scopesToUnmark, keycloakDefaultClientScope.Name)
}
}

// unmark scopes that aren't in tf state
err = keycloakClient.UnmarkClientScopesAsRealmDefault(ctx, realmId, scopesToUnmark)
if err != nil {
return diag.FromErr(err)
}

// mark scopes as default that exist in tf state but not in keycloak
err = keycloakClient.MarkClientScopesAsRealmDefault(ctx, realmId, interfaceSliceToStringSlice(tfDefaultClientScopes.List()))
if err != nil {
return diag.FromErr(err)
}

data.SetId(realmId)

return resourceKeycloakRealmDefaultClientScopesRead(ctx, data, meta)
}

func resourceKeycloakRealmDefaultClientScopesDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
keycloakClient := meta.(*keycloak.KeycloakClient)

realmId := data.Get("realm_id").(string)
defaultClientScopes := data.Get("default_scopes").(*schema.Set)

return diag.FromErr(keycloakClient.UnmarkClientScopesAsRealmDefault(ctx, realmId, interfaceSliceToStringSlice(defaultClientScopes.List())))
}
Loading

0 comments on commit 425c754

Please sign in to comment.