Skip to content

new resource: keycloak_openid_client_optional_scopes #96

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

Merged
merged 11 commits into from
Feb 26, 2019
10 changes: 6 additions & 4 deletions docs/resources/keycloak_openid_client_default_scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ This means that once Terraform controls a particular client's default scopes,
it will attempt to remove any default scopes that were attached manually,
and it will attempt to add any default scopes that were detached manually.

By default, Keycloak sets the `profile` and `email` scopes as default scopes
for every newly created client. If you create this resource for the first
time and do not include these scopes, a following run of `terraform plan`
will result in changes.
By default, Keycloak sets the `profile`, `email`, `roles`, and `web-origins`
scopes as default scopes for every newly created client. If you create this
resource for the first time and do not include these scopes, a following run
of `terraform plan` will result in changes.

### Example Usage

Expand Down Expand Up @@ -43,6 +43,8 @@ resource "keycloak_openid_client_default_scopes" "client_default_scopes" {
default_scopes = [
"profile",
"email",
"roles",
"web-origins",
"${keycloak_openid_client_scope.client_scope.name}"
]
}
Expand Down
65 changes: 65 additions & 0 deletions docs/resources/keycloak_openid_client_optional_scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# keycloak_openid_client_optional_scopes

Allows for managing a Keycloak client's optional client scopes. An optional
scope that is attached to a client using the OpenID Connect protocol will
allow a client to request it using the OAuth 2.0 `scope` parameter. When
requested, the scope's protocol mappers defined within that scope will be
used to build claims for this client.

Note that this resource attempts to be an **authoritative** source over
optional scopes for a Keycloak client using the OpenID Connect protocol.
This means that once Terraform controls a particular client's optional scopes,
it will attempt to remove any optional scopes that were attached manually,
and it will attempt to add any optional scopes that were detached manually.

By default, Keycloak sets the `address`, `phone` and `offline_access` scopes as
optional scopes for every newly created client. If you create this resource for
the first time and do not include these scopes, a following run of `terraform plan`
will result in changes.

### Example Usage

```hcl
resource "keycloak_realm" "realm" {
realm = "my-realm"
enabled = true
}

resource "keycloak_openid_client" "client" {
realm_id = "${keycloak_realm.realm.id}"
client_id = "test-client"

access_type = "CONFIDENTIAL"
}

resource "keycloak_openid_client_scope" "client_scope" {
realm_id = "${keycloak_realm.realm.id}"
name = "test-client-scope"
}

resource "keycloak_openid_client_optional_scopes" "client_optional_scopes" {
realm_id = "${keycloak_realm.realm.id}"
client_id = "${keycloak_openid_client.client.id}"

optional_scopes = [
"address",
"phone",
"offline_access",
"${keycloak_openid_client_scope.client_scope.name}"
]
}

```

### Argument Reference

The following arguments are supported:

- `realm_id` - (Required) The realm this client and scopes exists in.
- `client_id` - (Required) The ID of the client to attach optional scopes to. Note that this is the unique ID of the client generated by Keycloak.
- `optional_scopes` - (Required) An array of client scope names to attach to this client as optional scopes.

### 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.
36 changes: 28 additions & 8 deletions example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,16 @@ resource "keycloak_openid_client" "test_client" {
client_secret = "secret"
}

resource "keycloak_openid_client_scope" "test_client_scope" {
name = "foo1"
resource "keycloak_openid_client_scope" "test_default_client_scope" {
name = "test-default-client-scope"
realm_id = "${keycloak_realm.test.id}"

description = "test"
consent_screen_text = "hello"
}

resource "keycloak_openid_client_scope" "test_optional_client_scope" {
name = "test-optional-client-scope"
realm_id = "${keycloak_realm.test.id}"

description = "test"
Expand All @@ -105,7 +113,19 @@ resource "keycloak_openid_client_default_scopes" "default_client_scopes" {
"email",
"roles",
"web-origins",
"${keycloak_openid_client_scope.test_client_scope.name}"
"${keycloak_openid_client_scope.test_default_client_scope.name}"
]
}

resource "keycloak_openid_client_optional_scopes" "optional_client_scopes" {
realm_id = "${keycloak_realm.test.id}"
client_id = "${keycloak_openid_client.test_client.id}"

optional_scopes = [
"address",
"phone",
"offline_access",
"${keycloak_openid_client_scope.test_optional_client_scope.name}"
]
}

Expand Down Expand Up @@ -195,7 +215,7 @@ resource "keycloak_openid_user_attribute_protocol_mapper" "map_user_attributes_c
resource "keycloak_openid_user_attribute_protocol_mapper" "map_user_attributes_client_scope" {
name = "tf-test-open-id-user-attribute-protocol-mapper-client-scope"
realm_id = "${keycloak_realm.test.id}"
client_scope_id = "${keycloak_openid_client_scope.test_client_scope.id}"
client_scope_id = "${keycloak_openid_client_scope.test_default_client_scope.id}"
user_attribute = "foo2"
claim_name = "bar2"
}
Expand All @@ -210,7 +230,7 @@ resource "keycloak_openid_group_membership_protocol_mapper" "map_group_membershi
resource "keycloak_openid_group_membership_protocol_mapper" "map_group_memberships_client_scope" {
name = "tf-test-open-id-group-membership-protocol-mapper-client-scope"
realm_id = "${keycloak_realm.test.id}"
client_scope_id = "${keycloak_openid_client_scope.test_client_scope.id}"
client_scope_id = "${keycloak_openid_client_scope.test_optional_client_scope.id}"
claim_name = "bar2"
}

Expand All @@ -223,7 +243,7 @@ resource "keycloak_openid_full_name_protocol_mapper" "map_full_names_client" {
resource "keycloak_openid_full_name_protocol_mapper" "map_full_names_client_scope" {
name = "tf-test-open-id-full-name-protocol-mapper-client-scope"
realm_id = "${keycloak_realm.test.id}"
client_scope_id = "${keycloak_openid_client_scope.test_client_scope.id}"
client_scope_id = "${keycloak_openid_client_scope.test_default_client_scope.id}"
}

resource "keycloak_openid_user_property_protocol_mapper" "map_user_properties_client" {
Expand All @@ -237,7 +257,7 @@ resource "keycloak_openid_user_property_protocol_mapper" "map_user_properties_cl
resource "keycloak_openid_user_property_protocol_mapper" "map_user_properties_client_scope" {
name = "tf-test-open-id-user-property-protocol-mapper-client-scope"
realm_id = "${keycloak_realm.test.id}"
client_scope_id = "${keycloak_openid_client_scope.test_client_scope.id}"
client_scope_id = "${keycloak_openid_client_scope.test_optional_client_scope.id}"
user_property = "foo2"
claim_name = "bar2"
}
Expand All @@ -254,7 +274,7 @@ resource "keycloak_openid_hardcoded_claim_protocol_mapper" "hardcoded_claim_clie
resource "keycloak_openid_hardcoded_claim_protocol_mapper" "hardcoded_claim_client_scope" {
name = "tf-test-open-id-hardcoded-claim-protocol-mapper-client-scope"
realm_id = "${keycloak_realm.test.id}"
client_scope_id = "${keycloak_openid_client_scope.test_client_scope.id}"
client_scope_id = "${keycloak_openid_client_scope.test_default_client_scope.id}"

claim_name = "foo"
claim_value = "bar"
Expand Down
61 changes: 55 additions & 6 deletions keycloak/openid_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,26 @@ func (keycloakClient *KeycloakClient) DeleteOpenidClient(realmId, id string) err
return keycloakClient.delete(fmt.Sprintf("/realms/%s/clients/%s", realmId, id))
}

func (keycloakClient *KeycloakClient) GetOpenidClientDefaultScopes(realmId, clientId string) ([]*OpenidClientScope, error) {
func (keycloakClient *KeycloakClient) getOpenidClientScopes(realmId, clientId, t string) ([]*OpenidClientScope, error) {
var scopes []*OpenidClientScope

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

return scopes, nil
}

func (keycloakClient *KeycloakClient) AttachOpenidClientDefaultScopes(realmId, clientId string, scopeNames []string) error {
func (keycloakClient *KeycloakClient) GetOpenidClientDefaultScopes(realmId, clientId string) ([]*OpenidClientScope, error) {
return keycloakClient.getOpenidClientScopes(realmId, clientId, "default")
}

func (keycloakClient *KeycloakClient) GetOpenidClientOptionalScopes(realmId, clientId string) ([]*OpenidClientScope, error) {
return keycloakClient.getOpenidClientScopes(realmId, clientId, "optional")
}

func (keycloakClient *KeycloakClient) attachOpenidClientScopes(realmId, clientId, t string, scopeNames []string) error {
openidClient, err := keycloakClient.GetOpenidClient(realmId, clientId)
if err != nil && ErrorIs404(err) {
return fmt.Errorf("validation error: client with id %s does not exist", clientId)
Expand All @@ -149,8 +157,33 @@ func (keycloakClient *KeycloakClient) AttachOpenidClientDefaultScopes(realmId, c
return err
}

var attachedClientScopes []*OpenidClientScope
var duplicateScopeAssignmentErrorMessage string
switch t {
case "optional":
attachedDefaultClientScopes, err := keycloakClient.GetOpenidClientDefaultScopes(realmId, clientId)
if err != nil {
return err
}
attachedClientScopes = append(attachedClientScopes, attachedDefaultClientScopes...)
duplicateScopeAssignmentErrorMessage = "validation error: scope %s is already attached to client as a default scope"
case "default":
attachedOptionalClientScopes, err := keycloakClient.GetOpenidClientOptionalScopes(realmId, clientId)
if err != nil {
return err
}
attachedClientScopes = append(attachedClientScopes, attachedOptionalClientScopes...)
duplicateScopeAssignmentErrorMessage = "validation error: scope %s is already attached to client as an optional scope"
}

for _, openidClientScope := range allOpenidClientScopes {
err := keycloakClient.put(fmt.Sprintf("/realms/%s/clients/%s/default-client-scopes/%s", realmId, clientId, openidClientScope.Id), nil)
for _, attachedClientScope := range attachedClientScopes {
if openidClientScope.Id == attachedClientScope.Id {
return fmt.Errorf(duplicateScopeAssignmentErrorMessage, attachedClientScope.Name)
}
}

err := keycloakClient.put(fmt.Sprintf("/realms/%s/clients/%s/%s-client-scopes/%s", realmId, clientId, t, openidClientScope.Id), nil)
if err != nil {
return err
}
Expand All @@ -159,18 +192,34 @@ func (keycloakClient *KeycloakClient) AttachOpenidClientDefaultScopes(realmId, c
return nil
}

func (keycloakClient *KeycloakClient) DetachOpenidClientDefaultScopes(realmId, clientId string, scopeNames []string) error {
func (keycloakClient *KeycloakClient) AttachOpenidClientDefaultScopes(realmId, clientId string, scopeNames []string) error {
return keycloakClient.attachOpenidClientScopes(realmId, clientId, "default", scopeNames)
}

func (keycloakClient *KeycloakClient) AttachOpenidClientOptionalScopes(realmId, clientId string, scopeNames []string) error {
return keycloakClient.attachOpenidClientScopes(realmId, clientId, "optional", scopeNames)
}

func (keycloakClient *KeycloakClient) detachOpenidClientScopes(realmId, clientId, t string, scopeNames []string) error {
allOpenidClientScopes, err := keycloakClient.listOpenidClientScopesWithFilter(realmId, includeOpenidClientScopesMatchingNames(scopeNames))
if err != nil {
return err
}

for _, openidClientScope := range allOpenidClientScopes {
err := keycloakClient.delete(fmt.Sprintf("/realms/%s/clients/%s/default-client-scopes/%s", realmId, clientId, openidClientScope.Id))
err := keycloakClient.delete(fmt.Sprintf("/realms/%s/clients/%s/%s-client-scopes/%s", realmId, clientId, t, openidClientScope.Id))
if err != nil {
return err
}
}

return nil
}

func (keycloakClient *KeycloakClient) DetachOpenidClientDefaultScopes(realmId, clientId string, scopeNames []string) error {
return keycloakClient.detachOpenidClientScopes(realmId, clientId, "default", scopeNames)
}

func (keycloakClient *KeycloakClient) DetachOpenidClientOptionalScopes(realmId, clientId string, scopeNames []string) error {
return keycloakClient.detachOpenidClientScopes(realmId, clientId, "optional", scopeNames)
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ nav:
- keycloak_openid_client: resources/keycloak_openid_client.md
- keycloak_openid_client_scope: resources/keycloak_openid_client_scope.md
- keycloak_openid_client_default_scopes: resources/keycloak_openid_client_default_scopes.md
- keycloak_openid_client_optional_scopes: resources/keycloak_openid_client_optional_scopes.md
- keycloak_openid_user_attribute_protocol_mapper: resources/keycloak_openid_user_attribute_protocol_mapper.md
- keycloak_openid_user_property_protocol_mapper: resources/keycloak_openid_user_property_protocol_mapper.md
- keycloak_openid_group_membership_protocol_mapper: resources/keycloak_openid_group_membership_protocol_mapper.md
Expand Down
Loading