diff --git a/example/roles.tf b/example/roles.tf index da847d6b9..1056194a9 100644 --- a/example/roles.tf +++ b/example/roles.tf @@ -96,6 +96,13 @@ 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 +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}" +} + // Users and groups resource "keycloak_group" "pet_api_base" { diff --git a/keycloak/generic_client.go b/keycloak/generic_client.go index 6e63028fe..169425359 100644 --- a/keycloak/generic_client.go +++ b/keycloak/generic_client.go @@ -28,6 +28,19 @@ func (keycloakClient *KeycloakClient) listGenericClients(realmId string) ([]*Gen return clients, nil } +func (keycloakClient *KeycloakClient) GetGenericClient(realmId, id string) (*GenericClient, error) { + var client GenericClient + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/clients/%s", realmId, id), &client, nil) + if err != nil { + return nil, err + } + + client.RealmId = realmId + + return &client, nil +} + func (keycloakClient *KeycloakClient) GetGenericClientByClientId(realmId, clientId string) (*GenericClient, error) { var clients []GenericClient diff --git a/keycloak/role_scope_mapping.go b/keycloak/role_scope_mapping.go new file mode 100644 index 000000000..5cfdffae9 --- /dev/null +++ b/keycloak/role_scope_mapping.go @@ -0,0 +1,43 @@ +package keycloak + +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 (keycloakClient *KeycloakClient) CreateRoleScopeMapping(realmId string, clientId string, role *Role) error { + roleUrl := roleScopeMappingUrl(realmId, clientId, role) + + _, _, err := keycloakClient.post(roleUrl, []Role{*role}) + if err != nil { + return err + } + + return nil +} + +func (keycloakClient *KeycloakClient) GetRoleScopeMapping(realmId string, clientId string, role *Role) (*Role, error) { + roleUrl := roleScopeMappingUrl(realmId, clientId, role) + var roles []Role + + err := keycloakClient.get(roleUrl, &roles, nil) + if err != nil { + return nil, err + } + + for _, mappedRole := range roles { + if mappedRole.Id == role.Id { + return role, nil + } + } + + return nil, nil +} + +func (keycloakClient *KeycloakClient) DeleteRoleScopeMapping(realmId string, clientId string, role *Role) error { + roleUrl := roleScopeMappingUrl(realmId, clientId, role) + return keycloakClient.delete(roleUrl, nil) +} diff --git a/provider/provider.go b/provider/provider.go index 6ecc05327..2704cef01 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -47,6 +47,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_openid_client_optional_scopes": resourceKeycloakOpenidClientOptionalScopes(), "keycloak_saml_client": resourceKeycloakSamlClient(), "keycloak_generic_client_protocol_mapper": resourceKeycloakGenericClientProtocolMapper(), + "keycloak_generic_client_role_mapper": resourceKeycloakGenericClientRoleMapper(), "keycloak_saml_user_attribute_protocol_mapper": resourceKeycloakSamlUserAttributeProtocolMapper(), "keycloak_saml_user_property_protocol_mapper": resourceKeycloakSamlUserPropertyProtocolMapper(), "keycloak_hardcoded_attribute_identity_provider_mapper": resourceKeycloakHardcodedAttributeIdentityProviderMapper(), diff --git a/provider/resource_keycloak_generic_client_role_mapper.go b/provider/resource_keycloak_generic_client_role_mapper.go new file mode 100644 index 000000000..f2e104329 --- /dev/null +++ b/provider/resource_keycloak_generic_client_role_mapper.go @@ -0,0 +1,92 @@ +package provider + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func resourceKeycloakGenericClientRoleMapper() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakGenericClientRoleMapperCreate, + Read: resourceKeycloakGenericClientRoleMapperRead, + Delete: resourceKeycloakGenericClientRoleMapperDelete, + + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "client_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceKeycloakGenericClientRoleMapperCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + roleId := data.Get("role_id").(string) + + role, err := keycloakClient.GetRole(realmId, roleId) + if err != nil { + return err + } + + err = keycloakClient.CreateRoleScopeMapping(realmId, clientId, role) + if err != nil { + return err + } + + data.SetId(fmt.Sprintf("%s/client/%s/scope-mappings/%s/%s", realmId, clientId, role.ClientId, role.Id)) + + return resourceKeycloakGenericClientRoleMapperRead(data, meta) +} + +func resourceKeycloakGenericClientRoleMapperRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + roleId := data.Get("role_id").(string) + + role, err := keycloakClient.GetRole(realmId, roleId) + if err != nil { + return err + } + + mappedRole, err := keycloakClient.GetRoleScopeMapping(realmId, clientId, role) + + if mappedRole == nil { + data.SetId("") + } + + return nil +} + +func resourceKeycloakGenericClientRoleMapperDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + roleId := data.Get("role_id").(string) + + role, err := keycloakClient.GetRole(realmId, roleId) + if err != nil { + return err + } + + return keycloakClient.DeleteRoleScopeMapping(realmId, clientId, role) +} diff --git a/provider/resource_keycloak_generic_client_role_mapper_test.go b/provider/resource_keycloak_generic_client_role_mapper_test.go new file mode 100644 index 000000000..5260f3aa8 --- /dev/null +++ b/provider/resource_keycloak_generic_client_role_mapper_test.go @@ -0,0 +1,142 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestGenericRoleMapper_basic(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + parentClientName := "client1-" + acctest.RandString(10) + parentRoleName := "role-" + acctest.RandString(10) + childClientName := "client2-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakGenericRoleMapping_basic(realmName, parentClientName, parentRoleName, childClientName), + Check: testAccCheckKeycloakScopeMappingExists("keycloak_generic_client_role_mapper.child-client-with-parent-client-role"), + }, + }, + }) +} + +func TestGenericRoleMapper_createAfterManualDestroy(t *testing.T) { + var role = &keycloak.Role{} + var childClient = &keycloak.GenericClient{} + + realmName := "terraform-" + acctest.RandString(10) + parentClientName := "client1-" + acctest.RandString(10) + parentRoleName := "role-" + acctest.RandString(10) + childClientName := "client2-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakGenericRoleMapping_basic(realmName, parentClientName, parentRoleName, childClientName), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakScopeMappingExists("keycloak_generic_client_role_mapper.child-client-with-parent-client-role"), + testAccCheckKeycloakRoleFetch("keycloak_role.parent-role", role), + testAccCheckKeycloakGenericClientFetch("keycloak_openid_client.child-client", childClient), + ), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteRoleScopeMapping(childClient.RealmId, childClient.Id, role) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakGenericRoleMapping_basic(realmName, parentClientName, parentRoleName, childClientName), + Check: testAccCheckKeycloakScopeMappingExists("keycloak_generic_client_role_mapper.child-client-with-parent-client-role"), + }, + }, + }) +} + +func testKeycloakGenericRoleMapping_basic(realmName, parentClientName, parentRoleName, childClientName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "parent-client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + access_type = "PUBLIC" +} + +resource "keycloak_role" "parent-role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.parent-client.id}" + name = "%s" +} + +resource "keycloak_openid_client" "child-client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + access_type = "PUBLIC" +} + +resource "keycloak_generic_client_role_mapper" "child-client-with-parent-client-role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.child-client.id}" + role_id = "${keycloak_role.parent-role.id}" +} + `, realmName, parentClientName, parentRoleName, childClientName) +} + +func testAccCheckKeycloakScopeMappingExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + return nil + } +} + +func testAccCheckKeycloakGenericClientFetch(resourceName string, client *keycloak.GenericClient) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedClient, err := getGenericClientFromState(s, resourceName) + if err != nil { + return err + } + + client.Id = fetchedClient.Id + client.ClientId = fetchedClient.ClientId + client.RealmId = fetchedClient.RealmId + + return nil + } +} + +func getGenericClientFromState(s *terraform.State, resourceName string) (*keycloak.GenericClient, 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.GetGenericClient(realm, id) + if err != nil { + return nil, fmt.Errorf("error getting generic client %s: %s", id, err) + } + + return client, nil +} diff --git a/provider/resource_keycloak_role_test.go b/provider/resource_keycloak_role_test.go index 1940af591..ef44144f4 100644 --- a/provider/resource_keycloak_role_test.go +++ b/provider/resource_keycloak_role_test.go @@ -308,6 +308,7 @@ func testAccCheckKeycloakRoleFetch(resourceName string, role *keycloak.Role) res role.Id = fetchedRole.Id role.Name = fetchedRole.Name role.RealmId = fetchedRole.RealmId + role.ClientId = fetchedRole.ClientId return nil }