diff --git a/docs/resources/keycloak_ldap_msad_lds_user_account_control_mapper.md b/docs/resources/keycloak_ldap_msad_lds_user_account_control_mapper.md new file mode 100644 index 000000000..4a119a4ce --- /dev/null +++ b/docs/resources/keycloak_ldap_msad_lds_user_account_control_mapper.md @@ -0,0 +1,60 @@ +# keycloak_ldap_msad_lds_user_account_control_mapper + +Allows for creating and managing MSAD-LDS user account control mappers for Keycloak +users federated via LDAP. + +The MSAD-LDS (Microsoft Active Directory Lightweight Directory Service) user account control mapper is specific +to LDAP user federation providers that are pulling from AD-LDS, and it can propagate +AD-LDS user state to Keycloak in order to enforce settings like expired passwords +or disabled accounts. + +### Example Usage + +```hcl +resource "keycloak_realm" "realm" { + realm = "test" + enabled = true +} + +resource "keycloak_ldap_user_federation" "ldap_user_federation" { + name = "ad" + realm_id = "${keycloak_realm.realm.id}" + + username_ldap_attribute = "cn" + rdn_ldap_attribute = "cn" + uuid_ldap_attribute = "objectGUID" + user_object_classes = [ + "person", + "organizationalPerson", + "user" + ] + connection_url = "ldap://my-ad-server" + users_dn = "dc=example,dc=org" + bind_dn = "cn=admin,dc=example,dc=org" + bind_credential = "admin" +} + +resource "keycloak_ldap_msad_lds_user_account_control_mapper" "msad_lds_user_account_control_mapper" { + realm_id = "${keycloak_realm.realm.id}" + ldap_user_federation_id = "${keycloak_ldap_user_federation.ldap_user_federation.id}" + name = "msad-lds-user-account-control-mapper" +} +``` + +### Argument Reference + +The following arguments are supported: + +- `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. + +### Import + +LDAP mappers can be imported using the format `{{realm_id}}/{{ldap_user_federation_id}}/{{ldap_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: + +```bash +$ terraform import keycloak_ldap_msad_lds_user_account_control_mapper.msad_lds_user_account_control_mapper my-realm/af2a6ca3-e4d7-49c3-b08b-1b3c70b4b860/3d923ece-1a91-4bf7-adaf-3b82f2a12b67 +``` diff --git a/example/main.tf b/example/main.tf index 9ece70f1d..5b1cefd94 100644 --- a/example/main.tf +++ b/example/main.tf @@ -278,6 +278,12 @@ resource "keycloak_ldap_msad_user_account_control_mapper" "msad_uac_mapper" { ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap.id}" } +resource "keycloak_ldap_msad_lds_user_account_control_mapper" "msad_lds_uac_mapper" { + name = "msad-lds-uac-mapper" + realm_id = "${keycloak_ldap_user_federation.openldap.realm_id}" + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap.id}" +} + resource "keycloak_ldap_full_name_mapper" "full_name_mapper" { name = "full-name-mapper" realm_id = "${keycloak_ldap_user_federation.openldap.realm_id}" diff --git a/keycloak/ldap_msad_lds_user_account_control_mapper.go b/keycloak/ldap_msad_lds_user_account_control_mapper.go new file mode 100644 index 000000000..9b3ecb5bc --- /dev/null +++ b/keycloak/ldap_msad_lds_user_account_control_mapper.go @@ -0,0 +1,61 @@ +package keycloak + +import ( + "fmt" +) + +type LdapMsadLdsUserAccountControlMapper struct { + Id string + Name string + RealmId string + LdapUserFederationId string +} + +func convertFromLdapMsadLdsUserAccountControlMapperToComponent(ldapMsadLdsUserAccountControlMapper *LdapMsadLdsUserAccountControlMapper) *component { + return &component{ + Id: ldapMsadLdsUserAccountControlMapper.Id, + Name: ldapMsadLdsUserAccountControlMapper.Name, + ProviderId: "msad-lds-user-account-control-mapper", + ProviderType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + ParentId: ldapMsadLdsUserAccountControlMapper.LdapUserFederationId, + } +} + +func convertFromComponentToLdapMsadLdsUserAccountControlMapper(component *component, realmId string) (*LdapMsadLdsUserAccountControlMapper, error) { + return &LdapMsadLdsUserAccountControlMapper{ + Id: component.Id, + Name: component.Name, + RealmId: realmId, + LdapUserFederationId: component.ParentId, + }, nil +} + +func (keycloakClient *KeycloakClient) NewLdapMsadLdsUserAccountControlMapper(ldapMsadLdsUserAccountControlMapper *LdapMsadLdsUserAccountControlMapper) error { + _, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/components", ldapMsadLdsUserAccountControlMapper.RealmId), convertFromLdapMsadLdsUserAccountControlMapperToComponent(ldapMsadLdsUserAccountControlMapper)) + if err != nil { + return err + } + + ldapMsadLdsUserAccountControlMapper.Id = getIdFromLocationHeader(location) + + return nil +} + +func (keycloakClient *KeycloakClient) GetLdapMsadLdsUserAccountControlMapper(realmId, id string) (*LdapMsadLdsUserAccountControlMapper, error) { + var component *component + + err := keycloakClient.get(fmt.Sprintf("/realms/%s/components/%s", realmId, id), &component, nil) + if err != nil { + return nil, err + } + + return convertFromComponentToLdapMsadLdsUserAccountControlMapper(component, realmId) +} + +func (keycloakClient *KeycloakClient) UpdateLdapMsadLdsUserAccountControlMapper(ldapMsadLdsUserAccountControlMapper *LdapMsadLdsUserAccountControlMapper) error { + return keycloakClient.put(fmt.Sprintf("/realms/%s/components/%s", ldapMsadLdsUserAccountControlMapper.RealmId, ldapMsadLdsUserAccountControlMapper.Id), convertFromLdapMsadLdsUserAccountControlMapperToComponent(ldapMsadLdsUserAccountControlMapper)) +} + +func (keycloakClient *KeycloakClient) DeleteLdapMsadLdsUserAccountControlMapper(realmId, id string) error { + return keycloakClient.delete(fmt.Sprintf("/realms/%s/components/%s", realmId, id), nil) +} diff --git a/provider/provider.go b/provider/provider.go index 58d5819c4..6ecc05327 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -32,6 +32,7 @@ func KeycloakProvider() *schema.Provider { "keycloak_ldap_group_mapper": resourceKeycloakLdapGroupMapper(), "keycloak_ldap_hardcoded_role_mapper": resourceKeycloakLdapHardcodedRoleMapper(), "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(), diff --git a/provider/resource_keycloak_ldap_msad_lds_user_account_control_mapper.go b/provider/resource_keycloak_ldap_msad_lds_user_account_control_mapper.go new file mode 100644 index 000000000..aab21da49 --- /dev/null +++ b/provider/resource_keycloak_ldap_msad_lds_user_account_control_mapper.go @@ -0,0 +1,111 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" +) + +func resourceKeycloakLdapMsadLdsUserAccountControlMapper() *schema.Resource { + return &schema.Resource{ + Create: resourceKeycloakLdapMsadLdsUserAccountControlMapperCreate, + Read: resourceKeycloakLdapMsadLdsUserAccountControlMapperRead, + Update: resourceKeycloakLdapMsadLdsUserAccountControlMapperUpdate, + Delete: resourceKeycloakLdapMsadLdsUserAccountControlMapperDelete, + // This resource can be imported using {{realm}}/{{provider_id}}/{{mapper_id}}. The Provider and Mapper IDs are displayed in the GUI + Importer: &schema.ResourceImporter{ + State: 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.", + }, + }, + } +} + +func getLdapMsadLdsUserAccountControlMapperFromData(data *schema.ResourceData) *keycloak.LdapMsadLdsUserAccountControlMapper { + return &keycloak.LdapMsadLdsUserAccountControlMapper{ + Id: data.Id(), + Name: data.Get("name").(string), + RealmId: data.Get("realm_id").(string), + LdapUserFederationId: data.Get("ldap_user_federation_id").(string), + } +} + +func setLdapMsadLdsUserAccountControlMapperData(data *schema.ResourceData, ldapMsadLdsUserAccountControlMapper *keycloak.LdapMsadLdsUserAccountControlMapper) { + data.SetId(ldapMsadLdsUserAccountControlMapper.Id) + + data.Set("id", ldapMsadLdsUserAccountControlMapper.Id) + data.Set("name", ldapMsadLdsUserAccountControlMapper.Name) + data.Set("realm_id", ldapMsadLdsUserAccountControlMapper.RealmId) + data.Set("ldap_user_federation_id", ldapMsadLdsUserAccountControlMapper.LdapUserFederationId) +} + +func resourceKeycloakLdapMsadLdsUserAccountControlMapperCreate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + ldapMsadLdsUserAccountControlMapper := getLdapMsadLdsUserAccountControlMapperFromData(data) + + err := keycloakClient.NewLdapMsadLdsUserAccountControlMapper(ldapMsadLdsUserAccountControlMapper) + if err != nil { + return err + } + + setLdapMsadLdsUserAccountControlMapperData(data, ldapMsadLdsUserAccountControlMapper) + + return resourceKeycloakLdapMsadLdsUserAccountControlMapperRead(data, meta) +} + +func resourceKeycloakLdapMsadLdsUserAccountControlMapperRead(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + ldapMsadLdsUserAccountControlMapper, err := keycloakClient.GetLdapMsadLdsUserAccountControlMapper(realmId, id) + if err != nil { + return handleNotFoundError(err, data) + } + + setLdapMsadLdsUserAccountControlMapperData(data, ldapMsadLdsUserAccountControlMapper) + + return nil +} + +func resourceKeycloakLdapMsadLdsUserAccountControlMapperUpdate(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + ldapMsadLdsUserAccountControlMapper := getLdapMsadLdsUserAccountControlMapperFromData(data) + + err := keycloakClient.UpdateLdapMsadLdsUserAccountControlMapper(ldapMsadLdsUserAccountControlMapper) + if err != nil { + return err + } + + setLdapMsadLdsUserAccountControlMapperData(data, ldapMsadLdsUserAccountControlMapper) + + return nil +} + +func resourceKeycloakLdapMsadLdsUserAccountControlMapperDelete(data *schema.ResourceData, meta interface{}) error { + keycloakClient := meta.(*keycloak.KeycloakClient) + + realmId := data.Get("realm_id").(string) + id := data.Id() + + return keycloakClient.DeleteLdapMsadLdsUserAccountControlMapper(realmId, id) +} diff --git a/provider/resource_keycloak_ldap_msad_lds_user_account_control_mapper_test.go b/provider/resource_keycloak_ldap_msad_lds_user_account_control_mapper_test.go new file mode 100644 index 000000000..69b288d05 --- /dev/null +++ b/provider/resource_keycloak_ldap_msad_lds_user_account_control_mapper_test.go @@ -0,0 +1,277 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/mrparkers/terraform-provider-keycloak/keycloak" + "testing" +) + +func TestAccKeycloakLdapMsadLdsUserAccountControlMapper_basic(t *testing.T) { + realmName := "terraform-" + acctest.RandString(10) + msadLdsUacMapperName := "terraform-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapMsadLdsUserAccountControlMapper_basic(realmName, msadLdsUacMapperName), + Check: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperExists("keycloak_ldap_msad_lds_user_account_control_mapper.uac_mapper"), + }, + { + ResourceName: "keycloak_ldap_msad_lds_user_account_control_mapper.uac_mapper", + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: getLdapGenericMapperImportId("keycloak_ldap_msad_lds_user_account_control_mapper.uac_mapper"), + }, + }, + }) +} + +func TestAccKeycloakLdapMsadLdsUserAccountControlMapper_createAfterManualDestroy(t *testing.T) { + var mapper = &keycloak.LdapMsadLdsUserAccountControlMapper{} + + realmName := "terraform-" + acctest.RandString(10) + msadLdsUacMapperName := "terraform-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapMsadLdsUserAccountControlMapper_basic(realmName, msadLdsUacMapperName), + Check: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperFetch("keycloak_ldap_msad_lds_user_account_control_mapper.uac_mapper", mapper), + }, + { + PreConfig: func() { + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + err := keycloakClient.DeleteLdapMsadLdsUserAccountControlMapper(mapper.RealmId, mapper.Id) + if err != nil { + t.Fatal(err) + } + }, + Config: testKeycloakLdapMsadLdsUserAccountControlMapper_basic(realmName, msadLdsUacMapperName), + Check: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperExists("keycloak_ldap_msad_lds_user_account_control_mapper.uac_mapper"), + }, + }, + }) +} + +func TestAccKeycloakLdapMsadLdsUserAccountControlMapper_updateLdapUserFederation(t *testing.T) { + realmOne := "terraform-" + acctest.RandString(10) + realmTwo := "terraform-" + acctest.RandString(10) + msadLdsUacMapperName := "terraform-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapMsadLdsUserAccountControlMapper_updateLdapUserFederationBefore(realmOne, realmTwo, msadLdsUacMapperName), + Check: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperExists("keycloak_ldap_msad_lds_user_account_control_mapper.uac_mapper"), + }, + { + Config: testKeycloakLdapMsadLdsUserAccountControlMapper_updateLdapUserFederationAfter(realmOne, realmTwo, msadLdsUacMapperName), + Check: testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperExists("keycloak_ldap_msad_lds_user_account_control_mapper.uac_mapper"), + }, + }, + }) +} + +func testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, err := getLdapMsadLdsUserAccountControlMapperFromState(s, resourceName) + if err != nil { + return err + } + + return nil + } +} + +func testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperFetch(resourceName string, mapper *keycloak.LdapMsadLdsUserAccountControlMapper) resource.TestCheckFunc { + return func(s *terraform.State) error { + fetchedMapper, err := getLdapMsadLdsUserAccountControlMapperFromState(s, resourceName) + if err != nil { + return err + } + + mapper.Id = fetchedMapper.Id + mapper.RealmId = fetchedMapper.RealmId + + return nil + } +} + +func testAccCheckKeycloakLdapMsadLdsUserAccountControlMapperDestroy() resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "keycloak_ldap_msad_lds_user_account_control_mapper" { + continue + } + + id := rs.Primary.ID + realm := rs.Primary.Attributes["realm_id"] + + keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient) + + ldapMsadLdsUserAccountControlMapper, _ := keycloakClient.GetLdapMsadLdsUserAccountControlMapper(realm, id) + if ldapMsadLdsUserAccountControlMapper != nil { + return fmt.Errorf("ldap msad-lds uac mapper with id %s still exists", id) + } + } + + return nil + } +} + +func getLdapMsadLdsUserAccountControlMapperFromState(s *terraform.State, resourceName string) (*keycloak.LdapMsadLdsUserAccountControlMapper, 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"] + + ldapMsadLdsUserAccountControlMapper, err := keycloakClient.GetLdapMsadLdsUserAccountControlMapper(realm, id) + if err != nil { + return nil, fmt.Errorf("error getting ldap msad-lds uac mapper with id %s: %s", id, err) + } + + return ldapMsadLdsUserAccountControlMapper, nil +} + +func testKeycloakLdapMsadLdsUserAccountControlMapper_basic(realm, msadLdsUacMapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm" { + realm = "%s" +} +resource "keycloak_ldap_user_federation" "openldap" { + name = "openldap" + realm_id = "${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_ldap_msad_lds_user_account_control_mapper" "uac_mapper" { + name = "%s" + realm_id = "${keycloak_realm.realm.id}" + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap.id}" +} + `, realm, msadLdsUacMapperName) +} + +func testKeycloakLdapMsadLdsUserAccountControlMapper_updateLdapUserFederationBefore(realmOne, realmTwo, msadLdsUacMapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm_one" { + realm = "%s" +} +resource "keycloak_realm" "realm_two" { + realm = "%s" +} +resource "keycloak_ldap_user_federation" "openldap_one" { + name = "openldap" + realm_id = "${keycloak_realm.realm_one.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_ldap_user_federation" "openldap_two" { + name = "openldap" + realm_id = "${keycloak_realm.realm_two.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_ldap_msad_lds_user_account_control_mapper" "uac_mapper" { + name = "%s" + realm_id = "${keycloak_realm.realm_one.id}" + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap_one.id}" +} + `, realmOne, realmTwo, msadLdsUacMapperName) +} + +func testKeycloakLdapMsadLdsUserAccountControlMapper_updateLdapUserFederationAfter(realmOne, realmTwo, msadLdsUacMapperName string) string { + return fmt.Sprintf(` +resource "keycloak_realm" "realm_one" { + realm = "%s" +} +resource "keycloak_realm" "realm_two" { + realm = "%s" +} +resource "keycloak_ldap_user_federation" "openldap_one" { + name = "openldap" + realm_id = "${keycloak_realm.realm_one.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_ldap_user_federation" "openldap_two" { + name = "openldap" + realm_id = "${keycloak_realm.realm_two.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_ldap_msad_lds_user_account_control_mapper" "uac_mapper" { + name = "%s" + realm_id = "${keycloak_realm.realm_two.id}" + ldap_user_federation_id = "${keycloak_ldap_user_federation.openldap_two.id}" +} + `, realmOne, realmTwo, msadLdsUacMapperName) +}