From a813fd3710507ae09f122fedf47bccc5a4234c8a Mon Sep 17 00:00:00 2001 From: Zadkiel AHARONIAN Date: Fri, 28 Jan 2022 12:01:41 +0100 Subject: [PATCH] feat: add fine-grained realm-wide client scope management Signed-off-by: GitHub --- docs/resources/openid_default_client_scope.md | 33 +++++++ .../resources/openid_optional_client_scope.md | 33 +++++++ keycloak/openid_default_client_scope.go | 31 +++++++ keycloak/openid_optional_client_scope.go | 31 +++++++ provider/provider.go | 2 + .../resource_keycloak_openid_client_scope.go | 5 +- ...ce_keycloak_openid_default_client_scope.go | 81 +++++++++++++++++ ...ycloak_openid_default_client_scope_test.go | 90 +++++++++++++++++++ ...e_keycloak_openid_optional_client_scope.go | 81 +++++++++++++++++ 9 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 docs/resources/openid_default_client_scope.md create mode 100644 docs/resources/openid_optional_client_scope.md create mode 100644 keycloak/openid_default_client_scope.go create mode 100644 keycloak/openid_optional_client_scope.go create mode 100644 provider/resource_keycloak_openid_default_client_scope.go create mode 100644 provider/resource_keycloak_openid_default_client_scope_test.go create mode 100644 provider/resource_keycloak_openid_optional_client_scope.go diff --git a/docs/resources/openid_default_client_scope.md b/docs/resources/openid_default_client_scope.md new file mode 100644 index 000000000..e25e5c35d --- /dev/null +++ b/docs/resources/openid_default_client_scope.md @@ -0,0 +1,33 @@ +--- +page_title: "keycloak_openid_default_client_scope Resource" +--- + +# keycloak\_openid\_default\_client\_scope Resource + +Allows for creating or removing Keycloak default client scopes from Realm that use the OpenID Connect protocol. + +A default client scope will be assigned automatically to each new client. + +## Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_openid_client_scope" "openid_client_scope" { + realm_id = keycloak_realm.realm.id + name = "groups" +} + +resource "keycloak_openid_default_client_scope" "openid_default_client_scope" { + realm_id = keycloak_realm.realm.id + client_scope_id = keycloak_openid_client_scope.client_scope.id +} +``` + +## Argument Reference + +- `realm_id` - (Required) The realm this client scope belongs to. +- `client_scope_id` - (Required) The client scope to manage. diff --git a/docs/resources/openid_optional_client_scope.md b/docs/resources/openid_optional_client_scope.md new file mode 100644 index 000000000..407ea29ee --- /dev/null +++ b/docs/resources/openid_optional_client_scope.md @@ -0,0 +1,33 @@ +--- +page_title: "keycloak_openid_optional_client_scope Resource" +--- + +# keycloak\_openid\_optional\_client\_scope Resource + +Allows for creating or removing Keycloak optional client scopes from Realm that use the OpenID Connect protocol. + +A optional client scope will be assigned automatically to each new client. + +## Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_openid_client_scope" "openid_client_scope" { + realm_id = keycloak_realm.realm.id + name = "groups" +} + +resource "keycloak_openid_optional_client_scope" "openid_optional_client_scope" { + realm_id = keycloak_realm.realm.id + client_scope_id = keycloak_openid_client_scope.client_scope.id +} +``` + +## Argument Reference + +- `realm_id` - (Required) The realm this client scope belongs to. +- `client_scope_id` - (Required) The client scope to manage. diff --git a/keycloak/openid_default_client_scope.go b/keycloak/openid_default_client_scope.go new file mode 100644 index 000000000..956b92eb4 --- /dev/null +++ b/keycloak/openid_default_client_scope.go @@ -0,0 +1,31 @@ +package keycloak + +import ( + "context" + "fmt" +) + +func (keycloakClient *KeycloakClient) GetOpenidRealmDefaultClientScope(ctx context.Context, realmId, clientScopeId string) (*OpenidClientScope, error) { + var clientScopes []OpenidClientScope + + err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/default-default-client-scopes", realmId), &clientScopes, nil) + if err != nil { + return nil, err + } + + for _, clientScope := range clientScopes { + if clientScope.Id == clientScopeId { + return &clientScope, nil + } + } + + return nil, err +} + +func (keycloakClient *KeycloakClient) PutOpenidRealmDefaultClientScope(ctx context.Context, realmId, clientScopeId string) error { + return keycloakClient.put(ctx, fmt.Sprintf("/realms/%s/default-default-client-scopes/%s", realmId, clientScopeId), nil) +} + +func (keycloakClient *KeycloakClient) DeleteOpenidRealmDefaultClientScope(ctx context.Context, realmId, clientScopeId string) error { + return keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/default-default-client-scopes/%s", realmId, clientScopeId), nil) +} diff --git a/keycloak/openid_optional_client_scope.go b/keycloak/openid_optional_client_scope.go new file mode 100644 index 000000000..07786b433 --- /dev/null +++ b/keycloak/openid_optional_client_scope.go @@ -0,0 +1,31 @@ +package keycloak + +import ( + "context" + "fmt" +) + +func (keycloakClient *KeycloakClient) GetOpenidRealmOptionalClientScope(ctx context.Context, realmId, clientScopeId string) (*OpenidClientScope, error) { + var clientScopes []OpenidClientScope + + err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/default-optional-client-scopes", realmId), &clientScopes, nil) + if err != nil { + return nil, err + } + + for _, clientScope := range clientScopes { + if clientScope.Id == clientScopeId { + return &clientScope, nil + } + } + + return nil, err +} + +func (keycloakClient *KeycloakClient) PutOpenidRealmOptionalClientScope(ctx context.Context, realmId, clientScopeId string) error { + return keycloakClient.put(ctx, fmt.Sprintf("/realms/%s/default-optional-client-scopes/%s", realmId, clientScopeId), nil) +} + +func (keycloakClient *KeycloakClient) DeleteOpenidRealmOptionalClientScope(ctx context.Context, realmId, clientScopeId string) error { + return keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/default-optional-client-scopes/%s", realmId, clientScopeId), nil) +} diff --git a/provider/provider.go b/provider/provider.go index a30091284..01656580d 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -49,6 +49,8 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { "keycloak_user_roles": resourceKeycloakUserRoles(), "keycloak_openid_client": resourceKeycloakOpenidClient(), "keycloak_openid_client_scope": resourceKeycloakOpenidClientScope(), + "keycloak_openid_default_client_scope": resourceKeycloakOpenidDefaultClientScope(), + "keycloak_openid_optional_client_scope": resourceKeycloakOpenidOptionalClientScope(), "keycloak_ldap_user_federation": resourceKeycloakLdapUserFederation(), "keycloak_ldap_user_attribute_mapper": resourceKeycloakLdapUserAttributeMapper(), "keycloak_ldap_group_mapper": resourceKeycloakLdapGroupMapper(), diff --git a/provider/resource_keycloak_openid_client_scope.go b/provider/resource_keycloak_openid_client_scope.go index 77e626a92..846765854 100644 --- a/provider/resource_keycloak_openid_client_scope.go +++ b/provider/resource_keycloak_openid_client_scope.go @@ -3,11 +3,12 @@ package provider import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/keycloak/terraform-provider-keycloak/keycloak/types" "strconv" "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/keycloak/terraform-provider-keycloak/keycloak/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/keycloak/terraform-provider-keycloak/keycloak" ) diff --git a/provider/resource_keycloak_openid_default_client_scope.go b/provider/resource_keycloak_openid_default_client_scope.go new file mode 100644 index 000000000..a8c33820e --- /dev/null +++ b/provider/resource_keycloak_openid_default_client_scope.go @@ -0,0 +1,81 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "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 resourceKeycloakOpenidDefaultClientScope() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKeycloakOpenidDefaultClientScopeCreate, + ReadContext: resourceKeycloakOpenidDefaultClientScopesRead, + DeleteContext: resourceKeycloakOpenidDefaultClientScopeDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceKeycloakOpenidDefaultClientScopeImport, + }, + + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "client_scope_id": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceKeycloakOpenidDefaultClientScopeCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + return diag.FromErr(keycloakClient.PutOpenidRealmDefaultClientScope(ctx, realmId, clientScopeId)) +} + +func resourceKeycloakOpenidDefaultClientScopesRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + clientScope, err := keycloakClient.GetOpenidRealmDefaultClientScope(ctx, realmId, clientScopeId) + if err != nil { + return handleNotFoundError(ctx, err, data) + } + + data.Set("client_scope_id", clientScope.Id) + data.Set("client_scope_name", clientScope.Name) + + return nil +} + +func resourceKeycloakOpenidDefaultClientScopeDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + return diag.FromErr(keycloakClient.DeleteOpenidRealmDefaultClientScope(ctx, realmId, clientScopeId)) +} + +func resourceKeycloakOpenidDefaultClientScopeImport(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{openidClientScopeId}}") + } + + d.Set("realm_id", parts[0]) + d.SetId(parts[1]) + + return []*schema.ResourceData{d}, nil +} diff --git a/provider/resource_keycloak_openid_default_client_scope_test.go b/provider/resource_keycloak_openid_default_client_scope_test.go new file mode 100644 index 000000000..3744ca1bb --- /dev/null +++ b/provider/resource_keycloak_openid_default_client_scope_test.go @@ -0,0 +1,90 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "strings" + "testing" +) + +func TestAccKeycloakDataSourceOpenidDefaultClientScope_basic(t *testing.T) { + t.Parallel() + clientId := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testAccKeycloakOpenidDefaultClientScope_basic(clientId), + Check: testAccCheckKeycloakOpenidClientHasDefaultScope("keycloak_openid_default_client_scope"), + }, + }, + }) +} + +func testAccCheckKeycloakOpenidClientHasDefaultScope(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + realm := rs.Primary.Attributes["realm_id"] + clientScopeId := rs.Primary.Attributes["client_scope_id"] + + var client string + if strings.HasPrefix(resourceName, "keycloak_openid_client") { + client = rs.Primary.Attributes["client_id"] + } else { + client = rs.Primary.ID + } + + keycloakDefaultClientScopes, err := keycloakClient.GetOpenidClientDefaultScopes(realm, client) + + if err != nil { + return err + } + + var found = false + for _, keycloakDefaultScope := range keycloakDefaultClientScopes { + if keycloakDefaultScope.Id == clientScopeId { + found = true + + break + } + } + + if !found { + return fmt.Errorf("default scope %s is not assigned to client", clientScopeId) + } + + return nil + } +} + +func testAccKeycloakOpenidDefaultClientScope_basic(clientId string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" + enabled = true +} + +resource "keycloak_openid_client_scope" "openid_client_scope" { + realm_id = keycloak_realm.realm.id + name = "groups" +} + +resource "keycloak_openid_default_client_scope" "openid_default_client_scope" { + realm_id = keycloak_realm.realm.id + client_scope_id = keycloak_openid_client_scope.client_scope.id +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = keycloak_realm.realm.id + client_id = "%s" +} +`, testAccRealm.Realm, clientId) +} diff --git a/provider/resource_keycloak_openid_optional_client_scope.go b/provider/resource_keycloak_openid_optional_client_scope.go new file mode 100644 index 000000000..62c9385b7 --- /dev/null +++ b/provider/resource_keycloak_openid_optional_client_scope.go @@ -0,0 +1,81 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "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 resourceKeycloakOpenidOptionalClientScope() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKeycloakOpenidOptionalClientScopeCreate, + ReadContext: resourceKeycloakOpenidOptionalClientScopesRead, + DeleteContext: resourceKeycloakOpenidOptionalClientScopeDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceKeycloakOpenidOptionalClientScopeImport, + }, + + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "client_scope_id": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func resourceKeycloakOpenidOptionalClientScopeCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + return diag.FromErr(keycloakClient.PutOpenidRealmOptionalClientScope(ctx, realmId, clientScopeId)) +} + +func resourceKeycloakOpenidOptionalClientScopesRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + clientScope, err := keycloakClient.GetOpenidRealmOptionalClientScope(ctx, realmId, clientScopeId) + if err != nil { + return handleNotFoundError(ctx, err, data) + } + + data.Set("client_scope_id", clientScope.Id) + data.Set("client_scope_name", clientScope.Name) + + return nil +} + +func resourceKeycloakOpenidOptionalClientScopeDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + return diag.FromErr(keycloakClient.DeleteOpenidRealmOptionalClientScope(ctx, realmId, clientScopeId)) +} + +func resourceKeycloakOpenidOptionalClientScopeImport(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{openidClientScopeId}}") + } + + d.Set("realm_id", parts[0]) + d.SetId(parts[1]) + + return []*schema.ResourceData{d}, nil +}