From 731c4cc36d3b2b895640e40003c037a12327ffda Mon Sep 17 00:00:00 2001 From: tomrutsaert Date: Mon, 29 Jun 2020 08:35:11 +0200 Subject: [PATCH] resource to enable the token exchange idp permission (#318) * resource to enable the token excahnge idp permission + auto create of client policy * fmt * Update provider/resource_keycloak_identity_provider_token_exchange_scope_permission.go Co-authored-by: Michael Parker * improved docs+ IsError409 helper method + better policy_type validation method + improved policy name creator logic * fmt Co-authored-by: Tom Rutsaert Co-authored-by: Michael Parker --- docker-compose.yml | 2 +- ...rovider_token_exchange_scope_permission.md | 88 ++++ example/external_token_exchange_example.tf | 57 +++ keycloak/error.go | 6 + keycloak/identity_provider_permissions.go | 46 ++ provider/provider.go | 127 ++--- ...rovider_token_exchange_scope_permission.go | 297 ++++++++++++ ...er_token_exchange_scope_permission_test.go | 447 ++++++++++++++++++ 8 files changed, 1006 insertions(+), 64 deletions(-) create mode 100644 docs/resources/keycloak_identity_provider_token_exchange_scope_permission.md create mode 100644 example/external_token_exchange_example.tf create mode 100644 keycloak/identity_provider_permissions.go create mode 100644 provider/resource_keycloak_identity_provider_token_exchange_scope_permission.go create mode 100644 provider/resource_keycloak_identity_provider_token_exchange_scope_permission_test.go diff --git a/docker-compose.yml b/docker-compose.yml index e0bc80804..7088737c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: - 8389:389 keycloak: image: jboss/keycloak:10.0.2 - command: -b 0.0.0.0 -Dkeycloak.profile.feature.upload_scripts=enabled + command: -b 0.0.0.0 -Dkeycloak.profile.feature.upload_scripts=enabled -Dkeycloak.profile.feature.admin_fine_grained_authz=enabled -Dkeycloak.profile.feature.token_exchange=enabled depends_on: - postgres - openldap diff --git a/docs/resources/keycloak_identity_provider_token_exchange_scope_permission.md b/docs/resources/keycloak_identity_provider_token_exchange_scope_permission.md new file mode 100644 index 000000000..2fb6cd856 --- /dev/null +++ b/docs/resources/keycloak_identity_provider_token_exchange_scope_permission.md @@ -0,0 +1,88 @@ +# keycloak_identity_provider_token_exchange_scope_permission + +Allows you to manage Identity Provider "Token exchange" Scope Based Permissions. + +This is part of a preview keycloak feature. You need to enable this feature to be able to use this resource. +More information about enabling the preview feature can be found here: https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange + +When enabling Identity Provider Permissions, Keycloak does several things automatically: +1. Enable Authorization on build-in realm-management client +1. Create a "token-exchange" scope +1. Create a resource representing the identity provider +1. Create a scope based permission for the "token-exchange" scope and identity provider resource + +The only thing that is missing is a policy set on the permission. +As the policy lives within the context of the realm-management client, you cannot create a policy resource and link to from with your _.tf_ file. This would also cause an implicit cycle dependency. +Thus, the only way to manage this in terraform is to create and manage the policy internally from within this terraform resource itself. +At the moment only a client policy type is supported. The client policy will automatically be created for the clients parameter. + +### Example Usage + +```hcl +resource "keycloak_realm" "token-exchange_realm" { + realm = "token-exchange_destination_realm" + enabled = true +} + +resource keycloak_oidc_identity_provider token-exchange_my_oidc_idp { + realm = keycloak_realm.token-exchange_realm.id + alias = "myIdp" + authorization_url = "http://localhost:8080/auth/realms/someRealm/protocol/openid-connect/auth" + token_url = "http://localhost:8080/auth/realms/someRealm/protocol/openid-connect/token" + client_id = "clientId" + client_secret = "secret" + default_scopes = "openid" +} + +resource "keycloak_openid_client" "token-exchange_webapp_client" { + realm_id = keycloak_realm.token-exchange_realm.id + name = "webapp_client" + client_id = "webapp_client" + client_secret = "secret" + description = "a webapp client on the destination realm" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + valid_redirect_uris = [ + "http://localhost:8080/*", + ] +} + +//relevant part +resource "keycloak_identity_provider_token_exchange_scope_permission" "oidc_idp_permission" { + realm_id = keycloak_realm.token-exchange_realm.id + provider_alias = keycloak_oidc_identity_provider.token-exchange_my_oidc_idp.alias + policy_type = "client" + clients = [keycloak_openid_client.token-exchange_webapp_client.id] +} +``` + +### Argument Reference + +The following arguments are supported: + +- `realm_id` - (Required) The realm this group exists in. +- `provider_alias` - (Required) Alias of the identity provider. +- `policy_type` - (Optional) Defaults to "client" This is also the only value policy type supported by this provider. +- `clients` - (Required) Ids of the clients for which a policy will be created and set on scope based token exchange permission. + +### Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +- `policy_id` - Policy id that will be set on the scope based token exchange permission automatically created by enabling permissions on the reference identity provider. +- `authorization_resource_server_id` - Resource server id representing the realm management client on which this permission is managed. +- `authorization_idp_resource_id` - Resource id representing the identity provider, this automatically created by keycloak. +- `authorization_token_exchange_scope_permission_id` - Permission id representing the Permission with scope 'Token Exchange' and the resource 'authorization_idp_resource_id', this automatically created by keycloak, the policy id will be set on this permission. + + +### Import + +This resource can be imported using the format +`{{realm_id}}/{{provider_alias}}`, where `provider_alias` is the alias that you assign to the identity provider upon creation. + +Example: + +```bash +$ terraform import keycloak_identity_provider_token_exchange_scope_permission.my_permission my-realm/my_idp +``` + diff --git a/example/external_token_exchange_example.tf b/example/external_token_exchange_example.tf new file mode 100644 index 000000000..86e3f2995 --- /dev/null +++ b/example/external_token_exchange_example.tf @@ -0,0 +1,57 @@ +resource "keycloak_realm" "token-exchange_source_realm" { + realm = "token-exchange_source_realm" + enabled = true +} + +resource "keycloak_openid_client" "token-exchange_destination_client" { + realm_id = keycloak_realm.token-exchange_source_realm.id + name = "destination_client" + client_id = "destination_client" + client_secret = "secret" + description = "a client used by the destination realm" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + valid_redirect_uris = [ + "http://localhost:8080/*", + ] +} + +resource "keycloak_realm" "token-exchange_destination_realm" { + realm = "token-exchange_destination_realm" + enabled = true +} + +resource keycloak_oidc_identity_provider token-exchange_source_oidc_idp { + realm = keycloak_realm.token-exchange_destination_realm.id + alias = "source" + authorization_url = "http://localhost:8080/auth/realms/${keycloak_realm.token-exchange_source_realm.id}/protocol/openid-connect/auth" + token_url = "http://localhost:8080/auth/realms/${keycloak_realm.token-exchange_source_realm.id}/protocol/openid-connect/token" + user_info_url = "http://localhost:8080/auth/realms/${keycloak_realm.token-exchange_source_realm.id}/protocol/openid-connect/userinfo" + jwks_url = "http://localhost:8080/auth/realms/${keycloak_realm.token-exchange_source_realm.id}/protocol/openid-connect/certs" + validate_signature = true + client_id = keycloak_openid_client.token-exchange_destination_client.client_id + client_secret = keycloak_openid_client.token-exchange_destination_client.client_secret + default_scopes = "openid" +} + +resource "keycloak_openid_client" "token-exchange_webapp_client" { + realm_id = keycloak_realm.token-exchange_destination_realm.id + name = "webapp_client" + client_id = "webapp_client" + client_secret = "secret" + description = "a webapp client on the destination realm" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + valid_redirect_uris = [ + "http://localhost:8080/*", + ] +} + +//token exchange feature enabler +resource "keycloak_identity_provider_token_exchange_scope_permission" "source_oidc_idp_permission" { + realm_id = keycloak_realm.token-exchange_destination_realm.id + provider_alias = keycloak_oidc_identity_provider.token-exchange_source_oidc_idp.alias + policy_type = "client" + clients = [keycloak_openid_client.token-exchange_webapp_client.id] +} + diff --git a/keycloak/error.go b/keycloak/error.go index 23bf7b310..db89f04ec 100644 --- a/keycloak/error.go +++ b/keycloak/error.go @@ -19,3 +19,9 @@ func ErrorIs404(err error) bool { return ok && keycloakError != nil && keycloakError.Code == http.StatusNotFound } + +func ErrorIs409(err error) bool { + keycloakError, ok := errwrap.GetType(err, &ApiError{}).(*ApiError) + + return ok && keycloakError != nil && keycloakError.Code == http.StatusConflict +} diff --git a/keycloak/identity_provider_permissions.go b/keycloak/identity_provider_permissions.go new file mode 100644 index 000000000..63458cb07 --- /dev/null +++ b/keycloak/identity_provider_permissions.go @@ -0,0 +1,46 @@ +package keycloak + +import ( + "fmt" +) + +type IdentityProviderPermissionsInput struct { + Enabled bool `json:"enabled"` +} + +type IdentityProviderPermissions struct { + RealmId string `json:"-"` + ProviderAlias string `json:"-"` + Enabled bool `json:"enabled"` + Resource string `json:"resource"` + ScopePermissions map[string]interface{} `json:"scopePermissions"` +} + +func (keycloakClient *KeycloakClient) EnableIdentityProviderPermissions(realmId, providerAlias string) error { + return keycloakClient.put(fmt.Sprintf("/realms/%s/identity-provider/instances/%s/management/permissions", realmId, providerAlias), IdentityProviderPermissionsInput{Enabled: true}) +} + +func (keycloakClient *KeycloakClient) DisableIdentityProviderPermissions(realmId, providerAlias string) error { + return keycloakClient.put(fmt.Sprintf("/realms/%s/identity-provider/instances/%s/management/permissions", realmId, providerAlias), IdentityProviderPermissionsInput{Enabled: false}) +} + +func (keycloakClient *KeycloakClient) GetIdentityProviderPermissions(realmId, providerAlias string) (*IdentityProviderPermissions, error) { + var identityProviderPermissions IdentityProviderPermissions + identityProviderPermissions.RealmId = realmId + identityProviderPermissions.ProviderAlias = providerAlias + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/identity-provider/instances/%s/management/permissions", realmId, providerAlias), &identityProviderPermissions, nil) + if err != nil { + return nil, err + } + + return &identityProviderPermissions, nil +} + +func (identityProviderPermissions *IdentityProviderPermissions) GetTokenExchangeScopedPermissionId() (string, error) { + if identityProviderPermissions.Enabled { + return identityProviderPermissions.ScopePermissions["token-exchange"].(string), nil + } else { + return "", fmt.Errorf("identity provider permissions are not enabled, thus can not return the linked 'token-exchange' scope based permission") + } +} diff --git a/provider/provider.go b/provider/provider.go index cf8d512d9..b06a1d6fb 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -19,69 +19,70 @@ func KeycloakProvider() *schema.Provider { "keycloak_saml_client_installation_provider": dataSourceKeycloakSamlClientInstallationProvider(), }, ResourcesMap: map[string]*schema.Resource{ - "keycloak_realm": resourceKeycloakRealm(), - "keycloak_realm_events": resourceKeycloakRealmEvents(), - "keycloak_required_action": resourceKeycloakRequiredAction(), - "keycloak_group": resourceKeycloakGroup(), - "keycloak_group_memberships": resourceKeycloakGroupMemberships(), - "keycloak_default_groups": resourceKeycloakDefaultGroups(), - "keycloak_group_roles": resourceKeycloakGroupRoles(), - "keycloak_user": resourceKeycloakUser(), - "keycloak_user_roles": resourceKeycloakUserRoles(), - "keycloak_openid_client": resourceKeycloakOpenidClient(), - "keycloak_openid_client_scope": resourceKeycloakOpenidClientScope(), - "keycloak_ldap_user_federation": resourceKeycloakLdapUserFederation(), - "keycloak_ldap_user_attribute_mapper": resourceKeycloakLdapUserAttributeMapper(), - "keycloak_ldap_group_mapper": resourceKeycloakLdapGroupMapper(), - "keycloak_ldap_role_mapper": resourceKeycloakLdapRoleMapper(), - "keycloak_ldap_hardcoded_role_mapper": resourceKeycloakLdapHardcodedRoleMapper(), - "keycloak_ldap_hardcoded_group_mapper": resourceKeycloakLdapHardcodedGroupMapper(), - "keycloak_ldap_msad_user_account_control_mapper": resourceKeycloakLdapMsadUserAccountControlMapper(), - "keycloak_ldap_msad_lds_user_account_control_mapper": resourceKeycloakLdapMsadLdsUserAccountControlMapper(), - "keycloak_ldap_full_name_mapper": resourceKeycloakLdapFullNameMapper(), - "keycloak_custom_user_federation": resourceKeycloakCustomUserFederation(), - "keycloak_openid_user_attribute_protocol_mapper": resourceKeycloakOpenIdUserAttributeProtocolMapper(), - "keycloak_openid_user_property_protocol_mapper": resourceKeycloakOpenIdUserPropertyProtocolMapper(), - "keycloak_openid_group_membership_protocol_mapper": resourceKeycloakOpenIdGroupMembershipProtocolMapper(), - "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_user_realm_role_protocol_mapper": resourceKeycloakOpenIdUserRealmRoleProtocolMapper(), - "keycloak_openid_user_client_role_protocol_mapper": resourceKeycloakOpenIdUserClientRoleProtocolMapper(), - "keycloak_openid_user_session_note_protocol_mapper": resourceKeycloakOpenIdUserSessionNoteProtocolMapper(), - "keycloak_openid_client_default_scopes": resourceKeycloakOpenidClientDefaultScopes(), - "keycloak_openid_client_optional_scopes": resourceKeycloakOpenidClientOptionalScopes(), - "keycloak_saml_client": resourceKeycloakSamlClient(), - "keycloak_generic_client_protocol_mapper": resourceKeycloakGenericClientProtocolMapper(), - "keycloak_generic_client_role_mapper": resourceKeycloakGenericClientRoleMapper(), - "keycloak_saml_user_attribute_protocol_mapper": resourceKeycloakSamlUserAttributeProtocolMapper(), - "keycloak_saml_user_property_protocol_mapper": resourceKeycloakSamlUserPropertyProtocolMapper(), - "keycloak_hardcoded_attribute_identity_provider_mapper": resourceKeycloakHardcodedAttributeIdentityProviderMapper(), - "keycloak_hardcoded_role_identity_provider_mapper": resourceKeycloakHardcodedRoleIdentityProviderMapper(), - "keycloak_attribute_importer_identity_provider_mapper": resourceKeycloakAttributeImporterIdentityProviderMapper(), - "keycloak_attribute_to_role_identity_provider_mapper": resourceKeycloakAttributeToRoleIdentityProviderMapper(), - "keycloak_user_template_importer_identity_provider_mapper": resourceKeycloakUserTemplateImporterIdentityProviderMapper(), - "keycloak_saml_identity_provider": resourceKeycloakSamlIdentityProvider(), - "keycloak_oidc_google_identity_provider": resourceKeycloakOidcGoogleIdentityProvider(), - "keycloak_oidc_identity_provider": resourceKeycloakOidcIdentityProvider(), - "keycloak_openid_client_authorization_resource": resourceKeycloakOpenidClientAuthorizationResource(), - "keycloak_openid_client_group_policy": resourceKeycloakOpenidClientAuthorizationGroupPolicy(), - "keycloak_openid_client_role_policy": resourceKeycloakOpenidClientAuthorizationRolePolicy(), - "keycloak_openid_client_aggregate_policy": resourceKeycloakOpenidClientAuthorizationAggregatePolicy(), - "keycloak_openid_client_js_policy": resourceKeycloakOpenidClientAuthorizationJSPolicy(), - "keycloak_openid_client_time_policy": resourceKeycloakOpenidClientAuthorizationTimePolicy(), - "keycloak_openid_client_user_policy": resourceKeycloakOpenidClientAuthorizationUserPolicy(), - "keycloak_openid_client_client_policy": resourceKeycloakOpenidClientAuthorizationClientPolicy(), - "keycloak_openid_client_authorization_scope": resourceKeycloakOpenidClientAuthorizationScope(), - "keycloak_openid_client_authorization_permission": resourceKeycloakOpenidClientAuthorizationPermission(), - "keycloak_openid_client_service_account_role": resourceKeycloakOpenidClientServiceAccountRole(), - "keycloak_openid_client_service_account_realm_role": resourceKeycloakOpenidClientServiceAccountRealmRole(), - "keycloak_role": resourceKeycloakRole(), - "keycloak_authentication_flow": resourceKeycloakAuthenticationFlow(), - "keycloak_authentication_subflow": resourceKeycloakAuthenticationSubFlow(), - "keycloak_authentication_execution": resourceKeycloakAuthenticationExecution(), - "keycloak_authentication_execution_config": resourceKeycloakAuthenticationExecutionConfig(), + "keycloak_realm": resourceKeycloakRealm(), + "keycloak_realm_events": resourceKeycloakRealmEvents(), + "keycloak_required_action": resourceKeycloakRequiredAction(), + "keycloak_group": resourceKeycloakGroup(), + "keycloak_group_memberships": resourceKeycloakGroupMemberships(), + "keycloak_default_groups": resourceKeycloakDefaultGroups(), + "keycloak_group_roles": resourceKeycloakGroupRoles(), + "keycloak_user": resourceKeycloakUser(), + "keycloak_user_roles": resourceKeycloakUserRoles(), + "keycloak_openid_client": resourceKeycloakOpenidClient(), + "keycloak_openid_client_scope": resourceKeycloakOpenidClientScope(), + "keycloak_ldap_user_federation": resourceKeycloakLdapUserFederation(), + "keycloak_ldap_user_attribute_mapper": resourceKeycloakLdapUserAttributeMapper(), + "keycloak_ldap_group_mapper": resourceKeycloakLdapGroupMapper(), + "keycloak_ldap_role_mapper": resourceKeycloakLdapRoleMapper(), + "keycloak_ldap_hardcoded_role_mapper": resourceKeycloakLdapHardcodedRoleMapper(), + "keycloak_ldap_hardcoded_group_mapper": resourceKeycloakLdapHardcodedGroupMapper(), + "keycloak_ldap_msad_user_account_control_mapper": resourceKeycloakLdapMsadUserAccountControlMapper(), + "keycloak_ldap_msad_lds_user_account_control_mapper": resourceKeycloakLdapMsadLdsUserAccountControlMapper(), + "keycloak_ldap_full_name_mapper": resourceKeycloakLdapFullNameMapper(), + "keycloak_custom_user_federation": resourceKeycloakCustomUserFederation(), + "keycloak_openid_user_attribute_protocol_mapper": resourceKeycloakOpenIdUserAttributeProtocolMapper(), + "keycloak_openid_user_property_protocol_mapper": resourceKeycloakOpenIdUserPropertyProtocolMapper(), + "keycloak_openid_group_membership_protocol_mapper": resourceKeycloakOpenIdGroupMembershipProtocolMapper(), + "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_user_realm_role_protocol_mapper": resourceKeycloakOpenIdUserRealmRoleProtocolMapper(), + "keycloak_openid_user_client_role_protocol_mapper": resourceKeycloakOpenIdUserClientRoleProtocolMapper(), + "keycloak_openid_user_session_note_protocol_mapper": resourceKeycloakOpenIdUserSessionNoteProtocolMapper(), + "keycloak_openid_client_default_scopes": resourceKeycloakOpenidClientDefaultScopes(), + "keycloak_openid_client_optional_scopes": resourceKeycloakOpenidClientOptionalScopes(), + "keycloak_saml_client": resourceKeycloakSamlClient(), + "keycloak_generic_client_protocol_mapper": resourceKeycloakGenericClientProtocolMapper(), + "keycloak_generic_client_role_mapper": resourceKeycloakGenericClientRoleMapper(), + "keycloak_saml_user_attribute_protocol_mapper": resourceKeycloakSamlUserAttributeProtocolMapper(), + "keycloak_saml_user_property_protocol_mapper": resourceKeycloakSamlUserPropertyProtocolMapper(), + "keycloak_hardcoded_attribute_identity_provider_mapper": resourceKeycloakHardcodedAttributeIdentityProviderMapper(), + "keycloak_hardcoded_role_identity_provider_mapper": resourceKeycloakHardcodedRoleIdentityProviderMapper(), + "keycloak_attribute_importer_identity_provider_mapper": resourceKeycloakAttributeImporterIdentityProviderMapper(), + "keycloak_attribute_to_role_identity_provider_mapper": resourceKeycloakAttributeToRoleIdentityProviderMapper(), + "keycloak_user_template_importer_identity_provider_mapper": resourceKeycloakUserTemplateImporterIdentityProviderMapper(), + "keycloak_saml_identity_provider": resourceKeycloakSamlIdentityProvider(), + "keycloak_oidc_google_identity_provider": resourceKeycloakOidcGoogleIdentityProvider(), + "keycloak_oidc_identity_provider": resourceKeycloakOidcIdentityProvider(), + "keycloak_openid_client_authorization_resource": resourceKeycloakOpenidClientAuthorizationResource(), + "keycloak_openid_client_group_policy": resourceKeycloakOpenidClientAuthorizationGroupPolicy(), + "keycloak_openid_client_role_policy": resourceKeycloakOpenidClientAuthorizationRolePolicy(), + "keycloak_openid_client_aggregate_policy": resourceKeycloakOpenidClientAuthorizationAggregatePolicy(), + "keycloak_openid_client_js_policy": resourceKeycloakOpenidClientAuthorizationJSPolicy(), + "keycloak_openid_client_time_policy": resourceKeycloakOpenidClientAuthorizationTimePolicy(), + "keycloak_openid_client_user_policy": resourceKeycloakOpenidClientAuthorizationUserPolicy(), + "keycloak_openid_client_client_policy": resourceKeycloakOpenidClientAuthorizationClientPolicy(), + "keycloak_openid_client_authorization_scope": resourceKeycloakOpenidClientAuthorizationScope(), + "keycloak_openid_client_authorization_permission": resourceKeycloakOpenidClientAuthorizationPermission(), + "keycloak_openid_client_service_account_role": resourceKeycloakOpenidClientServiceAccountRole(), + "keycloak_openid_client_service_account_realm_role": resourceKeycloakOpenidClientServiceAccountRealmRole(), + "keycloak_role": resourceKeycloakRole(), + "keycloak_authentication_flow": resourceKeycloakAuthenticationFlow(), + "keycloak_authentication_subflow": resourceKeycloakAuthenticationSubFlow(), + "keycloak_authentication_execution": resourceKeycloakAuthenticationExecution(), + "keycloak_authentication_execution_config": resourceKeycloakAuthenticationExecutionConfig(), + "keycloak_identity_provider_token_exchange_scope_permission": resourceKeycloakIdentityProviderTokenExchangeScopePermission(), }, Schema: map[string]*schema.Schema{ "client_id": { diff --git a/provider/resource_keycloak_identity_provider_token_exchange_scope_permission.go b/provider/resource_keycloak_identity_provider_token_exchange_scope_permission.go new file mode 100644 index 000000000..781d20305 --- /dev/null +++ b/provider/resource_keycloak_identity_provider_token_exchange_scope_permission.go @@ -0,0 +1,297 @@ +package provider + +import ( + "encoding/hex" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "log" + "math/rand" + "strings" +) + +var ( + keycloakIdpTokenExchangePermissionPolicyTypes = []string{"client"} +) + +func resourceKeycloakIdentityProviderTokenExchangeScopePermission() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakIdentityProviderTokenExchangeScopePermissionCreate, + Read: resourceKeycloakIdentityProviderTokenExchangeScopePermissionRead, + Delete: resourceKeycloakIdentityProviderTokenExchangeScopePermissionDelete, + Update: resourceKeycloakIdentityProviderTokenExchangeScopePermissionUpdate, + // This resource can be imported using {{realmId}}/{{providerAlias}}. The provider alias is displayed in the URL when editing it from the GUI + Importer: &schema.ResourceImporter{ + State: resourceKeycloakIdentityProviderTokenExchangeScopePermissionImport, + }, + Schema: map[string]*schema.Schema{ + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "provider_alias": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "policy_type": { + Type: schema.TypeString, + Optional: true, + Default: "client", + Description: "Type of policy that is created. At the moment only 'client' type is supported", + ValidateFunc: validation.StringInSlice(keycloakIdpTokenExchangePermissionPolicyTypes, false), + }, + "clients": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "Ids of the clients for which a policy will be created and set on scope based token exchange permission", + }, + "policy_id": { + Type: schema.TypeString, + Computed: true, + Description: "Policy id that will be set on the scope based token exchange permission automatically created by enabling permissions on the reference identity provider", + }, + "authorization_resource_server_id": { + Type: schema.TypeString, + Computed: true, + Description: "Resource server id representing the realm management client on which this permission is managed", + }, + "authorization_idp_resource_id": { + Type: schema.TypeString, + Computed: true, + Description: "Resource id representing the identity provider, this automatically created by keycloak", + }, + "authorization_token_exchange_scope_permission_id": { + Type: schema.TypeString, + Computed: true, + Description: "Permission id representing the Permission with scope 'Token Exchange' and the resource 'authorization_idp_resource_id', this automatically created by keycloak, the policy id will be set on this permission", + }, + }, + } +} + +func setIdentityProviderTokenExchangeScopePermissionClientPolicy(keycloakClient *keycloak.KeycloakClient, realmId, providerAlias string, clients []string) error { + identityProviderPermissions, err := keycloakClient.GetIdentityProviderPermissions(realmId, providerAlias) + if err != nil { + return err + } + + realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management") + if err != nil { + return err + } + + tokenExchangeScopedPermissionId, err := identityProviderPermissions.GetTokenExchangeScopedPermissionId() + if err != nil { + return err + } + + permission, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, tokenExchangeScopedPermissionId) + if err != nil { + return err + } + + if len(permission.Policies) == 0 { + policyId, err := createClientPolicy(keycloakClient, realmId, realmManagementClient.Id, providerAlias, clients) + if err != nil { + return err + } + permission.Policies = []string{policyId} + return keycloakClient.UpdateOpenidClientAuthorizationPermission(permission) + + } else if len(permission.Policies) == 1 { + openidClientAuthorizationClientPolicy, err := keycloakClient.GetOpenidClientAuthorizationClientPolicy(realmId, realmManagementClient.Id, permission.Policies[0]) + if err != nil { + return err + } + openidClientAuthorizationClientPolicy.Clients = clients + return keycloakClient.UpdateOpenidClientAuthorizationClientPolicy(openidClientAuthorizationClientPolicy) + + } else { + return fmt.Errorf("only one client policy is supported, but %d were found", len(permission.Policies)) + } +} + +func createClientPolicy(keycloakClient *keycloak.KeycloakClient, realmId, realmManagementClientId, providerAlias string, clients []string) (string, error) { + openidClientAuthorizationClientPolicy := &keycloak.OpenidClientAuthorizationClientPolicy{ + RealmId: realmId, + ResourceServerId: realmManagementClientId, + Name: providerAlias + "_idp_client_policy", + DecisionStrategy: "UNANIMOUS", + Logic: "POSITIVE", + Type: "client", + Clients: clients, + } + err := keycloakClient.NewOpenidClientAuthorizationClientPolicy(openidClientAuthorizationClientPolicy) + if err != nil { + if keycloak.ErrorIs409(err) { + b := make([]byte, 4) + rand.Read(b) + suffix := hex.EncodeToString(b) + openidClientAuthorizationClientPolicy.Name = providerAlias + "_" + suffix + "_idp_client_policy" + err = keycloakClient.NewOpenidClientAuthorizationClientPolicy(openidClientAuthorizationClientPolicy) + } + } + if err != nil { + return "", err + } + return openidClientAuthorizationClientPolicy.Id, nil +} + +func unsetIdentityProviderTokenExchangeScopePermissionPolicy(keycloakClient *keycloak.KeycloakClient, realmId, providerAlias, policyId string) error { + identityProviderPermissions, err := keycloakClient.GetIdentityProviderPermissions(realmId, providerAlias) + if err != nil { + return err + } + + realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management") + if err != nil { + return err + } + + tokenExchangeScopedPermissionId, err := identityProviderPermissions.GetTokenExchangeScopedPermissionId() + if err != nil { + return err + } + + permission, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, tokenExchangeScopedPermissionId) + if err != nil { + return err + } + + permission.Policies = []string{} + err = keycloakClient.UpdateOpenidClientAuthorizationPermission(permission) + if err != nil { + return err + } + + err = keycloakClient.DisableIdentityProviderPermissions(realmId, providerAlias) + if err != nil { + return err + } + + _ = keycloakClient.DeleteOpenidClientAuthorizationClientPolicy(realmId, realmManagementClient.Id, policyId) + + return nil +} + +func resourceKeycloakIdentityProviderTokenExchangeScopePermissionCreate(data *schema.ResourceData, meta interface{}) error { + return resourceKeycloakIdentityProviderTokenExchangeScopePermissionUpdate(data, meta) +} + +func resourceKeycloakIdentityProviderTokenExchangeScopePermissionUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + realmId := data.Get("realm_id").(string) + providerAlias := data.Get("provider_alias").(string) + policyType := data.Get("policy_type").(string) + var clients []string + + if v, ok := data.GetOk("clients"); ok { + for _, client := range v.(*schema.Set).List() { + clients = append(clients, client.(string)) + } + } + + err := keycloakClient.EnableIdentityProviderPermissions(realmId, providerAlias) + if err != nil { + return err + } + if policyType == "client" { + err = setIdentityProviderTokenExchangeScopePermissionClientPolicy(keycloakClient, realmId, providerAlias, clients) + if err != nil { + return err + } + } else { + return fmt.Errorf("invalid policy type, supported types are ['client']") + } + return resourceKeycloakIdentityProviderTokenExchangeScopePermissionRead(data, meta) +} + +func resourceKeycloakIdentityProviderTokenExchangeScopePermissionRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + realmId := data.Get("realm_id").(string) + providerAlias := data.Get("provider_alias").(string) + + identityProviderPermissions, err := keycloakClient.GetIdentityProviderPermissions(realmId, providerAlias) + if err != nil { + return handleNotFoundError(err, data) + } + if !identityProviderPermissions.Enabled { + log.Printf("[WARN] Removing resource with id %s from state as it no longer enabled", data.Id()) + data.SetId("") + return nil + } + + data.SetId(identityProviderPermissions.RealmId + "/" + identityProviderPermissions.ProviderAlias) + data.Set("realm_id", identityProviderPermissions.RealmId) + data.Set("provider_alias", identityProviderPermissions.ProviderAlias) + + realmManagementClient, err := keycloakClient.GetOpenidClientByClientId(realmId, "realm-management") + if err != nil { + return err + } + + tokenExchangeScopedPermissionId, err := identityProviderPermissions.GetTokenExchangeScopedPermissionId() + if err != nil { + return err + } + + permission, err := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, realmManagementClient.Id, tokenExchangeScopedPermissionId) + if err != nil { + return err + } + + var openidClientAuthorizationClientPolicyId string + if len(permission.Policies) >= 1 { + openidClientAuthorizationClientPolicyId = permission.Policies[0] + } else { + openidClientAuthorizationClientPolicyId, err = createClientPolicy(keycloakClient, realmId, realmManagementClient.Id, providerAlias, data.Get("clients").([]string)) + if err != nil { + return err + } + } + openidClientAuthorizationClientPolicy, err := keycloakClient.GetOpenidClientAuthorizationClientPolicy(realmId, realmManagementClient.Id, openidClientAuthorizationClientPolicyId) + if err != nil { + return err + } + + data.Set("policy_id", openidClientAuthorizationClientPolicy.Id) + data.Set("clients", openidClientAuthorizationClientPolicy.Clients) + + data.Set("policy_type", data.Get("policy_type")) + data.Set("authorization_resource_server_id", realmManagementClient.Id) + data.Set("authorization_idp_resource_id", identityProviderPermissions.Resource) + + data.Set("authorization_token_exchange_scope_permission_id", tokenExchangeScopedPermissionId) + + return nil +} + +func resourceKeycloakIdentityProviderTokenExchangeScopePermissionDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + providerAlias := data.Get("provider_alias").(string) + policyId := data.Get("policy_id").(string) + + identityProviderPermissions, err := keycloakClient.GetIdentityProviderPermissions(realmId, providerAlias) + if err == nil && identityProviderPermissions.Enabled { + _ = unsetIdentityProviderTokenExchangeScopePermissionPolicy(keycloakClient, realmId, providerAlias, policyId) + } + return keycloakClient.DisableIdentityProviderPermissions(realmId, providerAlias) +} + +func resourceKeycloakIdentityProviderTokenExchangeScopePermissionImport(d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), "/") + + if len(parts) != 2 { + return nil, fmt.Errorf("Invalid import. Supported import formats: {{realmId}}/{{providerAlias}}") + } + d.SetId(parts[0] + "/" + parts[1]) + d.Set("realm_id", parts[0]) + d.Set("provider_alias", parts[1]) + return []*schema.ResourceData{d}, nil +} diff --git a/provider/resource_keycloak_identity_provider_token_exchange_scope_permission_test.go b/provider/resource_keycloak_identity_provider_token_exchange_scope_permission_test.go new file mode 100644 index 000000000..4597f5742 --- /dev/null +++ b/provider/resource_keycloak_identity_provider_token_exchange_scope_permission_test.go @@ -0,0 +1,447 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "regexp" + "testing" +) + +func TestAccKeycloakIdpTokenExchangeScopePermission_basic(t *testing.T) { + realmName := "tf_token_exchange-" + acctest.RandString(10) + providerAlias := "tf-" + acctest.RandString(10) + webappClientId := "tf-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakIdpTokenExchangeScopePermissionDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakIdpTokenExchangeScopePermission_basic(realmName, providerAlias, webappClientId), + Check: testAccCheckKeycloakIdpTokenExchangeScopePermissionExists("keycloak_identity_provider_token_exchange_scope_permission.my_permission"), + }, + }, + }) +} + +func TestAccKeycloakIdpTokenExchangeScopePermission_createAfterManualDestroy(t *testing.T) { + var idpPermissions = &keycloak.IdentityProviderPermissions{} + + realmName := "tf_token_exchange-" + acctest.RandString(10) + providerAlias := "tf-" + acctest.RandString(10) + webappClientId := "tf-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakIdpTokenExchangeScopePermissionDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakIdpTokenExchangeScopePermission_basic(realmName, providerAlias, webappClientId), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakIdpTokenExchangeScopePermissionExists("keycloak_identity_provider_token_exchange_scope_permission.my_permission"), + testAccCheckKeycloakIdpPermissionFetch("keycloak_identity_provider_token_exchange_scope_permission.my_permission", idpPermissions), + ), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + err := keycloakClient.DisableIdentityProviderPermissions(idpPermissions.RealmId, idpPermissions.ProviderAlias) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakIdpTokenExchangeScopePermission_basic(realmName, providerAlias, webappClientId), + Check: testAccCheckKeycloakIdpTokenExchangeScopePermissionExists("keycloak_identity_provider_token_exchange_scope_permission.my_permission"), + }, + }, + }) +} + +func TestAccKeycloakIdpTokenExchangeScopePermission_import(t *testing.T) { + realmName := "tf_token_exchange-" + acctest.RandString(10) + providerAlias := "tf-" + acctest.RandString(10) + webappClientId := "tf-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakIdpTokenExchangeScopePermissionDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakIdpTokenExchangeScopePermission_basic(realmName, providerAlias, webappClientId), + Check: testAccCheckKeycloakIdpTokenExchangeScopePermissionExists("keycloak_identity_provider_token_exchange_scope_permission.my_permission"), + }, + { + ResourceName: "keycloak_identity_provider_token_exchange_scope_permission.my_permission", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"policy_type"}, + }, + }, + }) +} + +func TestAccKeycloakIdpTokenExchangeScopePermission_updatePolicyMultipleClients(t *testing.T) { + realmName := "tf_token_exchange-" + acctest.RandString(10) + providerAlias := "tf-" + acctest.RandString(10) + webappClientId := "tf-" + acctest.RandString(10) + webappClientId2 := "tf-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakIdpTokenExchangeScopePermissionDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakIdpTokenExchangeScopePermission_basic(realmName, providerAlias, webappClientId), + Check: testAccCheckKeycloakIdpTokenExchangeScopePermissionClientPolicyHasClient("keycloak_identity_provider_token_exchange_scope_permission.my_permission", webappClientId), + }, + { + Config: testKeycloakIdpTokenExchangeScopePermission_multipleClients(realmName, providerAlias, webappClientId, webappClientId2), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakIdpTokenExchangeScopePermissionClientPolicyHasClient("keycloak_identity_provider_token_exchange_scope_permission.my_permission", webappClientId), + testAccCheckKeycloakIdpTokenExchangeScopePermissionClientPolicyHasClient("keycloak_identity_provider_token_exchange_scope_permission.my_permission", webappClientId2), + ), + }, + { + Config: testKeycloakIdpTokenExchangeScopePermission_basic(realmName, providerAlias, webappClientId2), + Check: testAccCheckKeycloakIdpTokenExchangeScopePermissionClientPolicyHasClient("keycloak_identity_provider_token_exchange_scope_permission.my_permission", webappClientId2), + }, + { + Config: testKeycloakIdpTokenExchangeScopePermission_basic(realmName, providerAlias, webappClientId), + Check: testAccCheckKeycloakIdpTokenExchangeScopePermissionClientPolicyHasClient("keycloak_identity_provider_token_exchange_scope_permission.my_permission", webappClientId), + }, + }, + }) +} + +func TestAccKeycloakIdpTokenExchangeScopePermission_rolePolicy(t *testing.T) { + realmName := "tf_token_exchange-" + acctest.RandString(10) + providerAlias := "tf-" + acctest.RandString(10) + webappClientId := "tf-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakIdpTokenExchangeScopePermissionDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakIdpTokenExchangeScopePermission_rolePolicy(realmName, providerAlias, webappClientId), + ExpectError: regexp.MustCompile(".*expected policy_type to be one of.*"), + }, + }, + }) +} + +func testAccCheckKeycloakIdpTokenExchangeScopePermissionDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_identity_provider_token_exchange_scope_permission" { + continue + } + + realmId := rs.Primary.Attributes["realm_id"] + providerAlias := rs.Primary.Attributes["provider_alias"] + policyId := rs.Primary.Attributes["policy_id"] + authorizationResourceServerId := rs.Primary.Attributes["authorization_resource_server_id"] + authorizationIdpResourceId := rs.Primary.Attributes["authorization_idp_resource_id"] + authorizationTokenExchangeScopePermissionId := rs.Primary.Attributes["authorization_token_exchange_scope_permission_id"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + permissions, _ := keycloakClient.GetIdentityProviderPermissions(realmId, providerAlias) + if permissions != nil { + return fmt.Errorf("idp permissions for realm id %s and provider alias %s still exists", realmId, providerAlias) + } + + tokenExchangePermission, _ := keycloakClient.GetOpenidClientAuthorizationPermission(realmId, authorizationResourceServerId, authorizationTokenExchangeScopePermissionId) + if tokenExchangePermission != nil { + return fmt.Errorf("tokenExchangePermission for realm id %s, resource server id %s and permission id %s still exists", realmId, authorizationResourceServerId, authorizationTokenExchangeScopePermissionId) + } + + idpResource, _ := keycloakClient.GetOpenidClientAuthorizationResource(realmId, authorizationResourceServerId, authorizationIdpResourceId) + if idpResource != nil { + return fmt.Errorf("idp resource for realm id%s, resource server id %s and resource id %s still exists", realmId, authorizationResourceServerId, authorizationIdpResourceId) + } + + policy, _ := keycloakClient.GetOpenidClientAuthorizationClientPolicy(realmId, authorizationResourceServerId, policyId) + if policy != nil { + return fmt.Errorf("client policy for realm id %s, resource server id %s and policy id %s still exists", realmId, authorizationResourceServerId, policyId) + } + } + + return nil + } +} + +func testAccCheckKeycloakIdpTokenExchangeScopePermissionExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + permissions, err := getIdpPermissionsFromState(s, resourceName) + if err != nil { + return err + } + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("resource not found: %s", resourceName) + } + policyId := rs.Primary.Attributes["policy_id"] + authorizationResourceServerId := rs.Primary.Attributes["authorization_resource_server_id"] + authorizationIdpResourceId := rs.Primary.Attributes["authorization_idp_resource_id"] + authorizationTokenExchangeScopePermissionId := rs.Primary.Attributes["authorization_token_exchange_scope_permission_id"] + + var realmManagementId string + clients, _ := keycloakClient.GetOpenidClients(permissions.RealmId, false) + for _, client := range clients { + if client.ClientId == "realm-management" { + realmManagementId = client.Id + break + } + } + + if authorizationResourceServerId != realmManagementId { + return fmt.Errorf("computed authorizationResourceServerId %s was not equal to %s (the id of the realm-management client)", authorizationResourceServerId, realmManagementId) + } + + tokenExchangeScopedPermissionId, err := permissions.GetTokenExchangeScopedPermissionId() + if err != nil { + return err + } + + if authorizationTokenExchangeScopePermissionId != tokenExchangeScopedPermissionId { + return fmt.Errorf("computed authorizationTokenExchangeScopePermissionId %s was not equal to %s scope permission id set on the idp permission", authorizationTokenExchangeScopePermissionId, tokenExchangeScopedPermissionId) + } + + tokenExchangeScopedPermission, err := keycloakClient.GetOpenidClientAuthorizationPermission(permissions.RealmId, realmManagementId, tokenExchangeScopedPermissionId) + if err != nil { + return err + } + + if tokenExchangeScopedPermission == nil { + return fmt.Errorf("token exchange scope permission represented to idp permission could not be found") + } + + if len(tokenExchangeScopedPermission.Policies) != 1 { + return fmt.Errorf("token exchange scope permission has not exact 1 policy, it has %d", len(tokenExchangeScopedPermission.Policies)) + } + + policy, err := keycloakClient.GetOpenidClientAuthorizationClientPolicy(permissions.RealmId, realmManagementId, tokenExchangeScopedPermission.Policies[0]) + if err != nil { + return err + } + + if policyId != policy.Id { + return fmt.Errorf("computed policyId %s was not equal to %s policyId found on the token exchange scope based permission", policyId, policy.Id) + } + + idpResource, err := keycloakClient.GetOpenidClientAuthorizationResource(permissions.RealmId, realmManagementId, permissions.Resource) + if err != nil { + return err + } + + if tokenExchangeScopedPermission.Resources[0] != idpResource.Id { + return fmt.Errorf("fetched permission resources %s do not correspond with the idp resource provided id %s", tokenExchangeScopedPermission.Resources[0], idpResource.Id) + } + + if authorizationIdpResourceId != idpResource.Id { + return fmt.Errorf("computed authorizationIdpResourceId %s was not equal to %s sidp resource id found on the token exchange scope based permission", authorizationIdpResourceId, idpResource.Id) + } + + return nil + } +} + +func testAccCheckKeycloakIdpTokenExchangeScopePermissionClientPolicyHasClient(resourceName, clientId 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) + } + realmId := rs.Primary.Attributes["realm_id"] + authorizationResourceServerId := rs.Primary.Attributes["authorization_resource_server_id"] + policyId := rs.Primary.Attributes["policy_id"] + + policy, err := keycloakClient.GetOpenidClientAuthorizationClientPolicy(realmId, authorizationResourceServerId, policyId) + if err != nil { + return err + } + + client, err := keycloakClient.GetOpenidClientByClientId(realmId, clientId) + if err != nil { + return err + } + + clientNotFound := true + for _, idOfClientString := range policy.Clients { + if idOfClientString == client.Id { + clientNotFound = false + break + } + } + if clientNotFound { + return fmt.Errorf("client with clientId %s was not linked to policy", clientId) + } + + return nil + } +} + +func testAccCheckKeycloakIdpPermissionFetch(resourceName string, idpPermissions *keycloak.IdentityProviderPermissions) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedPermissions, err := getIdpPermissionsFromState(s, resourceName) + if err != nil { + return err + } + + idpPermissions.RealmId = fetchedPermissions.RealmId + idpPermissions.Enabled = fetchedPermissions.Enabled + idpPermissions.ProviderAlias = fetchedPermissions.ProviderAlias + idpPermissions.ScopePermissions = fetchedPermissions.ScopePermissions + idpPermissions.Resource = fetchedPermissions.Resource + + return nil + } +} + +func getIdpPermissionsFromState(s *terraform.State, resourceName string) (*keycloak.IdentityProviderPermissions, error) { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return nil, fmt.Errorf("resource not found: %s", resourceName) + } + + realmId := rs.Primary.Attributes["realm_id"] + providerAlias := rs.Primary.Attributes["provider_alias"] + + permissions, err := keycloakClient.GetIdentityProviderPermissions(realmId, providerAlias) + if err != nil { + return nil, fmt.Errorf("error getting idp permissions with realm id %s and provider alias %s: %s", realmId, providerAlias, err) + + } + return permissions, nil +} + +func testKeycloakIdpTokenExchangeScopePermission_basic(realmId, providerAlias, webappClientId string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_oidc_identity_provider" "my_idp" { + realm = keycloak_realm.realm.id + alias = "%s" + authorization_url = "http://localhost:8080/auth/realms/something/protocol/openid-connect/auth" + token_url = "http://localhost:8080/auth/realms/something/protocol/openid-connect/token" + client_id = "clientid" + client_secret = "secret" +} + +resource "keycloak_openid_client" "webapp_client" { + realm_id = keycloak_realm.realm.id + name = "webapp_client" + client_id = "%s" + client_secret = "secret" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + valid_redirect_uris = [ + "http://localhost:8080/*", + ] +} + +resource "keycloak_identity_provider_token_exchange_scope_permission" "my_permission" { + realm_id = keycloak_realm.realm.id + provider_alias = keycloak_oidc_identity_provider.my_idp.alias + policy_type = "client" + clients = [keycloak_openid_client.webapp_client.id] +} + `, realmId, providerAlias, webappClientId) +} + +func testKeycloakIdpTokenExchangeScopePermission_multipleClients(realmId, providerAlias, webappClientId, webappClientId2 string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_oidc_identity_provider" "my_idp" { + realm = keycloak_realm.realm.id + alias = "%s" + authorization_url = "http://localhost:8080/auth/realms/something/protocol/openid-connect/auth" + token_url = "http://localhost:8080/auth/realms/something/protocol/openid-connect/token" + client_id = "clientid" + client_secret = "secret" +} + +resource "keycloak_openid_client" "webapp_client" { + realm_id = keycloak_realm.realm.id + name = "webapp_client" + client_id = "%s" + client_secret = "secret" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + valid_redirect_uris = [ + "http://localhost:8080/*", + ] +} + +resource "keycloak_openid_client" "webapp_client2" { + realm_id = keycloak_realm.realm.id + name = "webapp_client" + client_id = "%s" + client_secret = "secret" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + valid_redirect_uris = [ + "http://localhost:8080/*", + ] +} + +resource "keycloak_identity_provider_token_exchange_scope_permission" "my_permission" { + realm_id = keycloak_realm.realm.id + provider_alias = keycloak_oidc_identity_provider.my_idp.alias + policy_type = "client" + clients = [keycloak_openid_client.webapp_client.id, keycloak_openid_client.webapp_client2.id] +} + `, realmId, providerAlias, webappClientId, webappClientId2) +} + +func testKeycloakIdpTokenExchangeScopePermission_rolePolicy(realmId, providerAlias, webappClientId string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_oidc_identity_provider" "my_idp" { + realm = keycloak_realm.realm.id + alias = "%s" + authorization_url = "http://localhost:8080/auth/realms/something/protocol/openid-connect/auth" + token_url = "http://localhost:8080/auth/realms/something/protocol/openid-connect/token" + client_id = "clientid" + client_secret = "secret" +} + +resource "keycloak_openid_client" "webapp_client" { + realm_id = keycloak_realm.realm.id + name = "webapp_client" + client_id = "%s" + client_secret = "secret" + access_type = "CONFIDENTIAL" + standard_flow_enabled = true + valid_redirect_uris = [ + "http://localhost:8080/*", + ] +} + +resource "keycloak_identity_provider_token_exchange_scope_permission" "my_permission" { + realm_id = keycloak_realm.realm.id + provider_alias = keycloak_oidc_identity_provider.my_idp.alias + policy_type = "role" + clients = [keycloak_openid_client.webapp_client.id] +} + `, realmId, providerAlias, webappClientId) +}