Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configure default and optional client scopes per realm via dedicated resources #1079

Merged
merged 2 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/resources/realm.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,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