diff --git a/docs/resources/keycloak_ldap_user_federation.md b/docs/resources/keycloak_ldap_user_federation.md index 2a90cc11b..94816ee47 100644 --- a/docs/resources/keycloak_ldap_user_federation.md +++ b/docs/resources/keycloak_ldap_user_federation.md @@ -35,6 +35,12 @@ resource "keycloak_ldap_user_federation" "ldap_user_federation" { connection_timeout = "5s" read_timeout = "10s" + + kerberos { + kerberos_realm = "FOO.LOCAL" + server_principal = "HTTP/host.foo.com@FOO.LOCAL" + keytab = "/etc/host.keytab" + } } ``` @@ -74,6 +80,11 @@ The following arguments are supported: - `full_sync_period` - (Optional) How frequently Keycloak should sync all LDAP users, in seconds. Omit this property to disable periodic full sync. - `changed_sync_period` - (Optional) How frequently Keycloak should sync changed LDAP users, in seconds. Omit this property to disable periodic changed users sync. - `cache_policy` - (Optional) Can be one of `DEFAULT`, `EVICT_DAILY`, `EVICT_WEEKLY`, `MAX_LIFESPAN`, or `NO_CACHE`. Defaults to `DEFAULT`. +- `kerberos` - (Optional) A block containing the kerberos settings. + - `kerberos_realm` - (Required) The name of the kerberos realm, e.g. FOO.LOCAL. + - `server_principal` - (Required) The kerberos server principal, e.g. 'HTTP/host.foo.com@FOO.LOCAL'. + - `key_tab` - (Required) Path to the kerberos keytab file on the server with credentials of the service principal. + - `use_kerberos_for_password_authentication` - (Optional) Use kerberos login module instead of ldap service api. Defaults to `false`. ### Import diff --git a/example/main.tf b/example/main.tf index ca66c7a7c..8d7c40868 100644 --- a/example/main.tf +++ b/example/main.tf @@ -242,6 +242,13 @@ resource "keycloak_ldap_user_federation" "openldap" { connection_timeout = "5s" read_timeout = "10s" + kerberos { + server_principal = "HTTP/keycloak.local@FOO.LOCAL" + use_kerberos_for_password_authentication = false + key_tab = "/etc/keycloak.keytab" + kerberos_realm = "FOO.LOCAL" + } + cache_policy = "NO_CACHE" } diff --git a/keycloak/ldap_user_federation.go b/keycloak/ldap_user_federation.go index ae9554182..b9d3aafcb 100644 --- a/keycloak/ldap_user_federation.go +++ b/keycloak/ldap_user_federation.go @@ -36,6 +36,12 @@ type LdapUserFederation struct { ReadTimeout string // duration string (ex: 1h30m) Pagination bool + ServerPrincipal string + UseKerberosForPasswordAuthentication bool + AllowKerberosAuthentication bool + KeyTab string + KerberosRealm string + BatchSizeForSync int FullSyncPeriod int // either a number, in milliseconds, or -1 if full sync is disabled ChangedSyncPeriod int // either a number, in milliseconds, or -1 if changed sync is disabled @@ -102,6 +108,22 @@ func convertFromLdapUserFederationToComponent(ldap *LdapUserFederation) (*compon "changedSyncPeriod": { strconv.Itoa(ldap.ChangedSyncPeriod), }, + + "serverPrincipal": { + ldap.ServerPrincipal, + }, + "useKerberosForPasswordAuthentication": { + strconv.FormatBool(ldap.UseKerberosForPasswordAuthentication), + }, + "allowKerberosAuthentication": { + strconv.FormatBool(ldap.AllowKerberosAuthentication), + }, + "keyTab": { + ldap.KeyTab, + }, + "kerberosRealm": { + ldap.KerberosRealm, + }, } if ldap.BindDn != "" && ldap.BindCredential != "" { @@ -209,6 +231,16 @@ func convertFromComponentToLdapUserFederation(component *component) (*LdapUserFe return nil, err } + useKerberosForPasswordAuthentication, err := parseBoolAndTreatEmptyStringAsFalse(component.getConfig("useKerberosForPasswordAuthentication")) + if err != nil { + return nil, err + } + + allowKerberosAuthentication, err := parseBoolAndTreatEmptyStringAsFalse(component.getConfig("allowKerberosAuthentication")) + if err != nil { + return nil, err + } + ldap := &LdapUserFederation{ Id: component.Id, Name: component.Name, @@ -237,6 +269,12 @@ func convertFromComponentToLdapUserFederation(component *component) (*LdapUserFe UseTruststoreSpi: component.getConfig("useTruststoreSpi"), Pagination: pagination, + ServerPrincipal: component.getConfig("serverPrincipal"), + UseKerberosForPasswordAuthentication: useKerberosForPasswordAuthentication, + AllowKerberosAuthentication: allowKerberosAuthentication, + KeyTab: component.getConfig("keyTab"), + KerberosRealm: component.getConfig("kerberosRealm"), + BatchSizeForSync: batchSizeForSync, FullSyncPeriod: fullSyncPeriod, ChangedSyncPeriod: changedSyncPeriod, diff --git a/provider/resource_keycloak_ldap_user_federation.go b/provider/resource_keycloak_ldap_user_federation.go index d20d105e4..84b8cd674 100644 --- a/provider/resource_keycloak_ldap_user_federation.go +++ b/provider/resource_keycloak_ldap_user_federation.go @@ -2,10 +2,11 @@ package provider import ( "fmt" + "strings" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" "github.com/mrparkers/terraform-provider-keycloak/keycloak" - "strings" ) var ( @@ -189,6 +190,38 @@ func resourceKeycloakLdapUserFederation() *schema.Resource { Description: "How frequently Keycloak should sync changed LDAP users, in seconds. Omit this property to disable periodic changed users sync.", }, + "kerberos": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Description: "Settings regarding kerberos authentication for this realm.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "kerberos_realm": { + Type: schema.TypeString, + Required: true, + Description: "The name of the kerberos realm, e.g. FOO.LOCAL", + }, + "server_principal": { + Type: schema.TypeString, + Required: true, + Description: "The kerberos server principal, e.g. 'HTTP/host.foo.com@FOO.LOCAL'.", + }, + "key_tab": { + Type: schema.TypeString, + Required: true, + Description: "Path to the kerberos keytab file on the server with credentials of the service principal.", + }, + "use_kerberos_for_password_authentication": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Use kerberos login module instead of ldap service api. Defaults to `false`.", + }, + }, + }, + }, + "cache_policy": { Type: schema.TypeString, Optional: true, @@ -219,7 +252,7 @@ func getLdapUserFederationFromData(data *schema.ResourceData) *keycloak.LdapUser userObjectClasses = append(userObjectClasses, userObjectClass.(string)) } - return &keycloak.LdapUserFederation{ + ldapUserFederation := &keycloak.LdapUserFederation{ Id: data.Id(), Name: data.Get("name").(string), RealmId: data.Get("realm_id").(string), @@ -255,6 +288,21 @@ func getLdapUserFederationFromData(data *schema.ResourceData) *keycloak.LdapUser CachePolicy: data.Get("cache_policy").(string), } + + if kerberos, ok := data.GetOk("kerberos"); ok { + ldapUserFederation.AllowKerberosAuthentication = true + kerberosSettingsData := kerberos.(*schema.Set).List()[0] + kerberosSettings := kerberosSettingsData.(map[string]interface{}) + + ldapUserFederation.KerberosRealm = kerberosSettings["kerberos_realm"].(string) + ldapUserFederation.ServerPrincipal = kerberosSettings["server_principal"].(string) + ldapUserFederation.UseKerberosForPasswordAuthentication = kerberosSettings["use_kerberos_for_password_authentication"].(bool) + ldapUserFederation.KeyTab = kerberosSettings["key_tab"].(string) + } else { + ldapUserFederation.AllowKerberosAuthentication = false + } + + return ldapUserFederation } func setLdapUserFederationData(data *schema.ResourceData, ldap *keycloak.LdapUserFederation) { @@ -288,6 +336,20 @@ func setLdapUserFederationData(data *schema.ResourceData, ldap *keycloak.LdapUse data.Set("read_timeout", ldap.ReadTimeout) data.Set("pagination", ldap.Pagination) + if ldap.AllowKerberosAuthentication { + kerberosSettingsData := make([]interface{}, 1) + kerberosSettings := make(map[string]interface{}) + kerberosSettingsData[0] = kerberosSettings + + kerberosSettings["server_principal"] = ldap.ServerPrincipal + kerberosSettings["use_kerberos_for_password_authentication"] = ldap.UseKerberosForPasswordAuthentication + kerberosSettings["allow_kerberos_authentication"] = ldap.AllowKerberosAuthentication + kerberosSettings["key_tab"] = ldap.KeyTab + kerberosSettings["kerberos_realm"] = ldap.KerberosRealm + + data.Set("kerberos", kerberosSettingsData) + } + data.Set("batch_size_for_sync", ldap.BatchSizeForSync) data.Set("full_sync_period", ldap.FullSyncPeriod) data.Set("changed_sync_period", ldap.ChangedSyncPeriod) diff --git a/provider/resource_keycloak_ldap_user_federation_test.go b/provider/resource_keycloak_ldap_user_federation_test.go index e472fa742..bfb48d81e 100644 --- a/provider/resource_keycloak_ldap_user_federation_test.go +++ b/provider/resource_keycloak_ldap_user_federation_test.go @@ -2,13 +2,14 @@ package provider import ( "fmt" + "regexp" + "strconv" + "testing" + "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" - "strconv" - "testing" ) func TestAccKeycloakLdapUserFederation_basic(t *testing.T) { @@ -122,6 +123,97 @@ func TestAccKeycloakLdapUserFederation_basicUpdateRealm(t *testing.T) { }) } +func generateRandomLdapKerberos(enabled bool) *keycloak.LdapUserFederation { + connectionTimeout, _ := keycloak.GetDurationStringFromMilliseconds(strconv.Itoa(acctest.RandIntRange(1, 3600) * 1000)) + readTimeout, _ := keycloak.GetDurationStringFromMilliseconds(strconv.Itoa(acctest.RandIntRange(1, 3600) * 1000)) + + return &keycloak.LdapUserFederation{ + RealmId: acctest.RandString(10), + Name: "terraform-" + acctest.RandString(10), + Enabled: enabled, + UsernameLDAPAttribute: acctest.RandString(10), + UuidLDAPAttribute: acctest.RandString(10), + UserObjectClasses: []string{acctest.RandString(10), acctest.RandString(10), acctest.RandString(10)}, + ConnectionUrl: "ldap://" + acctest.RandString(10), + UsersDn: acctest.RandString(10), + BindDn: acctest.RandString(10), + BindCredential: acctest.RandString(10), + SearchScope: randomStringInSlice([]string{"ONE_LEVEL", "SUBTREE"}), + ValidatePasswordPolicy: true, + UseTruststoreSpi: randomStringInSlice([]string{"ALWAYS", "ONLY_FOR_LDAPS", "NEVER"}), + ConnectionTimeout: connectionTimeout, + ReadTimeout: readTimeout, + Pagination: true, + BatchSizeForSync: acctest.RandIntRange(50, 10000), + FullSyncPeriod: acctest.RandIntRange(1, 3600), + ChangedSyncPeriod: acctest.RandIntRange(1, 3600), + CachePolicy: randomStringInSlice([]string{"DEFAULT", "EVICT_DAILY", "EVICT_WEEKLY", "MAX_LIFESPAN", "NO_CACHE"}), + ServerPrincipal: acctest.RandString(10), + UseKerberosForPasswordAuthentication: randomBool(), + AllowKerberosAuthentication: true, + KeyTab: acctest.RandString(10), + KerberosRealm: acctest.RandString(10), + } +} + +func checkMatchingNestedKey(resourcePath string, blockName string, fieldInBlock string, value string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[resourcePath] + if !ok { + return fmt.Errorf("Could not find resource %s", resourcePath) + } + + matchExpression := fmt.Sprintf(`%s\.\d\.mappings\.\d+\.%s`, blockName, fieldInBlock) + + for k, v := range resource.Primary.Attributes { + if isMatch, _ := regexp.Match(matchExpression, []byte(k)); isMatch { + if v == value { + return nil + } + + return fmt.Errorf("Value for attribute %s.%s does match: %s != %s", blockName, fieldInBlock, v, value) + } + } + + return nil + } +} + +func TestAccKeycloakLdapUserFederation_basicUpdateKerberosSettings(t *testing.T) { + firstLdap := generateRandomLdapKerberos(true) + secondLdap := generateRandomLdapKerberos(false) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testAccCheckKeycloakLdapUserFederationDestroy(), + Steps: []resource.TestStep{ + { + Config: testKeycloakLdapUserFederation_basicFromInterface(firstLdap), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakLdapUserFederationExists("keycloak_ldap_user_federation.openldap"), + resource.TestCheckResourceAttr("keycloak_ldap_user_federation.openldap", "realm_id", firstLdap.RealmId), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "kerberos_realm", firstLdap.KerberosRealm), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "server_principal", firstLdap.ServerPrincipal), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "use_kerberos_for_password_authentication", strconv.FormatBool(firstLdap.UseKerberosForPasswordAuthentication)), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "key_tab", firstLdap.KeyTab), + ), + }, + { + Config: testKeycloakLdapUserFederation_basicFromInterface(secondLdap), + Check: resource.ComposeTestCheckFunc( + testAccCheckKeycloakLdapUserFederationExists("keycloak_ldap_user_federation.openldap"), + resource.TestCheckResourceAttr("keycloak_ldap_user_federation.openldap", "realm_id", secondLdap.RealmId), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "kerberos_realm", secondLdap.KerberosRealm), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "server_principal", secondLdap.ServerPrincipal), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "use_kerberos_for_password_authentication", strconv.FormatBool(secondLdap.UseKerberosForPasswordAuthentication)), + checkMatchingNestedKey("keycloak_ldap_user_federation.openldap", "kerberos", "key_tab", secondLdap.KeyTab), + ), + }, + }, + }) +} + func TestAccKeycloakLdapUserFederation_basicUpdateAll(t *testing.T) { realmName := "terraform-" + acctest.RandString(10) firstEnabled := randomBool() @@ -134,49 +226,59 @@ func TestAccKeycloakLdapUserFederation_basicUpdateAll(t *testing.T) { secondReadTimeout, _ := keycloak.GetDurationStringFromMilliseconds(strconv.Itoa(acctest.RandIntRange(1, 3600) * 1000)) firstLdap := &keycloak.LdapUserFederation{ - RealmId: realmName, - Name: "terraform-" + acctest.RandString(10), - Enabled: firstEnabled, - UsernameLDAPAttribute: acctest.RandString(10), - UuidLDAPAttribute: acctest.RandString(10), - UserObjectClasses: []string{acctest.RandString(10), acctest.RandString(10), acctest.RandString(10)}, - ConnectionUrl: "ldap://" + acctest.RandString(10), - UsersDn: acctest.RandString(10), - BindDn: acctest.RandString(10), - BindCredential: acctest.RandString(10), - SearchScope: randomStringInSlice([]string{"ONE_LEVEL", "SUBTREE"}), - ValidatePasswordPolicy: firstValidatePasswordPolicy, - UseTruststoreSpi: randomStringInSlice([]string{"ALWAYS", "ONLY_FOR_LDAPS", "NEVER"}), - ConnectionTimeout: firstConnectionTimeout, - ReadTimeout: firstReadTimeout, - Pagination: firstPagination, - BatchSizeForSync: acctest.RandIntRange(50, 10000), - FullSyncPeriod: acctest.RandIntRange(1, 3600), - ChangedSyncPeriod: acctest.RandIntRange(1, 3600), - CachePolicy: randomStringInSlice([]string{"DEFAULT", "EVICT_DAILY", "EVICT_WEEKLY", "MAX_LIFESPAN", "NO_CACHE"}), + RealmId: realmName, + Name: "terraform-" + acctest.RandString(10), + Enabled: firstEnabled, + UsernameLDAPAttribute: acctest.RandString(10), + UuidLDAPAttribute: acctest.RandString(10), + UserObjectClasses: []string{acctest.RandString(10), acctest.RandString(10), acctest.RandString(10)}, + ConnectionUrl: "ldap://" + acctest.RandString(10), + UsersDn: acctest.RandString(10), + BindDn: acctest.RandString(10), + BindCredential: acctest.RandString(10), + SearchScope: randomStringInSlice([]string{"ONE_LEVEL", "SUBTREE"}), + ValidatePasswordPolicy: firstValidatePasswordPolicy, + UseTruststoreSpi: randomStringInSlice([]string{"ALWAYS", "ONLY_FOR_LDAPS", "NEVER"}), + ConnectionTimeout: firstConnectionTimeout, + ReadTimeout: firstReadTimeout, + Pagination: firstPagination, + BatchSizeForSync: acctest.RandIntRange(50, 10000), + FullSyncPeriod: acctest.RandIntRange(1, 3600), + ChangedSyncPeriod: acctest.RandIntRange(1, 3600), + CachePolicy: randomStringInSlice([]string{"DEFAULT", "EVICT_DAILY", "EVICT_WEEKLY", "MAX_LIFESPAN", "NO_CACHE"}), + ServerPrincipal: acctest.RandString(10), + UseKerberosForPasswordAuthentication: randomBool(), + AllowKerberosAuthentication: randomBool(), + KeyTab: acctest.RandString(10), + KerberosRealm: acctest.RandString(10), } secondLdap := &keycloak.LdapUserFederation{ - RealmId: realmName, - Name: "terraform-" + acctest.RandString(10), - Enabled: !firstEnabled, - UsernameLDAPAttribute: acctest.RandString(10), - UuidLDAPAttribute: acctest.RandString(10), - UserObjectClasses: []string{acctest.RandString(10)}, - ConnectionUrl: "ldap://" + acctest.RandString(10), - UsersDn: acctest.RandString(10), - BindDn: acctest.RandString(10), - BindCredential: acctest.RandString(10), - SearchScope: randomStringInSlice([]string{"ONE_LEVEL", "SUBTREE"}), - ValidatePasswordPolicy: !firstValidatePasswordPolicy, - UseTruststoreSpi: randomStringInSlice([]string{"ALWAYS", "ONLY_FOR_LDAPS", "NEVER"}), - ConnectionTimeout: secondConnectionTimeout, - ReadTimeout: secondReadTimeout, - Pagination: !firstPagination, - BatchSizeForSync: acctest.RandIntRange(50, 10000), - FullSyncPeriod: acctest.RandIntRange(1, 3600), - ChangedSyncPeriod: acctest.RandIntRange(1, 3600), - CachePolicy: randomStringInSlice([]string{"DEFAULT", "EVICT_DAILY", "EVICT_WEEKLY", "MAX_LIFESPAN", "NO_CACHE"}), + RealmId: realmName, + Name: "terraform-" + acctest.RandString(10), + Enabled: !firstEnabled, + UsernameLDAPAttribute: acctest.RandString(10), + UuidLDAPAttribute: acctest.RandString(10), + UserObjectClasses: []string{acctest.RandString(10)}, + ConnectionUrl: "ldap://" + acctest.RandString(10), + UsersDn: acctest.RandString(10), + BindDn: acctest.RandString(10), + BindCredential: acctest.RandString(10), + SearchScope: randomStringInSlice([]string{"ONE_LEVEL", "SUBTREE"}), + ValidatePasswordPolicy: !firstValidatePasswordPolicy, + UseTruststoreSpi: randomStringInSlice([]string{"ALWAYS", "ONLY_FOR_LDAPS", "NEVER"}), + ConnectionTimeout: secondConnectionTimeout, + ReadTimeout: secondReadTimeout, + Pagination: !firstPagination, + BatchSizeForSync: acctest.RandIntRange(50, 10000), + FullSyncPeriod: acctest.RandIntRange(1, 3600), + ChangedSyncPeriod: acctest.RandIntRange(1, 3600), + CachePolicy: randomStringInSlice([]string{"DEFAULT", "EVICT_DAILY", "EVICT_WEEKLY", "MAX_LIFESPAN", "NO_CACHE"}), + ServerPrincipal: acctest.RandString(10), + UseKerberosForPasswordAuthentication: randomBool(), + AllowKerberosAuthentication: randomBool(), + KeyTab: acctest.RandString(10), + KerberosRealm: acctest.RandString(10), } resource.Test(t, resource.TestCase{ @@ -550,9 +652,16 @@ resource "keycloak_ldap_user_federation" "openldap" { full_sync_period = %d changed_sync_period = %d + kerberos { + server_principal = "%s" + use_kerberos_for_password_authentication = %t + key_tab = "%s" + kerberos_realm = "%s" + } + cache_policy = "%s" } - `, ldap.RealmId, ldap.Name, ldap.Enabled, ldap.UsernameLDAPAttribute, ldap.RdnLDAPAttribute, ldap.UuidLDAPAttribute, arrayOfStringsForTerraformResource(ldap.UserObjectClasses), ldap.ConnectionUrl, ldap.UsersDn, ldap.BindDn, ldap.BindCredential, ldap.SearchScope, ldap.ValidatePasswordPolicy, ldap.UseTruststoreSpi, ldap.ConnectionTimeout, ldap.ReadTimeout, ldap.Pagination, ldap.BatchSizeForSync, ldap.FullSyncPeriod, ldap.ChangedSyncPeriod, ldap.CachePolicy) + `, ldap.RealmId, ldap.Name, ldap.Enabled, ldap.UsernameLDAPAttribute, ldap.RdnLDAPAttribute, ldap.UuidLDAPAttribute, arrayOfStringsForTerraformResource(ldap.UserObjectClasses), ldap.ConnectionUrl, ldap.UsersDn, ldap.BindDn, ldap.BindCredential, ldap.SearchScope, ldap.ValidatePasswordPolicy, ldap.UseTruststoreSpi, ldap.ConnectionTimeout, ldap.ReadTimeout, ldap.Pagination, ldap.BatchSizeForSync, ldap.FullSyncPeriod, ldap.ChangedSyncPeriod, ldap.ServerPrincipal, ldap.UseKerberosForPasswordAuthentication, ldap.KeyTab, ldap.KerberosRealm, ldap.CachePolicy) } func testKeycloakLdapUserFederation_basicWithAttrValidation(attr, realm, ldap, val string) string {