diff --git a/example/roles.tf b/example/roles.tf index 1056194a..da229576 100644 --- a/example/roles.tf +++ b/example/roles.tf @@ -15,6 +15,13 @@ resource "keycloak_openid_client" "pet_api" { access_type = "BEARER-ONLY" } +// Optional client scope for mapping additional client role +resource "keycloak_openid_client_scope" "extended_pet_details" { + realm_id = "${keycloak_realm.roles_example.id}" + name = "extended-pet-details" + description = "Optional scope offering additional information when getting pets" +} + resource "keycloak_role" "pet_api_create_pet" { name = "create-pet" realm_id = "${keycloak_realm.roles_example.id}" @@ -43,6 +50,20 @@ resource "keycloak_role" "pet_api_delete_pet" { description = "Ability to delete a pet" } +resource "keycloak_role" "pet_api_read_pet_details" { + name = "read-pet-with-details" + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_api.id}" + description = "Ability to read / list pets with further details" +} + +// Map a role from the "pet_api" api client to the "extended_pet_details" client scope +resource "keycloak_generic_client_role_mapper" "pet_api_read_pet_details_role_mapping" { + realm_id = "${keycloak_realm.roles_example.id}" + client_scope_id = "${keycloak_openid_client_scope.extended_pet_details.id}" + role_id = "${keycloak_role.pet_api_read_pet_details.id}" +} + resource "keycloak_role" "pet_api_admin" { name = "admin" realm_id = "${keycloak_realm.roles_example.id}" @@ -76,6 +97,18 @@ resource "keycloak_openid_client" "pet_app" { valid_redirect_uris = [ "http://localhost:5555/openid-callback", ] + + // disable full scope, roles are assigned via keycloak_generic_client_role_mapper + full_scope_allowed = false +} + +resource "keycloak_openid_client_optional_scopes" "pet_app_optional_scopes" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_app.id}" + + optional_scopes = [ + "${keycloak_openid_client_scope.extended_pet_details.name}" + ] } // The app will always need access to the API, so this audience should be used regardless of auth type @@ -96,13 +129,37 @@ resource "keycloak_openid_hardcoded_role_protocol_mapper" "pet_app_pet_api_read_ role_id = "${keycloak_role.pet_api_read_pet.id}" } -// Map a role from the "pet_api" api client to the "pet_app" consumer client +// Map all roles from the "pet_api" api client to the "pet_app" consumer client, read_pet_details comes via client scope resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_read_role_mapping" { realm_id = "${keycloak_realm.roles_example.id}" client_id = "${keycloak_openid_client.pet_app.id}" role_id = "${keycloak_role.pet_api_read_pet.id}" } +resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_delete_role_mapping" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_app.id}" + role_id = "${keycloak_role.pet_api_delete_pet.id}" +} + +resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_create_role_mapping" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_app.id}" + role_id = "${keycloak_role.pet_api_create_pet.id}" +} + +resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_update_role_mapping" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_app.id}" + role_id = "${keycloak_role.pet_api_update_pet.id}" +} + +resource "keycloak_generic_client_role_mapper" "pet_app_pet_api_admin_role_mapping" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_app.id}" + role_id = "${keycloak_role.pet_api_admin.id}" +} + // Users and groups resource "keycloak_group" "pet_api_base" { @@ -133,6 +190,7 @@ resource "keycloak_group_roles" "admin_roles" { role_ids = [ "${keycloak_role.pet_api_read_pet.id}", + "${keycloak_role.pet_api_read_pet_details.id}", "${keycloak_role.pet_api_delete_pet.id}", "${keycloak_role.pet_api_create_pet.id}", "${data.keycloak_role.realm_offline_access.id}", @@ -145,6 +203,7 @@ resource "keycloak_group_roles" "front_desk_roles" { role_ids = [ "${keycloak_role.pet_api_read_pet.id}", + "${keycloak_role.pet_api_read_pet_details.id}", "${keycloak_role.pet_api_create_pet.id}", "${data.keycloak_role.realm_offline_access.id}", ] diff --git a/keycloak/role_scope_mapping.go b/keycloak/role_scope_mapping.go index 5cfdffae..4aabd4f3 100644 --- a/keycloak/role_scope_mapping.go +++ b/keycloak/role_scope_mapping.go @@ -4,12 +4,16 @@ import ( "fmt" ) -func roleScopeMappingUrl(realmId, clientId string, role *Role) string { - return fmt.Sprintf("/realms/%s/clients/%s/scope-mappings/clients/%s", realmId, clientId, role.ClientId) +func roleScopeMappingUrl(realmId, clientId string, clientScopeId string, role *Role) string { + if clientId != "" { + return fmt.Sprintf("/realms/%s/clients/%s/scope-mappings/clients/%s", realmId, clientId, role.ClientId) + } else { + return fmt.Sprintf("/realms/%s/client-scopes/%s/scope-mappings/clients/%s", realmId, clientScopeId, role.ClientId) + } } -func (keycloakClient *KeycloakClient) CreateRoleScopeMapping(realmId string, clientId string, role *Role) error { - roleUrl := roleScopeMappingUrl(realmId, clientId, role) +func (keycloakClient *KeycloakClient) CreateRoleScopeMapping(realmId string, clientId string, clientScopeId string, role *Role) error { + roleUrl := roleScopeMappingUrl(realmId, clientId, clientScopeId, role) _, _, err := keycloakClient.post(roleUrl, []Role{*role}) if err != nil { @@ -19,8 +23,8 @@ func (keycloakClient *KeycloakClient) CreateRoleScopeMapping(realmId string, cli return nil } -func (keycloakClient *KeycloakClient) GetRoleScopeMapping(realmId string, clientId string, role *Role) (*Role, error) { - roleUrl := roleScopeMappingUrl(realmId, clientId, role) +func (keycloakClient *KeycloakClient) GetRoleScopeMapping(realmId string, clientId string, clientScopeId string, role *Role) (*Role, error) { + roleUrl := roleScopeMappingUrl(realmId, clientId, clientScopeId, role) var roles []Role err := keycloakClient.get(roleUrl, &roles, nil) @@ -37,7 +41,7 @@ func (keycloakClient *KeycloakClient) GetRoleScopeMapping(realmId string, client return nil, nil } -func (keycloakClient *KeycloakClient) DeleteRoleScopeMapping(realmId string, clientId string, role *Role) error { - roleUrl := roleScopeMappingUrl(realmId, clientId, role) +func (keycloakClient *KeycloakClient) DeleteRoleScopeMapping(realmId string, clientId string, clientScopeId string, role *Role) error { + roleUrl := roleScopeMappingUrl(realmId, clientId, clientScopeId, role) return keycloakClient.delete(roleUrl, nil) } diff --git a/provider/resource_keycloak_generic_client_role_mapper.go b/provider/resource_keycloak_generic_client_role_mapper.go index f2e10432..9b39c879 100644 --- a/provider/resource_keycloak_generic_client_role_mapper.go +++ b/provider/resource_keycloak_generic_client_role_mapper.go @@ -15,19 +15,30 @@ func resourceKeycloakGenericClientRoleMapper() *schema.Resource { Schema: map[string]*schema.Schema{ "realm_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The realm id where the associated client or client scope exists.", }, "client_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The destination client of the client role. Cannot be used at the same time as client_scope_id.", + ConflictsWith: []string{"client_scope_id"}, + }, + "client_scope_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The destination client scope of the client role. Cannot be used at the same time as client_id.", + ConflictsWith: []string{"client_id"}, }, "role_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Id of the role to assign", }, }, } @@ -38,6 +49,7 @@ func resourceKeycloakGenericClientRoleMapperCreate(data *schema.ResourceData, me realmId := data.Get("realm_id").(string) clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) roleId := data.Get("role_id").(string) role, err := keycloakClient.GetRole(realmId, roleId) @@ -45,12 +57,16 @@ func resourceKeycloakGenericClientRoleMapperCreate(data *schema.ResourceData, me return err } - err = keycloakClient.CreateRoleScopeMapping(realmId, clientId, role) + err = keycloakClient.CreateRoleScopeMapping(realmId, clientId, clientScopeId, role) if err != nil { return err } - data.SetId(fmt.Sprintf("%s/client/%s/scope-mappings/%s/%s", realmId, clientId, role.ClientId, role.Id)) + if clientId != "" { + data.SetId(fmt.Sprintf("%s/client/%s/scope-mappings/%s/%s", realmId, clientId, role.ClientId, role.Id)) + } else { + data.SetId(fmt.Sprintf("%s/client-scope/%s/scope-mappings/%s/%s", realmId, clientScopeId, role.ClientId, role.Id)) + } return resourceKeycloakGenericClientRoleMapperRead(data, meta) } @@ -60,6 +76,7 @@ func resourceKeycloakGenericClientRoleMapperRead(data *schema.ResourceData, meta realmId := data.Get("realm_id").(string) clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) roleId := data.Get("role_id").(string) role, err := keycloakClient.GetRole(realmId, roleId) @@ -67,7 +84,7 @@ func resourceKeycloakGenericClientRoleMapperRead(data *schema.ResourceData, meta return err } - mappedRole, err := keycloakClient.GetRoleScopeMapping(realmId, clientId, role) + mappedRole, err := keycloakClient.GetRoleScopeMapping(realmId, clientId, clientScopeId, role) if mappedRole == nil { data.SetId("") @@ -81,6 +98,7 @@ func resourceKeycloakGenericClientRoleMapperDelete(data *schema.ResourceData, me realmId := data.Get("realm_id").(string) clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) roleId := data.Get("role_id").(string) role, err := keycloakClient.GetRole(realmId, roleId) @@ -88,5 +106,5 @@ func resourceKeycloakGenericClientRoleMapperDelete(data *schema.ResourceData, me return err } - return keycloakClient.DeleteRoleScopeMapping(realmId, clientId, role) + return keycloakClient.DeleteRoleScopeMapping(realmId, clientId, clientScopeId, role) } diff --git a/provider/resource_keycloak_generic_client_role_mapper_test.go b/provider/resource_keycloak_generic_client_role_mapper_test.go index 5260f3aa..7983f5cc 100644 --- a/provider/resource_keycloak_generic_client_role_mapper_test.go +++ b/provider/resource_keycloak_generic_client_role_mapper_test.go @@ -27,6 +27,24 @@ func TestGenericRoleMapper_basic(t *testing.T) { }) } +func TestGenericRoleMapperClientScope_basic(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + clientName := "client-" + acctest.RandString(10) + roleName := "role-" + acctest.RandString(10) + clientScopeName := "clientscope-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakGenericRoleMappingClientScope_basic(realmName, clientName, roleName, clientScopeName), + Check: testAccCheckKeycloakScopeMappingExists("keycloak_generic_client_role_mapper.clientscope-with-client-role"), + }, + }, + }) +} + func TestGenericRoleMapper_createAfterManualDestroy(t *testing.T) { var role = &keycloak.Role{} var childClient = &keycloak.GenericClient{} @@ -52,7 +70,7 @@ func TestGenericRoleMapper_createAfterManualDestroy(t *testing.T) { PreConfig: func() { keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) - err := keycloakClient.DeleteRoleScopeMapping(childClient.RealmId, childClient.Id, role) + err := keycloakClient.DeleteRoleScopeMapping(childClient.RealmId, childClient.Id, "", role) if err != nil { t.Fatal(err) } @@ -64,6 +82,43 @@ func TestGenericRoleMapper_createAfterManualDestroy(t *testing.T) { }) } +func TestGenericRoleMapperClientScope_createAfterManualDestroy(t *testing.T) { + var role = &keycloak.Role{} + var clientScope = &keycloak.OpenidClientScope{} + + realmName := "terraform-" + acctest.RandString(10) + clientName := "client-" + acctest.RandString(10) + roleName := "role-" + acctest.RandString(10) + clientScopeName := "clientscope-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakGenericRoleMappingClientScope_basic(realmName, clientName, roleName, clientScopeName), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakScopeMappingExists("keycloak_generic_client_role_mapper.clientscope-with-client-role"), + testAccCheckKeycloakRoleFetch("keycloak_role.role", role), + testAccCheckKeycloakOpenidClientScopeFetch("keycloak_openid_client_scope.clientscope", clientScope), + ), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteRoleScopeMapping(clientScope.RealmId, "", clientScope.Id, role) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakGenericRoleMappingClientScope_basic(realmName, clientName, roleName, clientScopeName), + Check: testAccCheckKeycloakScopeMappingExists("keycloak_generic_client_role_mapper.clientscope-with-client-role"), + }, + }, + }) +} + func testKeycloakGenericRoleMapping_basic(realmName, parentClientName, parentRoleName, childClientName string) string { return fmt.Sprintf(` resource "keycloak_realm" "realm" { @@ -96,6 +151,37 @@ resource "keycloak_generic_client_role_mapper" "child-client-with-parent-client- `, realmName, parentClientName, parentRoleName, childClientName) } +func testKeycloakGenericRoleMappingClientScope_basic(realmName, clientName, roleName, clientScopeName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + access_type = "PUBLIC" +} + +resource "keycloak_role" "role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.client.id}" + name = "%s" +} + +resource "keycloak_openid_client_scope" "clientscope" { + realm_id = "${keycloak_realm.realm.id}" + name = "%s" +} + +resource "keycloak_generic_client_role_mapper" "clientscope-with-client-role" { + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.clientscope.id}" + role_id = "${keycloak_role.role.id}" +} + `, realmName, clientName, roleName, clientScopeName) +} + func testAccCheckKeycloakScopeMappingExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { _, ok := s.RootModule().Resources[resourceName] @@ -122,6 +208,20 @@ func testAccCheckKeycloakGenericClientFetch(resourceName string, client *keycloa } } +func testAccCheckKeycloakOpenidClientScopeFetch(resourceName string, clientScope *keycloak.OpenidClientScope) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedClientScope, err := getOpenidClientScopeFromState(s, resourceName) + if err != nil { + return err + } + + clientScope.Id = fetchedClientScope.Id + clientScope.RealmId = fetchedClientScope.RealmId + + return nil + } +} + func getGenericClientFromState(s *terraform.State, resourceName string) (*keycloak.GenericClient, error) { keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) @@ -140,3 +240,22 @@ func getGenericClientFromState(s *terraform.State, resourceName string) (*keyclo return client, nil } + +func getOpenidClientScopeFromState(s *terraform.State, resourceName string) (*keycloak.OpenidClientScope, error) { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found: %s", resourceName) + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + client, err := keycloakClient.GetOpenidClientScope(realm, id) + if err != nil { + return nil, fmt.Errorf("error getting client scope %s: %s", id, err) + } + + return client, nil +}