diff --git a/.editorconfig b/.editorconfig index f9b68ea77..8edf18ff1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,6 @@ charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.yml] +[{*.yml, *.tf}] indent_style = space indent_size = 2 diff --git a/docs/resources/datasource_keycloak_realm_keys.md b/docs/data_sources/keycloak_realm_keys.md similarity index 96% rename from docs/resources/datasource_keycloak_realm_keys.md rename to docs/data_sources/keycloak_realm_keys.md index dcb482c3c..50e41b329 100644 --- a/docs/resources/datasource_keycloak_realm_keys.md +++ b/docs/data_sources/keycloak_realm_keys.md @@ -1,4 +1,4 @@ -# datasource keycloak_realm_keys +# keycloak_realm_keys data source Use this data source to get the keys of a realm. Keys can be filtered by algorithm and status. diff --git a/docs/data_sources/keycloak_role.md b/docs/data_sources/keycloak_role.md new file mode 100644 index 000000000..7dfc4f945 --- /dev/null +++ b/docs/data_sources/keycloak_role.md @@ -0,0 +1,51 @@ +# keycloak_role data source + +This data source can be used to fetch properties of a Keycloak role for +usage with other resources, such as `keycloak_group_roles`. + +### Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +data "keycloak_role" "offline_access" { + realm_id = "${keycloak_realm.realm.id}" + name = "offline_access" +} + +# use the data source + +resource "keycloak_group" "group" { + realm_id = "${keycloak_realm.realm.id}" + name = "group" +} + +resource "keycloak_group_roles" "group_roles" { + realm_id = "${keycloak_realm.realm.id}" + group_id = "${keycloak_group.group.id}" + + roles = [ + "${data.keycloak_role.offline_access.id}" + ] +} +``` + +### Argument Reference + +The following arguments are supported: + +- `realm_id` - (Required) The realm this role exists within. +- `client_id` - (Optional) When specified, this role is assumed to be a + client role belonging to the client with the provided ID +- `name` - (Required) The name of the role + +### Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +- `id` - The unique ID of the role, which can be used as an argument to + other resources supported by this provider. +- `description` - The description of the role. diff --git a/docs/resources/keycloak_group_roles.md b/docs/resources/keycloak_group_roles.md new file mode 100644 index 000000000..fb0bff27e --- /dev/null +++ b/docs/resources/keycloak_group_roles.md @@ -0,0 +1,83 @@ +# keycloak_group_roles + +Allows you to manage roles assigned to a Keycloak group. + +Note that this resource attempts to be an **authoritative** source over +group roles. When this resource takes control over a group's roles, +roles that are manually added to the group will be removed, and roles +that are manually removed from the group will be added upon the next run +of `terraform apply`. + +Note that when assigning composite roles to a group, you may see a +non-empty plan following a `terraform apply` if you assign a role and a +composite that includes that role to the same group. + +### Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_role" "realm_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "my-realm-role" + description = "My Realm Role" +} + +resource "keycloak_openid_client" "client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "client" + name = "client" + + enabled = true + + access_type = "BEARER-ONLY" +} + +resource "keycloak_role" "client_role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_client.client.id}" + name = "my-client-role" + description = "My Client Role" +} + +resource "keycloak_group" "group" { + realm_id = "${keycloak_realm.realm.id}" + name = "my-group" +} + +resource "keycloak_group_roles" "group_roles" { + realm_id = "${keycloak_realm.realm.id}" + group_id = "${keycloak_group.group.id}" + + roles = [ + "${keycloak_role.realm_role.id}", + "${keycloak_role.client_role.id}", + ] +} +``` + +### Argument Reference + +The following arguments are supported: + +- `realm_id` - (Required) The realm this group exists in. +- `group_id` - (Required) The ID of the group this resource should + manage roles for. +- `roles` - (Required) A list of role IDs to map to the group + +### Import + +This resource can be imported using the format +`{{realm_id}}/{{group_id}}`, where `group_id` is the unique ID that +Keycloak assigns to the group upon creation. This value can be found in +the URI when editing this group in the GUI, and is typically a GUID. + +Example: + +```bash +$ terraform import keycloak_group_roles.group_roles my-realm/18cc6b87-2ce7-4e59-bdc8-b9d49ec98a94 +``` + diff --git a/docs/resources/keycloak_openid_hardcoded_role_protocol_mapper.md b/docs/resources/keycloak_openid_hardcoded_role_protocol_mapper.md new file mode 100644 index 000000000..c13a07c07 --- /dev/null +++ b/docs/resources/keycloak_openid_hardcoded_role_protocol_mapper.md @@ -0,0 +1,93 @@ +# keycloak_openid_hardcoded_role_protocol_mapper + +Allows for creating and managing hardcoded role protocol mappers within +Keycloak. + +Hardcoded role protocol mappers allow you to specify a single role to +always map to an access token for a client. Protocol mappers can be +defined for a single client, or they can be defined for a client scope +which can be shared between multiple different clients. + +### Example Usage (Client) + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_role" "role" { + realm_id = "${keycloak_realm.realm.id}" + name = "my-role" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "test-client" + + name = "test client" + enabled = true + + access_type = "CONFIDENTIAL" + valid_redirect_uris = [ + "http://localhost:8080/openid-callback" + ] +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + name = "hardcoded-role-mapper" + role_id = "${keycloak_role.role.id}" +} +``` + +### Example Usage (Client Scope) + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_role" "role" { + realm_id = "${keycloak_realm.realm.id}" + name = "my-role" +} + +resource "keycloak_openid_client_scope" "client_scope" { + realm_id = "${keycloak_realm.realm.id}" + name = "test-client-scope" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper" { + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + name = "hardcoded-role-mapper" + role_id = "${keycloak_role.role.id}" +} +``` + +### Argument Reference + +The following arguments are supported: + +- `realm_id` - (Required) The realm this protocol mapper exists within. +- `client_id` - (Required if `client_scope_id` is not specified) The client this protocol mapper is attached to. +- `client_scope_id` - (Required if `client_id` is not specified) The client scope this protocol mapper is attached to. +- `name` - (Required) The display name of this protocol mapper in the + GUI. +- `role_id` - (Required) The ID of the role to map to an access token. + +### Import + +Protocol mappers can be imported using one of the following formats: +- Client: `{{realm_id}}/client/{{client_keycloak_id}}/{{protocol_mapper_id}}` +- Client Scope: `{{realm_id}}/client-scope/{{client_scope_keycloak_id}}/{{protocol_mapper_id}}` + +Example: + +```bash +$ terraform import keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper my-realm/client/a7202154-8793-4656-b655-1dd18c181e14/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +$ terraform import keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper my-realm/client-scope/b799ea7e-73ee-4a73-990a-1eafebe8e20a/71602afa-f7d1-4788-8c49-ef8fd00af0f4 +``` diff --git a/docs/resources/keycloak_role.md b/docs/resources/keycloak_role.md new file mode 100644 index 000000000..a9f7eaae8 --- /dev/null +++ b/docs/resources/keycloak_role.md @@ -0,0 +1,136 @@ +# keycloak_role + +Allows for creating and managing roles within Keycloak. + +Roles allow you define privileges within Keycloak and map them to users +and groups. + +### Example Usage (Realm role) + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_role" "realm_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "my-realm-role" + description = "My Realm Role" +} +``` + +### Example Usage (Client role) + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_openid_client" "client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "client" + name = "client" + + enabled = true + + access_type = "BEARER-ONLY" +} + +resource "keycloak_role" "client_role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_client.client.id}" + name = "my-client-role" + description = "My Client Role" +} +``` + +### Example Usage (Composite role) + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +# realm roles + +resource "keycloak_role" "create_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "create" +} + +resource "keycloak_role" "read_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "read" +} + +resource "keycloak_role" "update_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "update" +} + +resource "keycloak_role" "delete_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "delete" +} + +# client role + +resource "keycloak_openid_client" "client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "client" + name = "client" + + enabled = true + + access_type = "BEARER-ONLY" +} + +resource "keycloak_role" "client_role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_client.client.id}" + name = "my-client-role" + description = "My Client Role" +} + +resource "keycloak_role" "admin_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "admin" + composite_roles = [ + "{keycloak_role.create_role.id}", + "{keycloak_role.read_role.id}", + "{keycloak_role.update_role.id}", + "{keycloak_role.delete_role.id}", + "{keycloak_role.client_role.id}", + ] +} +``` + +### Argument Reference + +The following arguments are supported: + +- `realm_id` - (Required) The realm this role exists within. +- `client_id` - (Optional) When specified, this role will be created as + a client role attached to the client with the provided ID +- `name` - (Required) The name of the role +- `description` - (Optional) The description of the role +- `composite_roles` - (Optional) When specified, this role will be a + composite role, composed of all roles that have an ID present within + this list. + + +### Import + +Roles can be imported using the format `{{realm_id}}/{{role_id}}`, where +`role_id` is the unique ID that Keycloak assigns to the role. The ID is +not easy to find in the GUI, but it appears in the URL when editing the +role. + +Example: + +```bash +$ terraform import keycloak_role.role my-realm/7e8cf32a-8acb-4d34-89c4-04fb1d10ccad +``` diff --git a/example/main.tf b/example/main.tf index 6e094d6dc..e24bc6caf 100644 --- a/example/main.tf +++ b/example/main.tf @@ -9,7 +9,7 @@ resource "keycloak_realm" "test" { enabled = true display_name = "foo" - smtp_server = { + smtp_server { host = "mysmtphost.com" port = 25 from_display_name = "Tom" @@ -20,7 +20,7 @@ resource "keycloak_realm" "test" { starttls = true envelope_from = "nottom@myhost.com" - auth = { + auth { username = "tom" password = "tom" } @@ -30,17 +30,18 @@ resource "keycloak_realm" "test" { access_code_lifespan = "30m" - internationalization = { + internationalization { supported_locales = [ "en", "de", - "es" + "es", ] + default_locale = "en" } - security_defenses = { - headers = { + security_defenses { + headers { x_frame_options = "DENY" content_security_policy = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';" content_security_policy_report_only = "" @@ -114,7 +115,7 @@ resource "keycloak_user" "user_with_password" { last_name = "Tester" initial_password { - value = "my password" + value = "My password" temporary = false } } @@ -145,7 +146,8 @@ resource "keycloak_openid_client" "test_client" { realm_id = "${keycloak_realm.test.id}" description = "a test openid client" - standard_flow_enabled = true + standard_flow_enabled = true + service_accounts_enabled = true access_type = "CONFIDENTIAL" @@ -193,6 +195,7 @@ resource "keycloak_openid_client_optional_scopes" "optional_client_scopes" { "address", "phone", "offline_access", + "microprofile-jwt", "${keycloak_openid_client_scope.test_optional_client_scope.name}", ] } @@ -434,6 +437,7 @@ resource keycloak_oidc_identity_provider custom_oidc_idp { token_url = "https://example.com/token" client_id = "example_id" client_secret = "example_token" + extra_config = { dummyConfig = "dummyValue" } @@ -562,10 +566,14 @@ resource "keycloak_openid_client_authorization_permission" "resource" { resource_server_id = "${keycloak_openid_client.test_client_auth.resource_server_id}" realm_id = "${keycloak_realm.test.id}" name = "test" + policies = [ - "${data.keycloak_openid_client_authorization_policy.default.id}"] + "${data.keycloak_openid_client_authorization_policy.default.id}", + ] + resources = [ - "${keycloak_openid_client_authorization_resource.resource.id}"] + "${keycloak_openid_client_authorization_resource.resource.id}", + ] } resource "keycloak_openid_client_authorization_resource" "resource" { @@ -574,7 +582,7 @@ resource "keycloak_openid_client_authorization_resource" "resource" { realm_id = "${keycloak_realm.test.id}" uris = [ - "/endpoint/*" + "/endpoint/*", ] attributes = { diff --git a/example/roles.tf b/example/roles.tf new file mode 100644 index 000000000..da847d6b9 --- /dev/null +++ b/example/roles.tf @@ -0,0 +1,144 @@ +resource "keycloak_realm" "roles_example" { + realm = "roles-example" + enabled = true +} + +// API Client and roles + +resource "keycloak_openid_client" "pet_api" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "pet-api" + name = "pet-api" + + enabled = true + + access_type = "BEARER-ONLY" +} + +resource "keycloak_role" "pet_api_create_pet" { + name = "create-pet" + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_api.id}" + description = "Ability to create a new pet" +} + +resource "keycloak_role" "pet_api_update_pet" { + name = "update-pet" + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_api.id}" + description = "Ability to update a pet" +} + +resource "keycloak_role" "pet_api_read_pet" { + name = "read-pet" + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_api.id}" + description = "Ability to read / list pets" +} + +resource "keycloak_role" "pet_api_delete_pet" { + name = "delete-pet" + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_api.id}" + description = "Ability to delete a pet" +} + +resource "keycloak_role" "pet_api_admin" { + name = "admin" + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_api.id}" + + composite_roles = [ + "${keycloak_role.pet_api_create_pet.id}", + "${keycloak_role.pet_api_delete_pet.id}", + "${keycloak_role.pet_api_update_pet.id}", + ] +} + +// Consumer client + +resource "keycloak_openid_client" "pet_app" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "pet-app" + name = "pet-app" + + enabled = true + + access_type = "CONFIDENTIAL" + client_secret = "pet-app-secret" + + // authenticated users - could have many roles + standard_flow_enabled = true + + // unauthenticated users - needs at least read / list role for browsing + service_accounts_enabled = true + + valid_redirect_uris = [ + "http://localhost:5555/openid-callback", + ] +} + +// The app will always need access to the API, so this audience should be used regardless of auth type +resource "keycloak_openid_audience_protocol_mapper" "pet_app_pet_api_audience_mapper" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_app.id}" + name = "audience-mapper" + + included_client_audience = "${keycloak_openid_client.pet_api.client_id}" +} + +// The app will always need to read / list pets regardless of who is logged in +resource "keycloak_openid_hardcoded_role_protocol_mapper" "pet_app_pet_api_read_role" { + realm_id = "${keycloak_realm.roles_example.id}" + client_id = "${keycloak_openid_client.pet_app.id}" + name = "read-pets-role" + + role_id = "${keycloak_role.pet_api_read_pet.id}" +} + +// Users and groups + +resource "keycloak_group" "pet_api_base" { + realm_id = "${keycloak_realm.roles_example.id}" + name = "pets" +} + +resource "keycloak_group" "pet_api_admins" { + realm_id = "${keycloak_realm.roles_example.id}" + parent_id = "${keycloak_group.pet_api_base.id}" + name = "admins" +} + +resource "keycloak_group" "pet_api_front_desk" { + realm_id = "${keycloak_realm.roles_example.id}" + parent_id = "${keycloak_group.pet_api_base.id}" + name = "front-desk" +} + +data "keycloak_role" "realm_offline_access" { + realm_id = "${keycloak_realm.roles_example.id}" + name = "offline_access" +} + +resource "keycloak_group_roles" "admin_roles" { + realm_id = "${keycloak_realm.roles_example.id}" + group_id = "${keycloak_group.pet_api_admins.id}" + + role_ids = [ + "${keycloak_role.pet_api_read_pet.id}", + "${keycloak_role.pet_api_delete_pet.id}", + "${keycloak_role.pet_api_create_pet.id}", + "${data.keycloak_role.realm_offline_access.id}", + ] +} + +resource "keycloak_group_roles" "front_desk_roles" { + realm_id = "${keycloak_realm.roles_example.id}" + group_id = "${keycloak_group.pet_api_front_desk.id}" + + role_ids = [ + "${keycloak_role.pet_api_read_pet.id}", + "${keycloak_role.pet_api_create_pet.id}", + "${data.keycloak_role.realm_offline_access.id}", + ] +} diff --git a/keycloak/generic_client.go b/keycloak/generic_client.go index 6c98ac64c..6e63028fe 100644 --- a/keycloak/generic_client.go +++ b/keycloak/generic_client.go @@ -27,3 +27,26 @@ func (keycloakClient *KeycloakClient) listGenericClients(realmId string) ([]*Gen return clients, nil } + +func (keycloakClient *KeycloakClient) GetGenericClientByClientId(realmId, clientId string) (*GenericClient, error) { + var clients []GenericClient + + params := map[string]string{ + "clientId": clientId, + } + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/clients", realmId), &clients, params) + if err != nil { + return nil, err + } + + if len(clients) == 0 { + return nil, fmt.Errorf("generic client with name %s does not exist", clientId) + } + + client := clients[0] + + client.RealmId = realmId + + return &client, nil +} diff --git a/keycloak/group.go b/keycloak/group.go index 48e01fc5f..2077bb405 100644 --- a/keycloak/group.go +++ b/keycloak/group.go @@ -6,12 +6,14 @@ import ( ) type Group struct { - Id string `json:"id,omitempty"` - RealmId string `json:"-"` - ParentId string `json:"-"` - Name string `json:"name"` - Path string `json:"path,omitempty"` - SubGroups []*Group `json:"subGroups,omitempty"` + Id string `json:"id,omitempty"` + RealmId string `json:"-"` + ParentId string `json:"-"` + Name string `json:"name"` + Path string `json:"path,omitempty"` + SubGroups []*Group `json:"subGroups,omitempty"` + RealmRoles []string `json:"realmRoles,omitempty"` + ClientRoles map[string][]string `json:"clientRoles,omitempty"` } /* @@ -172,3 +174,27 @@ func (keycloakClient *KeycloakClient) GetDefaultGroups(realmName string) ([]Grou return defaultGroups, err } + +func (keycloakClient *KeycloakClient) AddRealmRolesToGroup(realmId, groupId string, roles []*Role) error { + _, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/realm", realmId, groupId), roles) + + return err +} + +func (keycloakClient *KeycloakClient) AddClientRolesToGroup(realmId, groupId, clientId string, roles []*Role) error { + _, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/clients/%s", realmId, groupId, clientId), roles) + + return err +} + +func (keycloakClient *KeycloakClient) RemoveRealmRolesFromGroup(realmId, groupId string, roles []*Role) error { + err := keycloakClient.delete(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/realm", realmId, groupId), roles) + + return err +} + +func (keycloakClient *KeycloakClient) RemoveClientRolesFromGroup(realmId, groupId, clientId string, roles []*Role) error { + err := keycloakClient.delete(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/clients/%s", realmId, groupId, clientId), roles) + + return err +} diff --git a/keycloak/openid_client.go b/keycloak/openid_client.go index 9bf9ca226..dbef60fc2 100644 --- a/keycloak/openid_client.go +++ b/keycloak/openid_client.go @@ -46,30 +46,6 @@ type OpenidClient struct { AuthorizationSettings *OpenidClientAuthorizationSettings `json:"authorizationSettings,omitempty"` } -func (keycloakClient *KeycloakClient) GetClientRoleByName(realm, clientId, name string) (*OpenidClientRole, error) { - var clientRole OpenidClientRole - err := keycloakClient.get(fmt.Sprintf("/realms/%s/clients/%s/roles/%s", realm, clientId, name), &clientRole, nil) - if err != nil { - return nil, err - } - return &clientRole, nil -} - -func (keycloakClient *KeycloakClient) GetClientByName(realm, clientId string) (*OpenidClient, error) { - var clients []OpenidClient - params := map[string]string{"clientId": clientId} - err := keycloakClient.get(fmt.Sprintf("/realms/%s/clients", realm), &clients, params) - if err != nil { - return nil, err - } - if len(clients) == 0 { - return nil, fmt.Errorf("no clients with name %s found", clientId) - } - client := clients[0] - client.RealmId = realm - return &client, nil -} - func (keycloakClient *KeycloakClient) GetOpenidClientServiceAccountUserId(realmId, clientId string) (*User, error) { var serviceAccountUser User err := keycloakClient.get(fmt.Sprintf("/realms/%s/clients/%s/service-account-user", realmId, clientId), &serviceAccountUser, nil) diff --git a/keycloak/openid_client_service_account_role.go b/keycloak/openid_client_service_account_role.go index d8e6aa895..bb4deb717 100644 --- a/keycloak/openid_client_service_account_role.go +++ b/keycloak/openid_client_service_account_role.go @@ -16,7 +16,7 @@ type OpenidClientServiceAccountRole struct { } func (keycloakClient *KeycloakClient) NewOpenidClientServiceAccountRole(serviceAccountRole *OpenidClientServiceAccountRole) error { - role, err := keycloakClient.GetClientRoleByName(serviceAccountRole.RealmId, serviceAccountRole.ContainerId, serviceAccountRole.Name) + role, err := keycloakClient.GetRoleByName(serviceAccountRole.RealmId, serviceAccountRole.ContainerId, serviceAccountRole.Name) if err != nil { return err } diff --git a/keycloak/openid_hardcoded_role_protocol_mapper.go b/keycloak/openid_hardcoded_role_protocol_mapper.go new file mode 100644 index 000000000..70d1b814e --- /dev/null +++ b/keycloak/openid_hardcoded_role_protocol_mapper.go @@ -0,0 +1,155 @@ +package keycloak + +import ( + "fmt" + "strings" +) + +type OpenIdHardcodedRoleProtocolMapper struct { + Id string + Name string + RealmId string + ClientId string + ClientScopeId string + + RoleId string +} + +var roleField = "role" + +func parseRoleClientIdAndName(roleProp string) (string, string) { + parts := strings.Split(roleProp, ".") + + if len(parts) == 2 { + return parts[0], parts[1] + } + + return "", parts[0] +} + +func (keycloakClient *KeycloakClient) getRolePropFromRole(role *Role) (string, error) { + if role.ClientRole { + client, err := keycloakClient.GetOpenidClient(role.RealmId, role.ContainerId) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s.%s", client.ClientId, role.Name), nil + } + + return role.Name, nil +} + +func (mapper *OpenIdHardcodedRoleProtocolMapper) convertToGenericProtocolMapper(roleProp string) *protocolMapper { + return &protocolMapper{ + Id: mapper.Id, + Name: mapper.Name, + Protocol: "openid-connect", + ProtocolMapper: "oidc-hardcoded-role-mapper", + Config: map[string]string{ + roleField: roleProp, + }, + } +} + +func (protocolMapper *protocolMapper) convertToOpenIdHardcodedRoleProtocolMapper(realmId, clientId, clientScopeId, roleId string) (*OpenIdHardcodedRoleProtocolMapper, error) { + return &OpenIdHardcodedRoleProtocolMapper{ + Id: protocolMapper.Id, + Name: protocolMapper.Name, + RealmId: realmId, + ClientId: clientId, + ClientScopeId: clientScopeId, + + RoleId: roleId, + }, nil +} + +func (keycloakClient *KeycloakClient) GetOpenIdHardcodedRoleProtocolMapper(realmId, clientId, clientScopeId, mapperId string) (*OpenIdHardcodedRoleProtocolMapper, error) { + var protocolMapper *protocolMapper + + err := keycloakClient.get(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), &protocolMapper, nil) + if err != nil { + return nil, err + } + + roleClientId, roleName := parseRoleClientIdAndName(protocolMapper.Config[roleField]) + + var roleClientUId = "" + if roleClientId != "" { + client, err := keycloakClient.GetOpenidClientByClientId(realmId, roleClientId) + if err != nil { + return nil, err + } + + roleClientUId = client.Id + } + + role, err := keycloakClient.GetRoleByName(realmId, roleClientUId, roleName) + if err != nil { + return nil, err + } + + return protocolMapper.convertToOpenIdHardcodedRoleProtocolMapper(realmId, clientId, clientScopeId, role.Id) +} + +func (keycloakClient *KeycloakClient) DeleteOpenIdHardcodedRoleProtocolMapper(realmId, clientId, clientScopeId, mapperId string) error { + return keycloakClient.delete(individualProtocolMapperPath(realmId, clientId, clientScopeId, mapperId), nil) +} + +func (keycloakClient *KeycloakClient) NewOpenIdHardcodedRoleProtocolMapper(mapper *OpenIdHardcodedRoleProtocolMapper) error { + role, err := keycloakClient.GetRole(mapper.RealmId, mapper.RoleId) + if err != nil { + return err + } + + roleProp, err := keycloakClient.getRolePropFromRole(role) + if err != nil { + return err + } + + path := protocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId) + + _, location, err := keycloakClient.post(path, mapper.convertToGenericProtocolMapper(roleProp)) + if err != nil { + return err + } + + mapper.Id = getIdFromLocationHeader(location) + + return nil +} + +func (keycloakClient *KeycloakClient) UpdateOpenIdHardcodedRoleProtocolMapper(mapper *OpenIdHardcodedRoleProtocolMapper) error { + role, err := keycloakClient.GetRole(mapper.RealmId, mapper.RoleId) + if err != nil { + return err + } + + roleProp, err := keycloakClient.getRolePropFromRole(role) + if err != nil { + return err + } + + path := individualProtocolMapperPath(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + + return keycloakClient.put(path, mapper.convertToGenericProtocolMapper(roleProp)) +} + +func (keycloakClient *KeycloakClient) ValidateOpenIdHardcodedRoleProtocolMapper(mapper *OpenIdHardcodedRoleProtocolMapper) error { + if mapper.ClientId == "" && mapper.ClientScopeId == "" { + return fmt.Errorf("validation error: one of ClientId or ClientScopeId must be set") + } + + protocolMappers, err := keycloakClient.listGenericProtocolMappers(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId) + if err != nil { + return err + } + + for _, protocolMapper := range protocolMappers { + if protocolMapper.Name == mapper.Name && protocolMapper.Id != mapper.Id { + return fmt.Errorf("validation error: a protocol mapper with name %s already exists for this client", mapper.Name) + } + } + + return nil +} diff --git a/keycloak/role.go b/keycloak/role.go new file mode 100644 index 000000000..17f0dd75c --- /dev/null +++ b/keycloak/role.go @@ -0,0 +1,124 @@ +package keycloak + +import ( + "fmt" +) + +type Role struct { + Id string `json:"id,omitempty"` + RealmId string `json:"-"` + ClientId string `json:"-"` + RoleId string `json:"-"` + Name string `json:"name"` + Description string `json:"description"` + ClientRole bool `json:"clientRole"` + ContainerId string `json:"containerId"` + Composite bool `json:"composite"` +} + +/* + * Realm roles: /realms/${realm_id}/roles + * Client roles: /realms/${realm_id}/clients/${client_id}/roles + */ +func roleByNameUrl(realmId, clientId string) string { + if clientId == "" { + return fmt.Sprintf("/realms/%s/roles", realmId) + } + + return fmt.Sprintf("/realms/%s/clients/%s/roles", realmId, clientId) +} + +func (keycloakClient *KeycloakClient) CreateRole(role *Role) error { + url := roleByNameUrl(role.RealmId, role.ClientId) + + if role.ClientId != "" { + role.ContainerId = role.ClientId + role.ClientRole = true + } + + _, _, err := keycloakClient.post(url, role) + if err != nil { + return err + } + + var createdRole Role + err = keycloakClient.get(fmt.Sprintf("%s/%s", url, role.Name), &createdRole, nil) + if err != nil { + return err + } + + role.Id = createdRole.Id + + return nil +} + +func (keycloakClient *KeycloakClient) GetRole(realmId, id string) (*Role, error) { + var role Role + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/roles-by-id/%s", realmId, id), &role, nil) + if err != nil { + return nil, err + } + + role.RealmId = realmId + + if role.ClientRole { + role.ClientId = role.ContainerId + } + + return &role, nil +} + +func (keycloakClient *KeycloakClient) GetRoleByName(realmId, clientId, name string) (*Role, error) { + var role Role + + err := keycloakClient.get(fmt.Sprintf("%s/%s", roleByNameUrl(realmId, clientId), name), &role, nil) + if err != nil { + return nil, err + } + + role.RealmId = realmId + + if role.ClientRole { + role.ClientId = role.ContainerId + } + + return &role, nil +} + +func (keycloakClient *KeycloakClient) UpdateRole(role *Role) error { + return keycloakClient.put(fmt.Sprintf("/realms/%s/roles-by-id/%s", role.RealmId, role.Id), role) +} + +func (keycloakClient *KeycloakClient) DeleteRole(realmId, id string) error { + return keycloakClient.delete(fmt.Sprintf("/realms/%s/roles-by-id/%s", realmId, id), nil) +} + +func (keycloakClient *KeycloakClient) AddCompositesToRole(role *Role, compositeRoles []*Role) error { + _, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/roles-by-id/%s/composites", role.RealmId, role.Id), compositeRoles) + if err != nil { + return err + } + + return nil +} + +func (keycloakClient *KeycloakClient) RemoveCompositesFromRole(role *Role, compositeRoles []*Role) error { + err := keycloakClient.delete(fmt.Sprintf("/realms/%s/roles-by-id/%s/composites", role.RealmId, role.Id), compositeRoles) + if err != nil { + return err + } + + return nil +} + +func (keycloakClient *KeycloakClient) GetRoleComposites(role *Role) ([]*Role, error) { + var composites []*Role + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/roles-by-id/%s/composites", role.RealmId, role.Id), &composites, nil) + if err != nil { + return nil, err + } + + return composites, nil +} diff --git a/mkdocs.yml b/mkdocs.yml index e1e0af165..7433ada49 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,13 +4,16 @@ repo_url: https://github.com/mrparkers/terraform-provider-keycloak nav: - Getting Started: index.md - Data Sources: - - keycloak_realm_keys: resources/datasource_keycloak_realm_keys.md + - keycloak_realm_keys: data_sources/keycloak_realm_keys.md + - keycloak_role: data_sources/keycloak_role.md - Resources: - keycloak_realm: resources/keycloak_realm.md - keycloak_user: resources/keycloak_user.md - - keycloak_generic_client_protocol_mapper: resources/keycloak_generic_client_protocol_mapper.md + - keycloak_role: resources/keycloak_role.md - keycloak_group: resources/keycloak_group.md - keycloak_group_memberships: resources/keycloak_group_memberships.md + - keycloak_group_roles: resources/keycloak_group_roles.md + - keycloak_default_groups: resources/keycloak_default_groups.md - 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 @@ -21,9 +24,11 @@ nav: - keycloak_openid_hardcoded_claim_protocol_mapper: resources/keycloak_openid_hardcoded_claim_protocol_mapper.md - keycloak_openid_full_name_protocol_mapper: resources/keycloak_openid_full_name_protocol_mapper.md - keycloak_openid_audience_protocol_mapper: resources/keycloak_openid_audience_protocol_mapper.md + - keycloak_openid_hardcoded_role_protocol_mapper: resources/keycloak_openid_hardcoded_role_protocol_mapper.md - keycloak_saml_client: resources/keycloak_saml_client.md - keycloak_saml_user_attribute_protocol_mapper: resources/keycloak_saml_user_attribute_protocol_mapper.md - keycloak_saml_user_property_protocol_mapper: resources/keycloak_saml_user_property_protocol_mapper.md + - keycloak_generic_client_protocol_mapper: resources/keycloak_generic_client_protocol_mapper.md - keycloak_ldap_user_federation: resources/keycloak_ldap_user_federation.md - keycloak_ldap_full_name_mapper: resources/keycloak_ldap_full_name_mapper.md - keycloak_ldap_group_mapper: resources/keycloak_ldap_group_mapper.md diff --git a/provider/data_source_keycloak_openid_client.go b/provider/data_source_keycloak_openid_client.go index 8450c4b2e..0635e003b 100644 --- a/provider/data_source_keycloak_openid_client.go +++ b/provider/data_source_keycloak_openid_client.go @@ -102,7 +102,7 @@ func dataSourceKeycloakOpenidClientRead(data *schema.ResourceData, meta interfac realmId := data.Get("realm_id").(string) clientId := data.Get("client_id").(string) - client, err := keycloakClient.GetClientByName(realmId, clientId) + client, err := keycloakClient.GetOpenidClientByClientId(realmId, clientId) if err != nil { return handleNotFoundError(err, data) } diff --git a/provider/data_source_keycloak_role.go b/provider/data_source_keycloak_role.go new file mode 100644 index 000000000..d0b3f84e3 --- /dev/null +++ b/provider/data_source_keycloak_role.go @@ -0,0 +1,47 @@ +package provider + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func dataSourceKeycloakRole() *schema.Resource { + return &schema.Resource{ + Read: dataSourceKeycloakRoleRead, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + }, + "client_id": { + Type: schema.TypeString, + Optional: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceKeycloakRoleRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + roleName := data.Get("name").(string) + + role, err := keycloakClient.GetRoleByName(realmId, clientId, roleName) + if err != nil { + return err + } + + mapFromRoleToData(data, role) + + return nil +} diff --git a/provider/data_source_keycloak_role_test.go b/provider/data_source_keycloak_role_test.go new file mode 100644 index 000000000..6eb7a5af4 --- /dev/null +++ b/provider/data_source_keycloak_role_test.go @@ -0,0 +1,116 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestAccKeycloakDataSourceRole_basic(t *testing.T) { + realm := "terraform-" + acctest.RandString(10) + client := "terraform-client-" + acctest.RandString(10) + realmRole := "terraform-role-" + acctest.RandString(10) + clientRole := "terraform-role-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + { + Config: testDataSourceKeycloakRole_basic(realm, client, realmRole, clientRole), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakRoleExists("keycloak_role.realm_role"), + testAccCheckKeycloakRoleExists("keycloak_role.client_role"), + // realm role + resource.TestCheckResourceAttrPair("keycloak_role.realm_role", "id", "data.keycloak_role.realm_role", "id"), + resource.TestCheckResourceAttrPair("keycloak_role.realm_role", "realm_id", "data.keycloak_role.realm_role", "realm_id"), + resource.TestCheckResourceAttrPair("keycloak_role.realm_role", "name", "data.keycloak_role.realm_role", "name"), + resource.TestCheckResourceAttrPair("keycloak_role.realm_role", "description", "data.keycloak_role.realm_role", "description"), + testAccCheckDataKeycloakRole("data.keycloak_role.realm_role"), + // client role + resource.TestCheckResourceAttrPair("keycloak_role.client_role", "id", "data.keycloak_role.client_role", "id"), + resource.TestCheckResourceAttrPair("keycloak_role.client_role", "realm_id", "data.keycloak_role.client_role", "realm_id"), + resource.TestCheckResourceAttrPair("keycloak_role.client_role", "client_id", "data.keycloak_role.client_role", "client_id"), + resource.TestCheckResourceAttrPair("keycloak_role.client_role", "name", "data.keycloak_role.client_role", "name"), + resource.TestCheckResourceAttrPair("keycloak_role.client_role", "description", "data.keycloak_role.client_role", "description"), + testAccCheckDataKeycloakRole("data.keycloak_role.client_role"), + // offline_access + resource.TestCheckResourceAttrPair("keycloak_realm.realm", "realm", "data.keycloak_role.realm_offline_access", "realm_id"), + resource.TestCheckResourceAttr("data.keycloak_role.realm_offline_access", "name", "offline_access"), + testAccCheckDataKeycloakRole("data.keycloak_role.realm_offline_access"), + ), + }, + }, + }) +} + +func testAccCheckDataKeycloakRole(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) + } + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + id := rs.Primary.ID + realmId := rs.Primary.Attributes["realm_id"] + name := rs.Primary.Attributes["name"] + + role, err := keycloakClient.GetRole(realmId, id) + if err != nil { + return err + } + + if role.Name != name { + return fmt.Errorf("expected role with ID %s to have name %s, but got %s", id, name, role.Name) + } + + return nil + } +} + +func testDataSourceKeycloakRole_basic(realm, client, realmRole, clientRole string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_role" "realm_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "client_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.client.id}" +} + +data "keycloak_role" "realm_role" { + realm_id = "${keycloak_realm.realm.id}" + name = "${keycloak_role.realm_role.name}" +} + +data "keycloak_role" "client_role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.client.id}" + name = "${keycloak_role.client_role.name}" +} + +data "keycloak_role" "realm_offline_access" { + realm_id = "${keycloak_realm.realm.id}" + name = "offline_access" +} + `, realm, client, realmRole, clientRole) +} diff --git a/provider/provider.go b/provider/provider.go index 85ca4bc29..a3f2c88f3 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -11,6 +11,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_openid_client": dataSourceKeycloakOpenidClient(), "keycloak_openid_client_authorization_policy": dataSourceKeycloakOpenidClientAuthorizationPolicy(), "keycloak_realm_keys": dataSourceKeycloakRealmKeys(), + "keycloak_role": dataSourceKeycloakRole(), }, ResourcesMap: map[string]*schema.Resource{ "keycloak_realm": resourceKeycloakRealm(), @@ -18,6 +19,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_group": resourceKeycloakGroup(), "keycloak_group_memberships": resourceKeycloakGroupMemberships(), "keycloak_default_groups": resourceKeycloakDefaultGroups(), + "keycloak_group_roles": resourceKeycloakGroupRoles(), "keycloak_user": resourceKeycloakUser(), "keycloak_openid_client": resourceKeycloakOpenidClient(), "keycloak_openid_client_scope": resourceKeycloakOpenidClientScope(), @@ -33,6 +35,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_openid_full_name_protocol_mapper": resourceKeycloakOpenIdFullNameProtocolMapper(), "keycloak_openid_hardcoded_claim_protocol_mapper": resourceKeycloakOpenIdHardcodedClaimProtocolMapper(), "keycloak_openid_audience_protocol_mapper": resourceKeycloakOpenIdAudienceProtocolMapper(), + "keycloak_openid_hardcoded_role_protocol_mapper": resourceKeycloakOpenIdHardcodedRoleProtocolMapper(), "keycloak_openid_client_default_scopes": resourceKeycloakOpenidClientDefaultScopes(), "keycloak_openid_client_optional_scopes": resourceKeycloakOpenidClientOptionalScopes(), "keycloak_saml_client": resourceKeycloakSamlClient(), @@ -50,6 +53,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_openid_client_authorization_scope": resourceKeycloakOpenidClientAuthorizationScope(), "keycloak_openid_client_authorization_permission": resourceKeycloakOpenidClientAuthorizationPermission(), "keycloak_openid_client_service_account_role": resourceKeycloakOpenidClientServiceAccountRole(), + "keycloak_role": resourceKeycloakRole(), }, Schema: map[string]*schema.Schema{ "client_id": { diff --git a/provider/resource_keycloak_group_roles.go b/provider/resource_keycloak_group_roles.go new file mode 100644 index 000000000..4dce9e0a8 --- /dev/null +++ b/provider/resource_keycloak_group_roles.go @@ -0,0 +1,356 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "strings" +) + +func resourceKeycloakGroupRoles() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakGroupRolesCreate, + Read: resourceKeycloakGroupRolesRead, + Update: resourceKeycloakGroupRolesUpdate, + Delete: resourceKeycloakGroupRolesDelete, + // This resource can be imported using {{realm}}/{{groupId}}. + Importer: &schema.ResourceImporter{ + State: resourceKeycloakGroupRolesImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "group_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "role_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Required: true, + }, + }, + } +} + +func groupRolesId(realmId, groupId string) string { + return fmt.Sprintf("%s/%s", realmId, groupId) +} + +func getMapOfRealmAndClientRoles(keycloakClient *keycloak.KeycloakClient, realmId string, roleIds []string) (map[string][]*keycloak.Role, error) { + roles := make(map[string][]*keycloak.Role) + + for _, roleId := range roleIds { + role, err := keycloakClient.GetRole(realmId, roleId) + if err != nil { + return nil, err + } + + if role.ClientRole { + roles[role.ClientId] = append(roles[role.ClientId], role) + } else { + roles["realm"] = append(roles["realm"], role) + } + } + + return roles, nil +} + +// given a group and a map of roles we already know about, fetch the roles we don't know about +// `localRoles` is used as a cache to avoid unnecessary http requests +func getMapOfRealmAndClientRolesFromGroup(keycloakClient *keycloak.KeycloakClient, group *keycloak.Group, localRoles map[string][]*keycloak.Role) (map[string][]*keycloak.Role, error) { + roles := make(map[string][]*keycloak.Role) + + // realm roles + if len(group.RealmRoles) != 0 { + var realmRoles []*keycloak.Role + + for _, realmRoleName := range group.RealmRoles { + found := false + + for _, localRealmRole := range localRoles["realm"] { + if localRealmRole.Name == realmRoleName { + found = true + realmRoles = append(realmRoles, localRealmRole) + + break + } + } + + if !found { + realmRole, err := keycloakClient.GetRoleByName(group.RealmId, "", realmRoleName) + if err != nil { + return nil, err + } + + realmRoles = append(realmRoles, realmRole) + } + } + + roles["realm"] = realmRoles + } + + // client roles + if len(group.ClientRoles) != 0 { + for clientName, clientRoleNames := range group.ClientRoles { + client, err := keycloakClient.GetGenericClientByClientId(group.RealmId, clientName) + if err != nil { + return nil, err + } + + var clientRoles []*keycloak.Role + for _, clientRoleName := range clientRoleNames { + found := false + + for _, localClientRole := range localRoles[client.Id] { + if localClientRole.Name == clientRoleName { + found = true + clientRoles = append(clientRoles, localClientRole) + + break + } + } + + if !found { + clientRole, err := keycloakClient.GetRoleByName(group.RealmId, client.Id, clientRoleName) + if err != nil { + return nil, err + } + + clientRoles = append(clientRoles, clientRole) + } + } + + roles[client.Id] = clientRoles + } + } + + return roles, nil +} + +func addRolesToGroup(keycloakClient *keycloak.KeycloakClient, rolesToAdd map[string][]*keycloak.Role, group *keycloak.Group) error { + if realmRoles, ok := rolesToAdd["realm"]; ok && len(realmRoles) != 0 { + err := keycloakClient.AddRealmRolesToGroup(group.RealmId, group.Id, realmRoles) + if err != nil { + return err + } + } + + for k, roles := range rolesToAdd { + if k == "realm" { + continue + } + + err := keycloakClient.AddClientRolesToGroup(group.RealmId, group.Id, k, roles) + if err != nil { + return err + } + } + + return nil +} + +func removeRolesFromGroup(keycloakClient *keycloak.KeycloakClient, rolesToRemove map[string][]*keycloak.Role, group *keycloak.Group) error { + if realmRoles, ok := rolesToRemove["realm"]; ok && len(realmRoles) != 0 { + err := keycloakClient.RemoveRealmRolesFromGroup(group.RealmId, group.Id, realmRoles) + if err != nil { + return err + } + } + + for k, roles := range rolesToRemove { + if k == "realm" { + continue + } + + err := keycloakClient.RemoveClientRolesFromGroup(group.RealmId, group.Id, k, roles) + if err != nil { + return err + } + } + + return nil +} + +func resourceKeycloakGroupRolesCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + groupId := data.Get("group_id").(string) + + group, err := keycloakClient.GetGroup(realmId, groupId) + if err != nil { + return err + } + + roleIds := interfaceSliceToStringSlice(data.Get("role_ids").(*schema.Set).List()) + rolesToAdd, err := getMapOfRealmAndClientRoles(keycloakClient, realmId, roleIds) + if err != nil { + return err + } + + err = addRolesToGroup(keycloakClient, rolesToAdd, group) + if err != nil { + return err + } + + data.SetId(groupRolesId(realmId, groupId)) + + return resourceKeycloakGroupRolesRead(data, meta) +} + +func resourceKeycloakGroupRolesRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + groupId := data.Get("group_id").(string) + + group, err := keycloakClient.GetGroup(realmId, groupId) + if err != nil { + return err + } + + var roleIds []string + + if len(group.RealmRoles) != 0 { + for _, realmRole := range group.RealmRoles { + role, err := keycloakClient.GetRoleByName(realmId, "", realmRole) + if err != nil { + return err + } + + roleIds = append(roleIds, role.Id) + } + } + + if len(group.ClientRoles) != 0 { + for clientName, clientRoles := range group.ClientRoles { + client, err := keycloakClient.GetGenericClientByClientId(realmId, clientName) + if err != nil { + return err + } + + for _, clientRole := range clientRoles { + role, err := keycloakClient.GetRoleByName(realmId, client.Id, clientRole) + if err != nil { + return err + } + + roleIds = append(roleIds, role.Id) + } + } + } + + data.Set("role_ids", roleIds) + data.SetId(groupRolesId(realmId, groupId)) + + return nil +} + +func resourceKeycloakGroupRolesUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + groupId := data.Get("group_id").(string) + + group, err := keycloakClient.GetGroup(realmId, groupId) + if err != nil { + return err + } + + roleIds := interfaceSliceToStringSlice(data.Get("role_ids").(*schema.Set).List()) + + tfRoles, err := getMapOfRealmAndClientRoles(keycloakClient, realmId, roleIds) + if err != nil { + return err + } + + remoteRoles, err := getMapOfRealmAndClientRolesFromGroup(keycloakClient, group, tfRoles) + if err != nil { + return err + } + + removeDuplicateRoles(&tfRoles, &remoteRoles) + + // `tfRoles` contains all roles that need to be added + // `remoteRoles` contains all roles that need to be removed + + err = addRolesToGroup(keycloakClient, tfRoles, group) + if err != nil { + return err + } + + err = removeRolesFromGroup(keycloakClient, remoteRoles, group) + if err != nil { + return err + } + + return nil +} + +func resourceKeycloakGroupRolesDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + groupId := data.Get("group_id").(string) + + group, err := keycloakClient.GetGroup(realmId, groupId) + + roleIds := interfaceSliceToStringSlice(data.Get("role_ids").(*schema.Set).List()) + rolesToRemove, err := getMapOfRealmAndClientRoles(keycloakClient, realmId, roleIds) + if err != nil { + return err + } + + err = removeRolesFromGroup(keycloakClient, rolesToRemove, group) + if err != nil { + return err + } + + return nil +} + +func resourceKeycloakGroupRolesImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid import. Supported import format: {{realm}}/{{groupId}}.") + } + + d.Set("realm_id", parts[0]) + d.Set("group_id", parts[1]) + + d.SetId(groupRolesId(parts[0], parts[1])) + + return []*schema.ResourceData{d}, nil +} + +func removeRoleFromSlice(slice []*keycloak.Role, index int) []*keycloak.Role { + slice[index] = slice[len(slice)-1] + return slice[:len(slice)-1] +} + +func removeDuplicateRoles(one, two *map[string][]*keycloak.Role) { + for k := range *one { + for i1 := 0; i1 < len((*one)[k]); i1++ { + s1 := (*one)[k][i1] + + for i2 := 0; i2 < len((*two)[k]); i2++ { + s2 := (*two)[k][i2] + + if s1.Id == s2.Id { + (*one)[k] = removeRoleFromSlice((*one)[k], i1) + (*two)[k] = removeRoleFromSlice((*two)[k], i2) + + i1-- + break + } + } + } + } +} diff --git a/provider/resource_keycloak_group_roles_test.go b/provider/resource_keycloak_group_roles_test.go new file mode 100644 index 000000000..f9d747ca2 --- /dev/null +++ b/provider/resource_keycloak_group_roles_test.go @@ -0,0 +1,425 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "regexp" + "testing" +) + +func TestAccKeycloakGroupRoles_basic(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + realmRoleName := "terraform-role-" + acctest.RandString(10) + openIdClientName := "terraform-openid-client-" + acctest.RandString(10) + openIdRoleName := "terraform-role-" + acctest.RandString(10) + samlClientName := "terraform-saml-client-" + acctest.RandString(10) + samlRoleName := "terraform-role-" + acctest.RandString(10) + groupName := "terraform-group-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testKeycloakGroupRoles_basic(realmName, openIdClientName, samlClientName, realmRoleName, openIdRoleName, samlRoleName, groupName), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + { + ResourceName: "keycloak_group_roles.group_roles", + ImportState: true, + ImportStateVerify: true, + }, + // check destroy + { + Config: testKeycloakGroupRoles_noGroupRoles(realmName, openIdClientName, samlClientName, realmRoleName, openIdRoleName, samlRoleName, groupName), + Check: testAccCheckKeycloakGroupHasNoRoles("keycloak_group.group"), + }, + }, + }) +} + +func TestAccKeycloakGroupRoles_update(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + + realmRoleOneName := "terraform-role-" + acctest.RandString(10) + realmRoleTwoName := "terraform-role-" + acctest.RandString(10) + openIdClientName := "terraform-openid-client-" + acctest.RandString(10) + openIdRoleOneName := "terraform-role-" + acctest.RandString(10) + openIdRoleTwoName := "terraform-role-" + acctest.RandString(10) + samlClientName := "terraform-saml-client-" + acctest.RandString(10) + samlRoleOneName := "terraform-role-" + acctest.RandString(10) + samlRoleTwoName := "terraform-role-" + acctest.RandString(10) + groupName := "terraform-group-" + acctest.RandString(10) + + allRoleIds := []string{ + "${keycloak_role.realm_role_one.id}", + "${keycloak_role.realm_role_two.id}", + "${keycloak_role.openid_client_role_one.id}", + "${keycloak_role.openid_client_role_two.id}", + "${keycloak_role.saml_client_role_one.id}", + "${keycloak_role.saml_client_role_two.id}", + "${data.keycloak_role.offline_access.id}", + } + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + Steps: []resource.TestStep{ + // initial setup, resource is defined but no roles are specified + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, []string{}), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // add all roles + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, allRoleIds), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // remove some + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, []string{ + "${keycloak_role.realm_role_two.id}", + "${keycloak_role.openid_client_role_one.id}", + "${keycloak_role.openid_client_role_two.id}", + "${data.keycloak_role.offline_access.id}", + }), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // add some and remove some + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, []string{ + "${keycloak_role.saml_client_role_one.id}", + "${keycloak_role.saml_client_role_two.id}", + "${keycloak_role.realm_role_one.id}", + }), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // add some and remove some again + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, []string{ + "${keycloak_role.saml_client_role_one.id}", + "${keycloak_role.openid_client_role_two.id}", + "${keycloak_role.realm_role_two.id}", + "${data.keycloak_role.offline_access.id}", + }), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // add all back + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, allRoleIds), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // random scenario 1 + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, randomStringSliceSubset(allRoleIds)), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // random scenario 2 + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, randomStringSliceSubset(allRoleIds)), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // random scenario 3 + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, randomStringSliceSubset(allRoleIds)), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + // remove all + { + Config: testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, []string{}), + Check: testAccCheckKeycloakGroupHasRoles("keycloak_group_roles.group_roles"), + }, + }, + }) +} + +func flattenGroupRoles(keycloakClient *keycloak.KeycloakClient, group *keycloak.Group) ([]string, error) { + var roles []string + + for _, realmRole := range group.RealmRoles { + roles = append(roles, realmRole) + } + + for clientId, clientRoles := range group.ClientRoles { + client, err := keycloakClient.GetGenericClientByClientId(group.RealmId, clientId) + if err != nil { + return nil, err + } + + for _, clientRole := range clientRoles { + roles = append(roles, fmt.Sprintf("%s/%s", client.Id, clientRole)) + } + } + + return roles, nil +} + +func testAccCheckKeycloakGroupHasRoles(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + realm := rs.Primary.Attributes["realm_id"] + groupId := rs.Primary.Attributes["group_id"] + + var roles []*keycloak.Role + for k, v := range rs.Primary.Attributes { + if match, _ := regexp.MatchString("role_ids\\.[^#]+", k); !match { + continue + } + + role, err := keycloakClient.GetRole(realm, v) + if err != nil { + return err + } + + roles = append(roles, role) + } + + group, err := keycloakClient.GetGroup(realm, groupId) + if err != nil { + return err + } + + groupRoles, err := flattenGroupRoles(keycloakClient, group) + if err != nil { + return err + } + + if len(groupRoles) != len(roles) { + return fmt.Errorf("expected number of group roles to be %d, got %d", len(roles), len(groupRoles)) + } + + for _, role := range roles { + var expectedRoleString string + if role.ClientRole { + expectedRoleString = fmt.Sprintf("%s/%s", role.ClientId, role.Name) + } else { + expectedRoleString = role.Name + } + + found := false + + for _, groupRole := range groupRoles { + if groupRole == expectedRoleString { + found = true + break + } + } + + if !found { + return fmt.Errorf("expected to find role %s assigned to group %s", expectedRoleString, group.Name) + } + } + + return nil + } +} + +func testAccCheckKeycloakGroupHasNoRoles(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + + realm := rs.Primary.Attributes["realm_id"] + id := rs.Primary.ID + + group, err := keycloakClient.GetGroup(realm, id) + if err != nil { + return err + } + + if len(group.RealmRoles) != 0 || len(group.ClientRoles) != 0 { + return fmt.Errorf("expected group %s to have no roles", group.Name) + } + + return nil + } +} + +func testKeycloakGroupRoles_basic(realmName, openIdClientName, samlClientName, realmRoleName, openIdRoleName, samlRoleName, groupName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_saml_client" "saml_client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "realm_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "openid_client_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" +} + +resource "keycloak_role" "saml_client_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_saml_client.saml_client.id}" +} + +data "keycloak_role" "offline_access" { + realm_id = "${keycloak_realm.realm.id}" + name = "offline_access" +} + +resource "keycloak_group" "group" { + realm_id = "${keycloak_realm.realm.id}" + name = "%s" +} + +resource "keycloak_group_roles" "group_roles" { + realm_id = "${keycloak_realm.realm.id}" + group_id = "${keycloak_group.group.id}" + + role_ids = [ + "${keycloak_role.realm_role.id}", + "${keycloak_role.openid_client_role.id}", + "${keycloak_role.saml_client_role.id}", + "${data.keycloak_role.offline_access.id}", + ] +} + `, realmName, openIdClientName, samlClientName, realmRoleName, openIdRoleName, samlRoleName, groupName) +} + +func testKeycloakGroupRoles_noGroupRoles(realmName, openIdClientName, samlClientName, realmRoleName, openIdRoleName, samlRoleName, groupName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_saml_client" "saml_client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "realm_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "openid_client_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" +} + +resource "keycloak_role" "saml_client_role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_saml_client.saml_client.id}" +} + +data "keycloak_role" "offline_access" { + realm_id = "${keycloak_realm.realm.id}" + name = "offline_access" +} + +resource "keycloak_group" "group" { + realm_id = "${keycloak_realm.realm.id}" + name = "%s" +} + `, realmName, openIdClientName, samlClientName, realmRoleName, openIdRoleName, samlRoleName, groupName) +} + +func testKeycloakGroupRoles_update(realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName string, roleIds []string) string { + tfRoleIds := fmt.Sprintf("role_ids = %s", arrayOfStringsForTerraformResource(roleIds)) + + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_saml_client" "saml_client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "realm_role_one" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "realm_role_two" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "openid_client_role_one" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" +} + +resource "keycloak_role" "openid_client_role_two" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" +} + +resource "keycloak_role" "saml_client_role_one" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_saml_client.saml_client.id}" +} + +resource "keycloak_role" "saml_client_role_two" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_saml_client.saml_client.id}" +} + +data "keycloak_role" "offline_access" { + realm_id = "${keycloak_realm.realm.id}" + name = "offline_access" +} + +resource "keycloak_group" "group" { + realm_id = "${keycloak_realm.realm.id}" + name = "%s" +} + +resource "keycloak_group_roles" "group_roles" { + realm_id = "${keycloak_realm.realm.id}" + group_id = "${keycloak_group.group.id}" + + %s +} + `, realmName, openIdClientName, samlClientName, realmRoleOneName, realmRoleTwoName, openIdRoleOneName, openIdRoleTwoName, samlRoleOneName, samlRoleTwoName, groupName, tfRoleIds) +} diff --git a/provider/resource_keycloak_openid_hardcoded_role_protocol_mapper.go b/provider/resource_keycloak_openid_hardcoded_role_protocol_mapper.go new file mode 100644 index 000000000..9c35972f9 --- /dev/null +++ b/provider/resource_keycloak_openid_hardcoded_role_protocol_mapper.go @@ -0,0 +1,145 @@ +package provider + +import ( + "github.com/hashicorp/terraform/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func resourceKeycloakOpenIdHardcodedRoleProtocolMapper() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakOpenIdHardcodedRoleProtocolMapperCreate, + Read: resourceKeycloakOpenIdHardcodedRoleProtocolMapperRead, + Update: resourceKeycloakOpenIdHardcodedRoleProtocolMapperUpdate, + Delete: resourceKeycloakOpenIdHardcodedRoleProtocolMapperDelete, + Importer: &schema.ResourceImporter{ + // import a mapper tied to a client: + // {{realmId}}/client/{{clientId}}/{{protocolMapperId}} + // or a client scope: + // {{realmId}}/client-scope/{{clientScopeId}}/{{protocolMapperId}} + State: genericProtocolMapperImport, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "A human-friendly name that will appear in the Keycloak console.", + }, + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The realm id where the associated client or client scope exists.", + }, + "client_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The mapper's associated client. 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 mapper's associated client scope. Cannot be used at the same time as client_id.", + ConflictsWith: []string{"client_id"}, + }, + "role_id": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func mapFromDataToOpenIdHardcodedRoleProtocolMapper(data *schema.ResourceData) *keycloak.OpenIdHardcodedRoleProtocolMapper { + return &keycloak.OpenIdHardcodedRoleProtocolMapper{ + Id: data.Id(), + Name: data.Get("name").(string), + 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), + } +} + +func mapFromOpenIdHardcodedRoleMapperToData(mapper *keycloak.OpenIdHardcodedRoleProtocolMapper, data *schema.ResourceData) { + data.SetId(mapper.Id) + data.Set("name", mapper.Name) + data.Set("realm_id", mapper.RealmId) + + if mapper.ClientId != "" { + data.Set("client_id", mapper.ClientId) + } else { + data.Set("client_scope_id", mapper.ClientScopeId) + } + + data.Set("role_id", mapper.RoleId) +} + +func resourceKeycloakOpenIdHardcodedRoleProtocolMapperCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + openIdHardcodedRoleMapper := mapFromDataToOpenIdHardcodedRoleProtocolMapper(data) + + err := keycloakClient.ValidateOpenIdHardcodedRoleProtocolMapper(openIdHardcodedRoleMapper) + if err != nil { + return err + } + + err = keycloakClient.NewOpenIdHardcodedRoleProtocolMapper(openIdHardcodedRoleMapper) + if err != nil { + return err + } + + mapFromOpenIdHardcodedRoleMapperToData(openIdHardcodedRoleMapper, data) + + return resourceKeycloakOpenIdHardcodedRoleProtocolMapperRead(data, meta) +} + +func resourceKeycloakOpenIdHardcodedRoleProtocolMapperRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + openIdHardcodedRoleMapper, err := keycloakClient.GetOpenIdHardcodedRoleProtocolMapper(realmId, clientId, clientScopeId, data.Id()) + if err != nil { + return handleNotFoundError(err, data) + } + + mapFromOpenIdHardcodedRoleMapperToData(openIdHardcodedRoleMapper, data) + + return nil +} + +func resourceKeycloakOpenIdHardcodedRoleProtocolMapperUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + openIdHardcodedRoleMapper := mapFromDataToOpenIdHardcodedRoleProtocolMapper(data) + + err := keycloakClient.ValidateOpenIdHardcodedRoleProtocolMapper(openIdHardcodedRoleMapper) + if err != nil { + return err + } + + err = keycloakClient.UpdateOpenIdHardcodedRoleProtocolMapper(openIdHardcodedRoleMapper) + if err != nil { + return err + } + + return resourceKeycloakOpenIdHardcodedRoleProtocolMapperRead(data, meta) +} + +func resourceKeycloakOpenIdHardcodedRoleProtocolMapperDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + clientId := data.Get("client_id").(string) + clientScopeId := data.Get("client_scope_id").(string) + + return keycloakClient.DeleteOpenIdHardcodedRoleProtocolMapper(realmId, clientId, clientScopeId, data.Id()) +} diff --git a/provider/resource_keycloak_openid_hardcoded_role_protocol_mapper_test.go b/provider/resource_keycloak_openid_hardcoded_role_protocol_mapper_test.go new file mode 100644 index 000000000..1a637091c --- /dev/null +++ b/provider/resource_keycloak_openid_hardcoded_role_protocol_mapper_test.go @@ -0,0 +1,420 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestAccKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_client(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + role := "terraform-role-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-hardcoded-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper_client" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdHardcodedRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_client(realmName, role, clientId, mapperName), + Check: testKeycloakOpenIdHardcodedRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdHardcodedRoleProtocolMapper_basicClientRole_client(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + clientIdForRole := "terraform-client-" + acctest.RandString(10) + role := "terraform-role-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-hardcoded-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper_client" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdHardcodedRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_basicClientRole_client(realmName, clientIdForRole, role, clientId, mapperName), + Check: testKeycloakOpenIdHardcodedRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_clientScope(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + role := "terraform-role-" + acctest.RandString(10) + clientScopeId := "terraform-client-scope-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-hardcoded-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdHardcodedRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_clientScope(realmName, role, clientScopeId, mapperName), + Check: testKeycloakOpenIdHardcodedRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdHardcodedRoleProtocolMapper_import(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + role := "terraform-role-" + acctest.RandString(10) + clientId := "terraform-openid-client-" + acctest.RandString(10) + clientScopeId := "terraform-client-scope-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-hardcoded-role-mapper-" + acctest.RandString(5) + + clientResourceName := "keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper_client" + clientScopeResourceName := "keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper_client_scope" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdFullNameProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_import(realmName, role, clientId, clientScopeId, mapperName), + Check: resource.ComposeTestCheckFunc( + testKeycloakOpenIdHardcodedRoleProtocolMapperExists(clientResourceName), + testKeycloakOpenIdHardcodedRoleProtocolMapperExists(clientScopeResourceName), + ), + }, + { + ResourceName: clientResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClient(clientResourceName), + }, + { + ResourceName: clientScopeResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getGenericProtocolMapperIdForClientScope(clientScopeResourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdHardcodedRoleProtocolMapper_update(t *testing.T) { + realmName := "terraform-realm-" + acctest.RandString(10) + roleOne := "terraform-role-" + acctest.RandString(10) + roleTwo := "terraform-role-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-hardcoded-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper_client" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdHardcodedRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_clientUpdateBefore(realmName, roleOne, roleTwo, clientId, mapperName), + Check: testKeycloakOpenIdHardcodedRoleProtocolMapperExists(resourceName), + }, + { + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_clientUpdateAfter(realmName, roleOne, roleTwo, clientId, mapperName), + Check: testKeycloakOpenIdHardcodedRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func TestAccKeycloakOpenIdHardcodedRoleProtocolMapper_createAfterManualDestroy(t *testing.T) { + var mapper = &keycloak.OpenIdHardcodedRoleProtocolMapper{} + + realmName := "terraform-realm-" + acctest.RandString(10) + role := "terraform-role-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + mapperName := "terraform-openid-connect-hardcoded-role-mapper-" + acctest.RandString(5) + + resourceName := "keycloak_openid_hardcoded_role_protocol_mapper.hardcoded_role_mapper_client" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccKeycloakOpenIdHardcodedRoleProtocolMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_client(realmName, role, clientId, mapperName), + Check: testKeycloakOpenIdHardcodedRoleProtocolMapperFetch(resourceName, mapper), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteOpenIdHardcodedRoleProtocolMapper(mapper.RealmId, mapper.ClientId, mapper.ClientScopeId, mapper.Id) + if err != nil { + t.Error(err) + } + }, + Config: testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_client(realmName, role, clientId, mapperName), + Check: testKeycloakOpenIdHardcodedRoleProtocolMapperExists(resourceName), + }, + }, + }) +} + +func testAccKeycloakOpenIdHardcodedRoleProtocolMapperDestroy() resource.TestCheckFunc { + return func(state *terraform.State) error { + for resourceName, rs := range state.RootModule().Resources { + if rs.Type != "keycloak_openid_hardcoded_role_protocol_mapper" { + continue + } + + mapper, _ := getHardcodedRoleMapperUsingState(state, resourceName) + + if mapper != nil { + return fmt.Errorf("openid user attribute protocol mapper with id %s still exists", rs.Primary.ID) + } + } + + return nil + } +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapperExists(resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + _, err := getHardcodedRoleMapperUsingState(state, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapperFetch(resourceName string, mapper *keycloak.OpenIdHardcodedRoleProtocolMapper) resource.TestCheckFunc { + return func(state *terraform.State) error { + fetchedMapper, err := getHardcodedRoleMapperUsingState(state, resourceName) + if err != nil { + return err + } + + mapper.Id = fetchedMapper.Id + mapper.ClientId = fetchedMapper.ClientId + mapper.ClientScopeId = fetchedMapper.ClientScopeId + mapper.RealmId = fetchedMapper.RealmId + + return nil + } +} + +func getHardcodedRoleMapperUsingState(state *terraform.State, resourceName string) (*keycloak.OpenIdHardcodedRoleProtocolMapper, error) { + rs, ok := state.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found in TF state: %s ", resourceName) + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + clientId := rs.Primary.Attributes["client_id"] + clientScopeId := rs.Primary.Attributes["client_scope_id"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + return keycloakClient.GetOpenIdHardcodedRoleProtocolMapper(realm, clientId, clientScopeId, id) +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_client(realmName, role, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + role_id = "${keycloak_role.role.id}" +}`, realmName, role, clientId, mapperName) +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_clientScope(realmName, role, clientScopeId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + role_id = "${keycloak_role.role.id}" +}`, realmName, role, clientScopeId, mapperName) +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapper_import(realmName, role, clientId, clientScopeId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + + role_id = "${keycloak_role.role.id}" +} + +resource "keycloak_openid_client_scope" "client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper_client_scope" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_scope_id = "${keycloak_openid_client_scope.client_scope.id}" + + role_id = "${keycloak_role.role.id}" +}`, realmName, role, clientId, mapperName, clientScopeId, mapperName) +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_clientUpdateBefore(realmName, roleOne, roleTwo, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_role" "role_one" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "role_two" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + role_id = "${keycloak_role.role_one.id}" +}`, realmName, roleOne, roleTwo, clientId, mapperName) +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapper_basicRealmRole_clientUpdateAfter(realmName, roleOne, roleTwo, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_role" "role_one" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "role_two" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + role_id = "${keycloak_role.role_two.id}" +}`, realmName, roleOne, roleTwo, clientId, mapperName) +} + +func testKeycloakOpenIdHardcodedRoleProtocolMapper_basicClientRole_client(realmName, clientIdForRole, role, clientId, mapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "openid_client_for_role" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client_for_role.id}" +} + +resource "keycloak_openid_client" "openid_client" { + realm_id = "${keycloak_realm.realm.id}" + client_id = "%s" + + access_type = "BEARER-ONLY" +} + +resource "keycloak_openid_hardcoded_role_protocol_mapper" "hardcoded_role_mapper_client" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.openid_client.id}" + role_id = "${keycloak_role.role.id}" +}`, realmName, clientIdForRole, role, clientId, mapperName) +} diff --git a/provider/resource_keycloak_role.go b/provider/resource_keycloak_role.go new file mode 100644 index 000000000..fbc3a60b3 --- /dev/null +++ b/provider/resource_keycloak_role.go @@ -0,0 +1,234 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "strings" +) + +func resourceKeycloakRole() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakRoleCreate, + Read: resourceKeycloakRoleRead, + Delete: resourceKeycloakRoleDelete, + Update: resourceKeycloakRoleUpdate, + // This resource can be imported using {{realm}}/{{roleId}}. The role's ID (a GUID) can be found in the URL when viewing the role + Importer: &schema.ResourceImporter{ + State: resourceKeycloakRoleImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "client_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "composite_roles": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + MinItems: 1, + Set: schema.HashString, + Optional: true, + }, + }, + } +} + +func mapFromDataToRole(data *schema.ResourceData) *keycloak.Role { + role := &keycloak.Role{ + Id: data.Id(), + RealmId: data.Get("realm_id").(string), + ClientId: data.Get("client_id").(string), + Name: data.Get("name").(string), + Description: data.Get("description").(string), + } + + return role +} + +func mapFromRoleToData(data *schema.ResourceData, role *keycloak.Role) { + data.SetId(role.Id) + + data.Set("realm_id", role.RealmId) + data.Set("client_id", role.ClientId) + data.Set("name", role.Name) + data.Set("description", role.Description) +} + +func resourceKeycloakRoleCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + role := mapFromDataToRole(data) + + var compositeRoles []*keycloak.Role + if v, ok := data.GetOk("composite_roles"); ok { + compositeRolesTf := v.(*schema.Set).List() + + for _, compositeRoleId := range compositeRolesTf { + compositeRoleToAdd, err := keycloakClient.GetRole(role.RealmId, compositeRoleId.(string)) + if err != nil { + return err + } + + compositeRoles = append(compositeRoles, compositeRoleToAdd) + } + + if len(compositeRoles) != 0 { // technically you can still specify composite_roles = [] in HCL + role.Composite = true + } + } + + err := keycloakClient.CreateRole(role) + if err != nil { + return err + } + + if role.Composite { + err = keycloakClient.AddCompositesToRole(role, compositeRoles) + if err != nil { + return err + } + } + + mapFromRoleToData(data, role) + + return resourceKeycloakRoleRead(data, meta) +} + +func resourceKeycloakRoleRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + role, err := keycloakClient.GetRole(realmId, id) + if err != nil { + return handleNotFoundError(err, data) + } + + mapFromRoleToData(data, role) + + if role.Composite { + composites, err := keycloakClient.GetRoleComposites(role) + if err != nil { + return err + } + + var compositeRoleIds []string + + for _, composite := range composites { + compositeRoleIds = append(compositeRoleIds, composite.Id) + } + + data.Set("composite_roles", compositeRoleIds) + } + + return nil +} + +func resourceKeycloakRoleUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + role := mapFromDataToRole(data) + + err := keycloakClient.UpdateRole(role) + if err != nil { + return err + } + + keycloakComposites, err := keycloakClient.GetRoleComposites(role) + if err != nil { + return err + } + + if v, ok := data.GetOk("composite_roles"); ok { + tfCompositeIds := v.(*schema.Set) + var keycloakCompositesToRemove []*keycloak.Role + + // get a list of all composites to remove and all composites to add + for _, keycloakComposite := range keycloakComposites { + if tfCompositeIds.Contains(keycloakComposite.Id) { + // if the composite exists in keycloak and tf state, we can remove them from the local list because this role does not need to be added + tfCompositeIds.Remove(keycloakComposite.Id) + } else { + // if the composite exists in keycloak but not tf state, it needs to be removed on keycloak's side + keycloakCompositesToRemove = append(keycloakCompositesToRemove, keycloakComposite) + } + } + + // at this point we have two slices: + // `keycloakCompositesToRemove` should be removed from the role's list of composites + // `tfCompositeIds` should be added to the role's list of composites. all of the roles that exist on both sides have already been removed + + if len(keycloakCompositesToRemove) != 0 { + err = keycloakClient.RemoveCompositesFromRole(role, keycloakCompositesToRemove) + if err != nil { + return err + } + } + + if tfCompositeIds.Len() != 0 { + var compositesToAdd []*keycloak.Role + for _, tfCompositeId := range tfCompositeIds.List() { + compositeToAdd, err := keycloakClient.GetRole(role.RealmId, tfCompositeId.(string)) + if err != nil { + return err + } + + compositesToAdd = append(compositesToAdd, compositeToAdd) + } + + err = keycloakClient.AddCompositesToRole(role, compositesToAdd) + if err != nil { + return err + } + } + } else { + // the user wants this role to have zero composites. if there are composites attached, remove them + if len(keycloakComposites) != 0 { + err = keycloakClient.RemoveCompositesFromRole(role, keycloakComposites) + if err != nil { + return err + } + } + } + + mapFromRoleToData(data, role) + + return nil +} + +func resourceKeycloakRoleDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + return keycloakClient.DeleteRole(realmId, id) +} + +func resourceKeycloakRoleImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid import. Supported import format: {{realm}}/{{roleId}}.") + } + + d.Set("realm_id", parts[0]) + d.SetId(parts[1]) + + return []*schema.ResourceData{d}, nil +} diff --git a/provider/resource_keycloak_role_test.go b/provider/resource_keycloak_role_test.go new file mode 100644 index 000000000..dc8048e6b --- /dev/null +++ b/provider/resource_keycloak_role_test.go @@ -0,0 +1,505 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestAccKeycloakRole_basicRealm(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + roleName := "terraform-role-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRole_basicRealm(realmName, roleName), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + { + ResourceName: "keycloak_role.role", + ImportState: true, + ImportStateVerify: true, + ImportStateIdPrefix: realmName + "/", + }, + }, + }) +} + +func TestAccKeycloakRole_basicClient(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + roleName := "terraform-role-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRole_basicClient(realmName, clientId, roleName), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + { + ResourceName: "keycloak_role.role", + ImportState: true, + ImportStateVerify: true, + ImportStateIdPrefix: realmName + "/", + }, + }, + }) +} + +func TestAccKeycloakRole_basicSamlClient(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + roleName := "terraform-role-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRole_basicSamlClient(realmName, clientId, roleName), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + { + ResourceName: "keycloak_role.role", + ImportState: true, + ImportStateVerify: true, + ImportStateIdPrefix: realmName + "/", + }, + }, + }) +} + +func TestAccKeycloakRole_basicRealmUpdate(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + roleName := "terraform-role-" + acctest.RandString(10) + descriptionOne := acctest.RandString(50) + descriptionTwo := acctest.RandString(50) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRole_basicRealmWithDescription(realmName, roleName, descriptionOne), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + { + Config: testKeycloakRole_basicRealmWithDescription(realmName, roleName, descriptionTwo), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + { + Config: testKeycloakRole_basicRealm(realmName, roleName), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + }, + }) +} + +func TestAccKeycloakRole_basicClientUpdate(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + clientId := "terraform-client-" + acctest.RandString(10) + roleName := "terraform-role-" + acctest.RandString(10) + descriptionOne := acctest.RandString(50) + descriptionTwo := acctest.RandString(50) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRole_basicClientWithDescription(realmName, clientId, roleName, descriptionOne), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + { + Config: testKeycloakRole_basicClientWithDescription(realmName, clientId, roleName, descriptionTwo), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + { + Config: testKeycloakRole_basicClient(realmName, clientId, roleName), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + }, + }) +} + +func TestAccKeycloakRole_createAfterManualDestroy(t *testing.T) { + var role = &keycloak.Role{} + + realmName := "terraform-" + acctest.RandString(10) + roleName := "terraform-role-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakRole_basicRealm(realmName, roleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakRoleExists("keycloak_role.role"), + testAccCheckKeycloakRoleFetch("keycloak_role.role", role), + ), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteRole(role.RealmId, role.Id) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakRole_basicRealm(realmName, roleName), + Check: testAccCheckKeycloakRoleExists("keycloak_role.role"), + }, + }, + }) +} + +func TestAccKeycloakRole_composites(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + clientOne := "terraform-client-" + acctest.RandString(10) + clientTwo := "terraform-client-" + acctest.RandString(10) + roleOne := "terraform-role-one-" + acctest.RandString(10) + roleTwo := "terraform-role-two-" + acctest.RandString(10) + roleThree := "terraform-role-three-" + acctest.RandString(10) + roleFour := "terraform-role-four-" + acctest.RandString(10) + roleWithComposites := "terraform-role-with-composites-" + acctest.RandString(10) + roleWithCompositesResourceName := "keycloak_role.role_with_composites" + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakRoleDestroy(), + Steps: []resource.TestStep{ + // initial setup - no composites attached + { + Config: testKeycloakRole_composites(realmName, clientOne, clientTwo, roleOne, roleTwo, roleThree, roleFour, roleWithComposites, []string{}), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakRoleExists("keycloak_role.role_1"), + testAccCheckKeycloakRoleExists("keycloak_role.role_2"), + testAccCheckKeycloakRoleExists("keycloak_role.role_3"), + testAccCheckKeycloakRoleExists("keycloak_role.role_with_composites"), + testAccCheckKeycloakRoleHasComposites(roleWithCompositesResourceName, []string{}), + ), + }, + // add all composites + { + Config: testKeycloakRole_composites(realmName, clientOne, clientTwo, roleOne, roleTwo, roleThree, roleFour, roleWithComposites, []string{ + "${keycloak_role.role_1.id}", + "${keycloak_role.role_2.id}", + "${keycloak_role.role_3.id}", + "${keycloak_role.role_4.id}", + }), + Check: testAccCheckKeycloakRoleHasComposites(roleWithCompositesResourceName, []string{ + roleOne, + roleTwo, + roleThree, + roleFour, + }), + }, + // remove two composites + { + Config: testKeycloakRole_composites(realmName, clientOne, clientTwo, roleOne, roleTwo, roleThree, roleFour, roleWithComposites, []string{ + "${keycloak_role.role_1.id}", + "${keycloak_role.role_2.id}", + }), + Check: testAccCheckKeycloakRoleHasComposites(roleWithCompositesResourceName, []string{ + roleOne, + roleTwo, + }), + }, + // add them back and remove the others + { + Config: testKeycloakRole_composites(realmName, clientOne, clientTwo, roleOne, roleTwo, roleThree, roleFour, roleWithComposites, []string{ + "${keycloak_role.role_3.id}", + "${keycloak_role.role_4.id}", + }), + Check: testAccCheckKeycloakRoleHasComposites(roleWithCompositesResourceName, []string{ + roleThree, + roleFour, + }), + }, + // remove them all + { + Config: testKeycloakRole_composites(realmName, clientOne, clientTwo, roleOne, roleTwo, roleThree, roleFour, roleWithComposites, []string{}), + Check: testAccCheckKeycloakRoleHasComposites(roleWithCompositesResourceName, []string{}), + }, + }, + }) +} + +func testAccCheckKeycloakRoleExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, err := getRoleFromState(s, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckKeycloakRoleDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_role" { + continue + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + role, _ := keycloakClient.GetRole(realm, id) + if role != nil { + return fmt.Errorf("role with id %s still exists", id) + } + } + + return nil + } +} + +func testAccCheckKeycloakRoleFetch(resourceName string, role *keycloak.Role) resource.TestCheckFunc { + return func(state *terraform.State) error { + fetchedRole, err := getRoleFromState(state, resourceName) + if err != nil { + return err + } + + role.Id = fetchedRole.Id + role.Name = fetchedRole.Name + role.RealmId = fetchedRole.RealmId + + return nil + } +} + +func testAccCheckKeycloakRoleHasComposites(resourceName string, compositeRoleNames []string) resource.TestCheckFunc { + return func(state *terraform.State) error { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + role, err := getRoleFromState(state, resourceName) + if err != nil { + return err + } + + if len(compositeRoleNames) != 0 && !role.Composite { + return fmt.Errorf("expected role %s to have composites, but has none", role.Name) + } + + if len(compositeRoleNames) == 0 && role.Composite { + return fmt.Errorf("expected role %s to have no composites, but has some", role.Name) + } + + composites, err := keycloakClient.GetRoleComposites(role) + if err != nil { + return err + } + + for _, compositeRoleName := range compositeRoleNames { + var found bool + + for _, composite := range composites { + if composite.Name == compositeRoleName { + found = true + } + } + + if !found { + return fmt.Errorf("expected role %s to have composite %s", role.Name, compositeRoleName) + } + } + + for _, composite := range composites { + var found bool + + for _, compositeRoleName := range compositeRoleNames { + if composite.Name == compositeRoleName { + found = true + } + } + + if !found { + return fmt.Errorf("role %s had unexpected composite %s", role.Name, composite.Name) + } + } + + return nil + } +} + +func getRoleFromState(s *terraform.State, resourceName string) (*keycloak.Role, 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"] + + role, err := keycloakClient.GetRole(realm, id) + if err != nil { + return nil, fmt.Errorf("error getting role with id %s: %s", id, err) + } + + return role, nil +} + +func testKeycloakRole_basicRealm(realm, role string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + `, realm, role) +} + +func testKeycloakRole_basicRealmWithDescription(realm, role, description string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_role" "role" { + name = "%s" + description = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + `, realm, role, description) +} + +func testKeycloakRole_basicClient(realm, clientId, role string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.client.id}" +} + `, realm, clientId, role) +} + +func testKeycloakRole_basicSamlClient(realm, clientId, role string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_saml_client" "client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_saml_client.client.id}" +} + `, realm, clientId, role) +} + +func testKeycloakRole_basicClientWithDescription(realm, clientId, role, description string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "client" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_role" "role" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.client.id}" + description = "%s" +} + `, realm, clientId, role, description) +} + +func testKeycloakRole_composites(realm, clientOne, clientTwo, roleOne, roleTwo, roleThree, roleFour, roleWithComposites string, composites []string) string { + var tfComposites string + if len(composites) != 0 { + tfComposites = fmt.Sprintf("composite_roles = %s", arrayOfStringsForTerraformResource(composites)) + } + + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_openid_client" "client_one" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_openid_client" "client_two" { + client_id = "%s" + realm_id = "${keycloak_realm.realm.id}" + access_type = "CONFIDENTIAL" +} + +resource "keycloak_role" "role_1" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "role_2" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.client_one.id}" +} + +resource "keycloak_role" "role_3" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" +} + +resource "keycloak_role" "role_4" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + client_id = "${keycloak_openid_client.client_two.id}" +} + +resource "keycloak_role" "role_with_composites" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + + %s +} + `, realm, clientOne, clientTwo, roleOne, roleTwo, roleThree, roleFour, roleWithComposites, tfComposites) +} diff --git a/provider/resource_keycloak_user_test.go b/provider/resource_keycloak_user_test.go index e7544b42d..aff21324f 100644 --- a/provider/resource_keycloak_user_test.go +++ b/provider/resource_keycloak_user_test.go @@ -19,7 +19,7 @@ func TestAccKeycloakUser_basic(t *testing.T) { realmName := "terraform-" + acctest.RandString(10) username := "terraform-user-" + acctest.RandString(10) attributeName := "terraform-attribute-" + acctest.RandString(10) - attributeValue := acctest.RandString(300) + attributeValue := acctest.RandString(250) resourceName := "keycloak_user.user" @@ -72,7 +72,7 @@ func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) { realmName := "terraform-" + acctest.RandString(10) username := "terraform-user-" + acctest.RandString(10) attributeName := "terraform-attribute-" + acctest.RandString(10) - attributeValue := acctest.RandString(300) + attributeValue := acctest.RandString(250) resourceName := "keycloak_user.user" resource.Test(t, resource.TestCase{ @@ -138,7 +138,7 @@ func TestAccKeycloakUser_updateUsername(t *testing.T) { usernameOne := "terraform-user-" + acctest.RandString(10) usernameTwo := "terraform-user-" + acctest.RandString(10) attributeName := "terraform-attribute-" + acctest.RandString(10) - attributeValue := acctest.RandString(300) + attributeValue := acctest.RandString(250) resourceName := "keycloak_user.user" @@ -242,7 +242,7 @@ func TestAccKeycloakUser_unsetOptionalAttributes(t *testing.T) { Enabled: randomBool(), Attributes: map[string][]string{ attributeName: { - acctest.RandString(255), + acctest.RandString(230), acctest.RandString(12), }, }, @@ -276,7 +276,7 @@ func TestAccKeycloakUser_validateLowercaseUsernames(t *testing.T) { realmName := "terraform-" + acctest.RandString(10) username := "terraform-user-" + strings.ToUpper(acctest.RandString(10)) attributeName := "terraform-attribute-" + acctest.RandString(10) - attributeValue := acctest.RandString(300) + attributeValue := acctest.RandString(250) resource.Test(t, resource.TestCase{ Providers: testAccProviders, diff --git a/provider/test_utils.go b/provider/test_utils.go index e8d4e8c9c..8b97fcbb3 100644 --- a/provider/test_utils.go +++ b/provider/test_utils.go @@ -21,6 +21,18 @@ func randomStringInSlice(slice []string) string { return slice[acctest.RandIntRange(0, len(slice)-1)] } +func randomStringSliceSubset(slice []string) []string { + var result []string + + for _, s := range slice { + if randomBool() { + result = append(result, s) + } + } + + return result +} + // Returns a slice of strings in the format ["foo", "bar"] for // use within terraform resource definitions for acceptance tests func arrayOfStringsForTerraformResource(parts []string) string {