diff --git a/docs/resources/hardcoded_attribute_mapper.md b/docs/resources/hardcoded_attribute_mapper.md new file mode 100644 index 00000000..85eabb46 --- /dev/null +++ b/docs/resources/hardcoded_attribute_mapper.md @@ -0,0 +1,66 @@ +--- +page_title: "keycloak_hardcoded_attribute_mapper Resource" +--- + +# keycloak_hardcoded_attribute_mapper Resource + +Allows for creating and managing hardcoded attribute mappers for Keycloak users federated via LDAP. + +The user model hardcoded attribute mapper will set the specified value to the attribute. + + +## Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "my-realm" + enabled = true +} + +resource "keycloak_ldap_user_federation" "ldap_user_federation" { + name = "openldap" + realm_id = keycloak_realm.realm.id + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" + + sync_registrations = true +} + +resource "keycloak_hardcoded_attribute_mapper" "email_verified" { + realm_id = keycloak_realm.realm.id + ldap_user_federation_id = keycloak_ldap_user_federation.ldap_user_federation.id + name = "email_verified" + attribute_name = "email_verified" + attribute_value = "true" +} +``` + +## Argument Reference + +- `realm_id` - (Required) The realm that this LDAP mapper will exist in. +- `ldap_user_federation_id` - (Required) The ID of the LDAP user federation provider to attach this mapper to. +- `name` - (Required) Display name of this mapper when displayed in the console. +- `attribute_name` - (Required) The name of the user model attribute to set. +- `attribute_value` - (Required) The value to set to model attribute. You can hardcode any value like 'foo'. + +## Import + +LDAP mappers can be imported using the format `{{realm_id}}/{{ldap_user_federation_id}}/{{attribute__mapper_id}}`. +The ID of the LDAP user federation provider and the mapper can be found within the Keycloak GUI, and they are typically GUIDs. + +Example: + +```bash +$ terraform import keycloak_hardcoded_attribute_mapper.email_verified my-realm/af2a6ca3-e4d7-49c3-b08b-1b3c70b4b860/3d923ece-1a91-4bf7-adaf-3b82f2a12b67 +``` diff --git a/keycloak/hardcoded_attribute_mapper.go b/keycloak/hardcoded_attribute_mapper.go new file mode 100644 index 00000000..c4ea676c --- /dev/null +++ b/keycloak/hardcoded_attribute_mapper.go @@ -0,0 +1,76 @@ +package keycloak + +import ( + "context" + "fmt" +) + +type HardcodedAttributeMapper struct { + Id string + Name string + RealmId string + LdapUserFederationId string + AttributeName string + AttributeValue string +} + +func convertFromHardcodedAttributeMapperToComponent(hardcodedMapper *HardcodedAttributeMapper) *component { + return &component{ + Id: hardcodedMapper.Id, + Name: hardcodedMapper.Name, + ProviderId: "hardcoded-attribute-mapper", + ProviderType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + ParentId: hardcodedMapper.LdapUserFederationId, + + Config: map[string][]string{ + "user.model.attribute": { + hardcodedMapper.AttributeName, + }, + "attribute.value": { + hardcodedMapper.AttributeValue, + }, + }, + } +} + +func convertFromComponentToHardcodedAttributeMapper(component *component, realmId string) *HardcodedAttributeMapper { + return &HardcodedAttributeMapper{ + Id: component.Id, + Name: component.Name, + RealmId: realmId, + LdapUserFederationId: component.ParentId, + + AttributeName: component.getConfig("user.model.attribute"), + AttributeValue: component.getConfig("attribute.value"), + } +} + +func (keycloakClient *KeycloakClient) NewHardcodedAttributeMapper(ctx context.Context, hardcodedMapper *HardcodedAttributeMapper) error { + _, location, err := keycloakClient.post(ctx, fmt.Sprintf("/realms/%s/components", hardcodedMapper.RealmId), convertFromHardcodedAttributeMapperToComponent(hardcodedMapper)) + if err != nil { + return err + } + + hardcodedMapper.Id = getIdFromLocationHeader(location) + + return nil +} + +func (keycloakClient *KeycloakClient) GetHardcodedAttributeMapper(ctx context.Context, realmId, id string) (*HardcodedAttributeMapper, error) { + var component *component + + err := keycloakClient.get(ctx, fmt.Sprintf("/realms/%s/components/%s", realmId, id), &component, nil) + if err != nil { + return nil, err + } + + return convertFromComponentToHardcodedAttributeMapper(component, realmId), nil +} + +func (keycloakClient *KeycloakClient) UpdateHardcodedAttributeMapper(ctx context.Context, hardcodedMapper *HardcodedAttributeMapper) error { + return keycloakClient.put(ctx, fmt.Sprintf("/realms/%s/components/%s", hardcodedMapper.RealmId, hardcodedMapper.Id), convertFromHardcodedAttributeMapperToComponent(hardcodedMapper)) +} + +func (keycloakClient *KeycloakClient) DeleteHardcodedAttributeMapper(ctx context.Context, realmId, id string) error { + return keycloakClient.delete(ctx, fmt.Sprintf("/realms/%s/components/%s", realmId, id), nil) +} diff --git a/provider/provider.go b/provider/provider.go index a3009128..ec66eabf 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -51,6 +51,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider { "keycloak_openid_client_scope": resourceKeycloakOpenidClientScope(), "keycloak_ldap_user_federation": resourceKeycloakLdapUserFederation(), "keycloak_ldap_user_attribute_mapper": resourceKeycloakLdapUserAttributeMapper(), + "keycloak_hardcoded_attribute_mapper": resourceKeycloakHardcodedAttributeMapper(), "keycloak_ldap_group_mapper": resourceKeycloakLdapGroupMapper(), "keycloak_ldap_role_mapper": resourceKeycloakLdapRoleMapper(), "keycloak_ldap_hardcoded_role_mapper": resourceKeycloakLdapHardcodedRoleMapper(), diff --git a/provider/resource_keycloak_hardcoded_attribute_mapper.go b/provider/resource_keycloak_hardcoded_attribute_mapper.go new file mode 100644 index 00000000..54dcc4de --- /dev/null +++ b/provider/resource_keycloak_hardcoded_attribute_mapper.go @@ -0,0 +1,130 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func resourceKeycloakHardcodedAttributeMapper() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKeycloakHardcodedAttributeMapperCreate, + ReadContext: resourceKeycloakHardcodedAttributeMapperRead, + UpdateContext: resourceKeycloakHardcodedAttributeMapperUpdate, + DeleteContext: resourceKeycloakHardcodedAttributeMapperDelete, + // This resource can be imported using {{realm}}/{{provider_id}}/{{mapper_id}}. The Provider and Mapper IDs are displayed in the GUI + Importer: &schema.ResourceImporter{ + StateContext: resourceKeycloakLdapGenericMapperImport, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Display name of the mapper when displayed in the console.", + }, + "realm_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The realm in which the ldap user federation provider exists.", + }, + "ldap_user_federation_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The ldap user federation provider to attach this mapper to.", + }, + "attribute_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the user schema attribute", + }, + "attribute_value": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Value of the attribute. You can hardcode any value like 'foo'", + }, + }, + } +} + +func getHardcodedAttributeMapperFromData(data *schema.ResourceData) *keycloak.HardcodedAttributeMapper { + return &keycloak.HardcodedAttributeMapper{ + Id: data.Id(), + Name: data.Get("name").(string), + RealmId: data.Get("realm_id").(string), + LdapUserFederationId: data.Get("ldap_user_federation_id").(string), + AttributeName: data.Get("attribute_name").(string), + AttributeValue: data.Get("attribute_value").(string), + } +} + +func setHardcodedAttributeMapperData(data *schema.ResourceData, ldapMapper *keycloak.HardcodedAttributeMapper) { + data.SetId(ldapMapper.Id) + data.Set("name", ldapMapper.Name) + data.Set("realm_id", ldapMapper.RealmId) + data.Set("ldap_user_federation_id", ldapMapper.LdapUserFederationId) + data.Set("attribute_name", ldapMapper.AttributeName) + data.Set("attribute_value", ldapMapper.AttributeValue) +} + +func resourceKeycloakHardcodedAttributeMapperCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + ldapMapper := getHardcodedAttributeMapperFromData(data) + + err := keycloakClient.NewHardcodedAttributeMapper(ctx, ldapMapper) + if err != nil { + return diag.FromErr(err) + } + + setHardcodedAttributeMapperData(data, ldapMapper) + + return resourceKeycloakHardcodedAttributeMapperRead(ctx, data, meta) +} + +func resourceKeycloakHardcodedAttributeMapperRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + ldapMapper, err := keycloakClient.GetHardcodedAttributeMapper(ctx, realmId, id) + if err != nil { + return handleNotFoundError(ctx, err, data) + } + + setHardcodedAttributeMapperData(data, ldapMapper) + + return diag.FromErr(nil) +} + +func resourceKeycloakHardcodedAttributeMapperUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + ldapMapper := getHardcodedAttributeMapperFromData(data) + + err := keycloakClient.UpdateHardcodedAttributeMapper(ctx, ldapMapper) + if err != nil { + return diag.FromErr(err) + } + + setHardcodedAttributeMapperData(data, ldapMapper) + + return diag.FromErr(nil) +} + +func resourceKeycloakHardcodedAttributeMapperDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + err := keycloakClient.DeleteHardcodedAttributeMapper(ctx, realmId, id) + + return diag.FromErr(err) +} diff --git a/provider/resource_keycloak_hardcoded_attribute_mapper_test.go b/provider/resource_keycloak_hardcoded_attribute_mapper_test.go new file mode 100644 index 00000000..193ec75d --- /dev/null +++ b/provider/resource_keycloak_hardcoded_attribute_mapper_test.go @@ -0,0 +1,165 @@ +package provider + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func TestAccKeycloakHardcodedAttributeMapper_basic(t *testing.T) { + t.Parallel() + attributeName := acctest.RandomWithPrefix("tf-acc") + attributeValue := acctest.RandomWithPrefix("tf-acc") + attributeMapperName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakHardcodedAttributeMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakHardcodedAttributeMapper(attributeMapperName, attributeName, attributeValue), + Check: testAccCheckKeycloakHardcodedAttributeMapperExists("keycloak_hardcoded_attribute_mapper.hardcoded_attribute_mapper"), + }, + { + ResourceName: "keycloak_hardcoded_attribute_mapper.hardcoded_attribute_mapper", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getLdapGenericMapperImportId("keycloak_hardcoded_attribute_mapper.hardcoded_attribute_mapper"), + }, + }, + }) +} + +func TestAccKeycloakHardcodedAttributeMapper_createAfterManualDestroy(t *testing.T) { + t.Parallel() + var mapper = &keycloak.HardcodedAttributeMapper{} + + attributeName := acctest.RandomWithPrefix("tf-acc") + attributeValue := acctest.RandomWithPrefix("tf-acc") + attributeMapperName := acctest.RandomWithPrefix("tf-acc") + + resource.Test(t, resource.TestCase{ + ProviderFactories: testAccProviderFactories, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakHardcodedAttributeMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakHardcodedAttributeMapper(attributeMapperName, attributeName, attributeValue), + Check: testAccCheckKeycloakHardcodedAttributeMapperFetch("keycloak_hardcoded_attribute_mapper.hardcoded_attribute_mapper", mapper), + }, + { + PreConfig: func() { + err := keycloakClient.DeleteHardcodedAttributeMapper(context.Background(), mapper.RealmId, mapper.Id) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakHardcodedAttributeMapper(attributeMapperName, attributeName, attributeValue), + Check: testAccCheckKeycloakHardcodedAttributeMapperExists("keycloak_hardcoded_attribute_mapper.hardcoded_attribute_mapper"), + }, + }, + }) +} + +func testAccCheckKeycloakHardcodedAttributeMapperExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, err := getHardcodedAttributeMapperFromState(s, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckKeycloakHardcodedAttributeMapperFetch(resourceName string, mapper *keycloak.HardcodedAttributeMapper) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedMapper, err := getHardcodedAttributeMapperFromState(s, resourceName) + if err != nil { + return err + } + + mapper.Id = fetchedMapper.Id + mapper.RealmId = fetchedMapper.RealmId + + return nil + } +} + +func testAccCheckKeycloakHardcodedAttributeMapperDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_hardcoded_attribute_mapper" { + continue + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + ldapMapper, _ := keycloakClient.GetHardcodedAttributeMapper(context.Background(), realm, id) + if ldapMapper != nil { + return fmt.Errorf("Hardcoded attribute mapper with id %s still exists", id) + } + } + + return nil + } +} + +func getHardcodedAttributeMapperFromState(s *terraform.State, resourceName string) (*keycloak.HardcodedAttributeMapper, error) { + 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"] + + ldapMapper, err := keycloakClient.GetHardcodedAttributeMapper(context.Background(), realm, id) + if err != nil { + return nil, fmt.Errorf("error getting attribute mapper with id %s: %s", id, err) + } + + return ldapMapper, nil +} + +func testKeycloakHardcodedAttributeMapper(attributeMapperName, attributeName, attributeValue string) string { + return fmt.Sprintf(` +data "keycloak_realm" "realm" { + realm = "%s" +} + +resource "keycloak_ldap_user_federation" "openldap" { + name = "openldap" + realm_id = data.keycloak_realm.realm.id + + enabled = true + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "entryDN" + user_object_classes = [ + "simpleSecurityObject", + "organizationalRole" + ] + connection_url = "ldap://openldap" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_hardcoded_attribute_mapper" "hardcoded_attribute_mapper" { + name = "%s" + realm_id = data.keycloak_realm.realm.id + ldap_user_federation_id = keycloak_ldap_user_federation.openldap.id + attribute_name = "%s" + attribute_value = "%s" +} + `, testAccRealmUserFederation.Realm, attributeMapperName, attributeName, attributeValue) +}