Repository: mrparkers/terraform-provider-keycloak
Pull request #262 (2020-03-25 11:36:32)

support openid client authentication flow binding overrides
support openid client authentication flow binding overrides


Patch:
diff --git a/docs/resources/keycloak_openid_client.md b/docs/resources/keycloak_openid_client.md
index c23f8a3..d2f88a0 100644
--- a/docs/resources/keycloak_openid_client.md
+++ b/docs/resources/keycloak_openid_client.md
@@ -58,6 +58,10 @@ is set to `true`.
 - `pkce_code_challenge_method` - (Optional) The challenge method to use for Proof Key for Code Exchange. Can be either `plain` or `S256` or set to empty value ``.
 - `full_scope_allowed` - (Optional) - Allow to include all roles mappings in the access token.
 - `access_token_lifespan` - (Optional) - Allow to set the lifespan of the access token
+- `consent_required` - (Optional) - Consent Required: If enabled, users have to consent to client access.
+- `authentication_flow_binding_overrides` - (Optional) - Override realm authentication flow bindings
+    - `browers_id` - (Optional) - Browser flow id, (flow needs to exist)
+    - `direct_grant_id` - (Optional) - Direct grant flow id (flow needs to exist)
 
 ### Attributes Reference
 
diff --git a/docs/resources/keycloak_saml_client.md b/docs/resources/keycloak_saml_client.md
index 3eec0de..63eb7b0 100644
--- a/docs/resources/keycloak_saml_client.md
+++ b/docs/resources/keycloak_saml_client.md
@@ -44,6 +44,7 @@ The following arguments are supported:
 - `force_post_binding` - (Optional) When `true`, Keycloak will always respond to an authentication request via the SAML POST Binding.
 - `front_channel_logout` - (Optional) When `true`, this client will require a browser redirect in order to perform a logout.
 - `name_id_format` - (Optional) Sets the Name ID format for the subject.
+- `force_name_id_format` - (Optional) Ignore requested NameID subject format and use the one defined in `name_id_format` instead.
 - `root_url` - (Optional) When specified, this value is prepended to all relative URLs.
 - `valid_redirect_uris` - (Optional) When specified, Keycloak will use this list to validate given Assertion Consumer URLs specified in the authentication request.
 - `base_url` - (Optional) When specified, this URL will be used whenever Keycloak needs to link to this client.
diff --git a/example/main.tf b/example/main.tf
index 3752679..cc0d9b3 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -725,3 +725,12 @@ resource "keycloak_authentication_execution_config" "config" {
     defaultProvider = "idp-XXX"
   }
 }
+
+resource "keycloak_openid_client" "client" {
+  client_id   = "my-override-flow-binding-client"
+  realm_id    = "${keycloak_realm.test.id}"
+  access_type = "PUBLIC"
+  authentication_flow_binding_overrides {
+    browser_id = "${keycloak_authentication_flow.browser-copy-flow.id}"
+  }
+}
diff --git a/keycloak/ldap_hardcoded_group_mapper.go b/keycloak/ldap_hardcoded_group_mapper.go
new file mode 100644
index 0000000..cda8003
--- /dev/null
+++ b/keycloak/ldap_hardcoded_group_mapper.go
@@ -0,0 +1,75 @@
+package keycloak
+
+import "fmt"
+
+type LdapHardcodedGroupMapper struct {
+	Id                   string
+	Name                 string
+	RealmId              string
+	LdapUserFederationId string
+	Group                string
+}
+
+func convertFromLdapHardcodedGroupMapperToComponent(ldapMapper *LdapHardcodedGroupMapper) *component {
+	return &component{
+		Id:           ldapMapper.Id,
+		Name:         ldapMapper.Name,
+		ProviderId:   "hardcoded-ldap-group-mapper",
+		ProviderType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
+		ParentId:     ldapMapper.LdapUserFederationId,
+
+		Config: map[string][]string{
+			"group": {
+				ldapMapper.Group,
+			},
+		},
+	}
+}
+
+func convertFromComponentToLdapHardcodedGroupMapper(component *component, realmId string) *LdapHardcodedGroupMapper {
+	return &LdapHardcodedGroupMapper{
+		Id:                   component.Id,
+		Name:                 component.Name,
+		RealmId:              realmId,
+		LdapUserFederationId: component.ParentId,
+
+		Group: component.getConfig("group"),
+	}
+}
+
+func (keycloakClient *KeycloakClient) ValidateLdapHardcodedGroupMapper(ldapMapper *LdapHardcodedGroupMapper) error {
+	if len(ldapMapper.Group) == 0 {
+		return fmt.Errorf("validation error: hardcoded group name must not be empty")
+	}
+	return nil
+}
+
+func (keycloakClient *KeycloakClient) NewLdapHardcodedGroupMapper(ldapMapper *LdapHardcodedGroupMapper) error {
+	_, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/components", ldapMapper.RealmId), convertFromLdapHardcodedGroupMapperToComponent(ldapMapper))
+	if err != nil {
+		return err
+	}
+
+	ldapMapper.Id = getIdFromLocationHeader(location)
+
+	return nil
+}
+
+func (keycloakClient *KeycloakClient) GetLdapHardcodedGroupMapper(realmId, id string) (*LdapHardcodedGroupMapper, error) {
+	var component *component
+
+	err := keycloakClient.get(fmt.Sprintf("/realms/%s/components/%s", realmId, id), &component, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return convertFromComponentToLdapHardcodedGroupMapper(component, realmId), nil
+}
+
+func (keycloakClient *KeycloakClient) UpdateLdapHardcodedGroupMapper(ldapMapper *LdapHardcodedGroupMapper) error {
+	return keycloakClient.put(fmt.Sprintf("/realms/%s/components/%s", ldapMapper.RealmId, ldapMapper.Id), convertFromLdapHardcodedGroupMapperToComponent(ldapMapper))
+}
+
+func (keycloakClient *KeycloakClient) DeleteLdapHardcodedGroupMapper(realmId, id string) error {
+	return keycloakClient.delete(fmt.Sprintf("/realms/%s/components/%s", realmId, id), nil)
+}
diff --git a/keycloak/ldap_user_federation.go b/keycloak/ldap_user_federation.go
index 15c775d..15cf6c9 100644
--- a/keycloak/ldap_user_federation.go
+++ b/keycloak/ldap_user_federation.go
@@ -332,7 +332,6 @@ func (keycloakClient *KeycloakClient) GetLdapUserFederationMappers(realmId, id s
 	if err != nil {
 		return nil, err
 	}
-
 	for _, component := range components {
 		switch component.ProviderId {
 		case "full-name-ldap-mapper":
@@ -347,6 +346,18 @@ func (keycloakClient *KeycloakClient) GetLdapUserFederationMappers(realmId, id s
 				return nil, err
 			}
 			ldapUserFederationMappers = append(ldapUserFederationMappers, mapper)
+		case "hardcoded-ldap-group-mapper":
+			mapper := convertFromComponentToLdapHardcodedGroupMapper(component, realmId)
+			ldapUserFederationMappers = append(ldapUserFederationMappers, mapper)
+		case "hardcoded-ldap-role-mapper":
+			mapper := convertFromComponentToLdapHardcodedRoleMapper(component, realmId)
+			ldapUserFederationMappers = append(ldapUserFederationMappers, mapper)
+		case "msad-lds-user-account-control-mapper":
+			mapper, err := convertFromComponentToLdapMsadLdsUserAccountControlMapper(component, realmId)
+			if err != nil {
+				return nil, err
+			}
+			ldapUserFederationMappers = append(ldapUserFederationMappers, mapper)
 		case "msad-user-account-control-mapper":
 			mapper, err := convertFromComponentToLdapMsadUserAccountControlMapper(component, realmId)
 			if err != nil {
diff --git a/keycloak/openid_client.go b/keycloak/openid_client.go
index dc5ad69..fd9e922 100644
--- a/keycloak/openid_client.go
+++ b/keycloak/openid_client.go
@@ -25,30 +25,31 @@ type OpenidClientAuthorizationSettings struct {
 }
 
 type OpenidClient struct {
-	Id                           string                             `json:"id,omitempty"`
-	ClientId                     string                             `json:"clientId"`
-	RealmId                      string                             `json:"-"`
-	Name                         string                             `json:"name"`
-	Protocol                     string                             `json:"protocol"`                // always openid-connect for this resource
-	ClientAuthenticatorType      string                             `json:"clientAuthenticatorType"` // always client-secret for now, don't have a need for JWT here
-	ClientSecret                 string                             `json:"secret,omitempty"`
-	Enabled                      bool                               `json:"enabled"`
-	Description                  string                             `json:"description"`
-	PublicClient                 bool                               `json:"publicClient"`
-	BearerOnly                   bool                               `json:"bearerOnly"`
-	StandardFlowEnabled          bool                               `json:"standardFlowEnabled"`
-	ImplicitFlowEnabled          bool                               `json:"implicitFlowEnabled"`
-	DirectAccessGrantsEnabled    bool                               `json:"directAccessGrantsEnabled"`
-	ServiceAccountsEnabled       bool                               `json:"serviceAccountsEnabled"`
-	AuthorizationServicesEnabled bool                               `json:"authorizationServicesEnabled"`
-	ValidRedirectUris            []string                           `json:"redirectUris"`
-	WebOrigins                   []string                           `json:"webOrigins"`
-	AdminUrl                     string                             `json:"adminUrl"`
-	BaseUrl                      string                             `json:"baseUrl"`
-	FullScopeAllowed             bool                               `json:"fullScopeAllowed"`
-	Attributes                   OpenidClientAttributes             `json:"attributes"`
-	AuthorizationSettings        *OpenidClientAuthorizationSettings `json:"authorizationSettings,omitempty"`
-	ConsentRequired              bool                               `json:"consentRequired"`
+	Id                                 string                                   `json:"id,omitempty"`
+	ClientId                           string                                   `json:"clientId"`
+	RealmId                            string                                   `json:"-"`
+	Name                               string                                   `json:"name"`
+	Protocol                           string                                   `json:"protocol"`                // always openid-connect for this resource
+	ClientAuthenticatorType            string                                   `json:"clientAuthenticatorType"` // always client-secret for now, don't have a need for JWT here
+	ClientSecret                       string                                   `json:"secret,omitempty"`
+	Enabled                            bool                                     `json:"enabled"`
+	Description                        string                                   `json:"description"`
+	PublicClient                       bool                                     `json:"publicClient"`
+	BearerOnly                         bool                                     `json:"bearerOnly"`
+	StandardFlowEnabled                bool                                     `json:"standardFlowEnabled"`
+	ImplicitFlowEnabled                bool                                     `json:"implicitFlowEnabled"`
+	DirectAccessGrantsEnabled          bool                                     `json:"directAccessGrantsEnabled"`
+	ServiceAccountsEnabled             bool                                     `json:"serviceAccountsEnabled"`
+	AuthorizationServicesEnabled       bool                                     `json:"authorizationServicesEnabled"`
+	ValidRedirectUris                  []string                                 `json:"redirectUris"`
+	WebOrigins                         []string                                 `json:"webOrigins"`
+	AdminUrl                           string                                   `json:"adminUrl"`
+	BaseUrl                            string                                   `json:"baseUrl"`
+	FullScopeAllowed                   bool                                     `json:"fullScopeAllowed"`
+	Attributes                         OpenidClientAttributes                   `json:"attributes"`
+	AuthorizationSettings              *OpenidClientAuthorizationSettings       `json:"authorizationSettings,omitempty"`
+	ConsentRequired                    bool                                     `json:"consentRequired"`
+	AuthenticationFlowBindingOverrides OpenidAuthenticationFlowBindingOverrides `json:"authenticationFlowBindingOverrides,omitempty"`
 }
 
 type OpenidClientAttributes struct {
@@ -57,6 +58,11 @@ type OpenidClientAttributes struct {
 	AccessTokenLifespan                 string             `json:"access.token.lifespan"`
 }
 
+type OpenidAuthenticationFlowBindingOverrides struct {
+	BrowserId     string `json:"browser"`
+	DirectGrantId string `json:"direct_grant"`
+}
+
 func (keycloakClient *KeycloakClient) GetOpenidClientServiceAccountUserId(realmId, clientId string) (*User, error) {
 	var serviceAccountUser User
 
diff --git a/keycloak/saml_client.go b/keycloak/saml_client.go
index b713dfe..4445a38 100644
--- a/keycloak/saml_client.go
+++ b/keycloak/saml_client.go
@@ -10,6 +10,7 @@ type SamlClientAttributes struct {
 	SignAssertions          *string `json:"saml.assertion.signature"`
 	ClientSignatureRequired *string `json:"saml.client.signature"`
 	ForcePostBinding        *string `json:"saml.force.post.binding"`
+	ForceNameIdFormat       *string `json:"saml_force_name_id_format"`
 	// attributes above are actually booleans, but the Keycloak API expects strings
 	NameIdFormat                    string  `json:"saml_name_id_format"`
 	SigningCertificate              *string `json:"saml.signing.certificate,omitempty"`
diff --git a/provider/data_source_keycloak_openid_client.go b/provider/data_source_keycloak_openid_client.go
index 13a0a3b..eb5caea 100644
--- a/provider/data_source_keycloak_openid_client.go
+++ b/provider/data_source_keycloak_openid_client.go
@@ -96,6 +96,27 @@ func dataSourceKeycloakOpenidClient() *schema.Resource {
 				Type:     schema.TypeBool,
 				Computed: true,
 			},
+			"consent_required": {
+				Type:     schema.TypeBool,
+				Computed: true,
+			},
+			"authentication_flow_binding_overrides": {
+				Type:     schema.TypeSet,
+				Computed: true,
+				MaxItems: 1,
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"browser_id": {
+							Type:     schema.TypeString,
+							Computed: true,
+						},
+						"direct_grant_id": {
+							Type:     schema.TypeString,
+							Computed: true,
+						},
+					},
+				},
+			},
 		},
 	}
 }
diff --git a/provider/provider.go b/provider/provider.go
index 1415344..319cbac 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -31,6 +31,7 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_ldap_user_attribute_mapper":                      resourceKeycloakLdapUserAttributeMapper(),
 			"keycloak_ldap_group_mapper":                               resourceKeycloakLdapGroupMapper(),
 			"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(),
diff --git a/provider/resource_keycloak_ldap_hardcoded_group_mapper.go b/provider/resource_keycloak_ldap_hardcoded_group_mapper.go
new file mode 100644
index 0000000..62e7ea2
--- /dev/null
+++ b/provider/resource_keycloak_ldap_hardcoded_group_mapper.go
@@ -0,0 +1,127 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func resourceKeycloakLdapHardcodedGroupMapper() *schema.Resource {
+	return &schema.Resource{
+		Create: resourceKeycloakLdapHardcodedGroupMapperCreate,
+		Read:   resourceKeycloakLdapHardcodedGroupMapperRead,
+		Update: resourceKeycloakLdapHardcodedGroupMapperUpdate,
+		Delete: resourceKeycloakLdapHardcodedGroupMapperDelete,
+		// 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.",
+			},
+			"group": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "Group to grant to user.",
+			},
+		},
+	}
+}
+
+func getLdapHardcodedGroupMapperFromData(data *schema.ResourceData) *keycloak.LdapHardcodedGroupMapper {
+	return &keycloak.LdapHardcodedGroupMapper{
+		Id:                   data.Id(),
+		Name:                 data.Get("name").(string),
+		RealmId:              data.Get("realm_id").(string),
+		LdapUserFederationId: data.Get("ldap_user_federation_id").(string),
+		Group:                data.Get("group").(string),
+	}
+}
+
+func setLdapHardcodedGroupMapperData(data *schema.ResourceData, ldapMapper *keycloak.LdapHardcodedGroupMapper) {
+	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("group", ldapMapper.Group)
+}
+
+func resourceKeycloakLdapHardcodedGroupMapperCreate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	ldapMapper := getLdapHardcodedGroupMapperFromData(data)
+
+	err := keycloakClient.ValidateLdapHardcodedGroupMapper(ldapMapper)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.NewLdapHardcodedGroupMapper(ldapMapper)
+	if err != nil {
+		return err
+	}
+
+	setLdapHardcodedGroupMapperData(data, ldapMapper)
+
+	return resourceKeycloakLdapHardcodedGroupMapperRead(data, meta)
+}
+
+func resourceKeycloakLdapHardcodedGroupMapperRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmId := data.Get("realm_id").(string)
+	id := data.Id()
+
+	ldapMapper, err := keycloakClient.GetLdapHardcodedGroupMapper(realmId, id)
+	if err != nil {
+		return handleNotFoundError(err, data)
+	}
+
+	setLdapHardcodedGroupMapperData(data, ldapMapper)
+
+	return nil
+}
+
+func resourceKeycloakLdapHardcodedGroupMapperUpdate(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	ldapMapper := getLdapHardcodedGroupMapperFromData(data)
+
+	err := keycloakClient.ValidateLdapHardcodedGroupMapper(ldapMapper)
+	if err != nil {
+		return err
+	}
+
+	err = keycloakClient.UpdateLdapHardcodedGroupMapper(ldapMapper)
+	if err != nil {
+		return err
+	}
+
+	setLdapHardcodedGroupMapperData(data, ldapMapper)
+
+	return nil
+}
+
+func resourceKeycloakLdapHardcodedGroupMapperDelete(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmId := data.Get("realm_id").(string)
+	id := data.Id()
+
+	return keycloakClient.DeleteLdapHardcodedGroupMapper(realmId, id)
+}
diff --git a/provider/resource_keycloak_openid_client.go b/provider/resource_keycloak_openid_client.go
index 5608eb8..ac3b3b3 100644
--- a/provider/resource_keycloak_openid_client.go
+++ b/provider/resource_keycloak_openid_client.go
@@ -155,6 +155,23 @@ func resourceKeycloakOpenidClient() *schema.Resource {
 				Optional: true,
 				Default:  false,
 			},
+			"authentication_flow_binding_overrides": {
+				Type:     schema.TypeSet,
+				Optional: true,
+				MaxItems: 1,
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"browser_id": {
+							Type:     schema.TypeString,
+							Optional: true,
+						},
+						"direct_grant_id": {
+							Type:     schema.TypeString,
+							Optional: true,
+						},
+					},
+				},
+			},
 		},
 	}
 }
@@ -231,6 +248,16 @@ func getOpenidClientFromData(data *schema.ResourceData) (*keycloak.OpenidClient,
 	} else {
 		openidClient.AuthorizationServicesEnabled = false
 	}
+
+	if v, ok := data.GetOk("authentication_flow_binding_overrides"); ok {
+		authenticationFlowBindingOverridesData := v.(*schema.Set).List()[0]
+		authenticationFlowBindingOverrides := authenticationFlowBindingOverridesData.(map[string]interface{})
+		openidClient.AuthenticationFlowBindingOverrides = keycloak.OpenidAuthenticationFlowBindingOverrides{
+			BrowserId:     authenticationFlowBindingOverrides["browser_id"].(string),
+			DirectGrantId: authenticationFlowBindingOverrides["direct_grant_id"].(string),
+		}
+	}
+
 	return openidClient, nil
 }
 
@@ -281,6 +308,16 @@ func setOpenidClientData(keycloakClient *keycloak.KeycloakClient, data *schema.R
 	} else {
 		data.Set("access_type", "CONFIDENTIAL")
 	}
+
+	if (keycloak.OpenidAuthenticationFlowBindingOverrides{}) == client.AuthenticationFlowBindingOverrides {
+		data.Set("authentication_flow_binding_overrides", nil)
+	} else {
+		authenticationFlowBindingOverridesSettings := make(map[string]interface{})
+		authenticationFlowBindingOverridesSettings["browser_id"] = client.AuthenticationFlowBindingOverrides.BrowserId
+		authenticationFlowBindingOverridesSettings["direct_grant_id"] = client.AuthenticationFlowBindingOverrides.DirectGrantId
+		data.Set("authentication_flow_binding_overrides", []interface{}{authenticationFlowBindingOverridesSettings})
+	}
+
 	return nil
 }
 
diff --git a/provider/resource_keycloak_saml_client.go b/provider/resource_keycloak_saml_client.go
index 5106172..6d49ac6 100644
--- a/provider/resource_keycloak_saml_client.go
+++ b/provider/resource_keycloak_saml_client.go
@@ -77,6 +77,11 @@ func resourceKeycloakSamlClient() *schema.Resource {
 				Optional: true,
 				Computed: true,
 			},
+			"force_name_id_format": {
+				Type:     schema.TypeBool,
+				Optional: true,
+				Computed: true,
+			},
 			"name_id_format": {
 				Type:         schema.TypeString,
 				Optional:     true,
@@ -202,6 +207,11 @@ func mapToSamlClientFromData(data *schema.ResourceData) *keycloak.SamlClient {
 		samlAttributes.IncludeAuthnStatement = &includeAuthnStatementString
 	}
 
+	if forceNameIdFormat, ok := data.GetOkExists("force_name_id_format"); ok {
+		forceNameIdFormatString := strconv.FormatBool(forceNameIdFormat.(bool))
+		samlAttributes.ForceNameIdFormat = &forceNameIdFormatString
+	}
+
 	if signDocuments, ok := data.GetOkExists("sign_documents"); ok {
 		signDocumentsString := strconv.FormatBool(signDocuments.(bool))
 		samlAttributes.SignDocuments = &signDocumentsString
@@ -252,6 +262,14 @@ func mapToDataFromSamlClient(data *schema.ResourceData, client *keycloak.SamlCli
 
 		data.Set("include_authn_statement", includeAuthnStatement)
 	}
+	if client.Attributes.ForceNameIdFormat != nil {
+		forceNameIdFormat, err := strconv.ParseBool(*client.Attributes.ForceNameIdFormat)
+		if err != nil {
+			return err
+		}
+
+		data.Set("force_name_id_format", forceNameIdFormat)
+	}
 
 	if client.Attributes.SignDocuments != nil {
 		signDocuments, err := strconv.ParseBool(*client.Attributes.SignDocuments)


Test patch:
diff --git a/provider/resource_keycloak_ldap_hardcoded_group_mapper_test.go b/provider/resource_keycloak_ldap_hardcoded_group_mapper_test.go
new file mode 100644
index 0000000..0e5eaee
--- /dev/null
+++ b/provider/resource_keycloak_ldap_hardcoded_group_mapper_test.go
@@ -0,0 +1,170 @@
+package provider
+
+import (
+	"fmt"
+	"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"
+)
+
+func TestAccKeycloakLdapHardcodedGroupMapper_basic(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	groupMapperName := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakLdapHardcodedGroupMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakLdapHardcodedGroupMapper(realmName, groupMapperName),
+				Check:  testAccCheckKeycloakLdapHardcodedGroupMapperExists("keycloak_ldap_hardcoded_group_mapper.hardcoded_group_mapper"),
+			},
+			{
+				ResourceName:      "keycloak_ldap_hardcoded_group_mapper.hardcoded_group_mapper",
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: getLdapGenericMapperImportId("keycloak_ldap_hardcoded_group_mapper.hardcoded_group_mapper"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakLdapHardcodedGroupMapper_createAfterManualDestroy(t *testing.T) {
+	var mapper = &keycloak.LdapHardcodedGroupMapper{}
+
+	realmName := "terraform-" + acctest.RandString(10)
+	groupMapperName := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakLdapHardcodedGroupMapperDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakLdapHardcodedGroupMapper(realmName, groupMapperName),
+				Check:  testAccCheckKeycloakLdapHardcodedGroupMapperFetch("keycloak_ldap_hardcoded_group_mapper.hardcoded_group_mapper", mapper),
+			},
+			{
+				PreConfig: func() {
+					keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+					err := keycloakClient.DeleteLdapHardcodedGroupMapper(mapper.RealmId, mapper.Id)
+					if err != nil {
+						t.Fatal(err)
+					}
+				},
+				Config: testKeycloakLdapHardcodedGroupMapper(realmName, groupMapperName),
+				Check:  testAccCheckKeycloakLdapHardcodedGroupMapperExists("keycloak_ldap_hardcoded_group_mapper.hardcoded_group_mapper"),
+			},
+		},
+	})
+}
+
+func testAccCheckKeycloakLdapHardcodedGroupMapperExists(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		_, err := getLdapHardcodedGroupMapperFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakLdapHardcodedGroupMapperFetch(resourceName string, mapper *keycloak.LdapHardcodedGroupMapper) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		fetchedMapper, err := getLdapHardcodedGroupMapperFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		mapper.Id = fetchedMapper.Id
+		mapper.RealmId = fetchedMapper.RealmId
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakLdapHardcodedGroupMapperDestroy() resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		for _, rs := range s.RootModule().Resources {
+			if rs.Type != "keycloak_ldap_hardcoded_group_mapper" {
+				continue
+			}
+
+			id := rs.Primary.ID
+			realm := rs.Primary.Attributes["realm_id"]
+
+			keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+			ldapMapper, _ := keycloakClient.GetLdapHardcodedGroupMapper(realm, id)
+			if ldapMapper != nil {
+				return fmt.Errorf("ldap hardcoded group mapper with id %s still exists", id)
+			}
+		}
+
+		return nil
+	}
+}
+
+func getLdapHardcodedGroupMapperFromState(s *terraform.State, resourceName string) (*keycloak.LdapHardcodedGroupMapper, 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"]
+
+	ldapMapper, err := keycloakClient.GetLdapHardcodedGroupMapper(realm, id)
+	if err != nil {
+		return nil, fmt.Errorf("error getting ldap group mapper with id %s: %s", id, err)
+	}
+
+	return ldapMapper, nil
+}
+
+func testKeycloakLdapHardcodedGroupMapper(realm, groupMapperName 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",
+		"organizationalGroup"
+	]
+	connection_url          = "ldap://openldap"
+	users_dn                = "dc=example,dc=org"
+	bind_dn                 = "cn=admin,dc=example,dc=org"
+	bind_credential         = "admin"
+}
+
+resource "keycloak_group" "hardcoded_group_mapper_test" {
+    realm_id    = keycloak_realm.realm.id
+    name        = "hardcoded-group-test"
+}
+
+resource "keycloak_ldap_hardcoded_group_mapper" "hardcoded_group_mapper" {
+	name                        = "%s"
+	realm_id                    = keycloak_realm.realm.id
+	ldap_user_federation_id     = keycloak_ldap_user_federation.openldap.id
+	group                        = keycloak_group.hardcoded_group_mapper_test.name
+}
+	`, realm, groupMapperName)
+}
diff --git a/provider/resource_keycloak_openid_client_test.go b/provider/resource_keycloak_openid_client_test.go
index b2cc026..f9c3f47 100644
--- a/provider/resource_keycloak_openid_client_test.go
+++ b/provider/resource_keycloak_openid_client_test.go
@@ -258,21 +258,6 @@ func TestAccKeycloakOpenidClient_AccessToken_basic(t *testing.T) {
 	})
 }
 
-func testAccCheckKeycloakOpenidClientAdminUrl(resourceName string, adminUrl string) resource.TestCheckFunc {
-	return func(s *terraform.State) error {
-		client, err := getOpenidClientFromState(s, resourceName)
-		if err != nil {
-			return err
-		}
-
-		if client.AdminUrl != adminUrl {
-			return fmt.Errorf("expected openid client to have adminUrl set to %s, but got %s", adminUrl, client.AdminUrl)
-		}
-
-		return nil
-	}
-}
-
 func TestAccKeycloakOpenidClient_secret(t *testing.T) {
 	realmName := "terraform-" + acctest.RandString(10)
 	clientId := "terraform-" + acctest.RandString(10)
@@ -455,6 +440,27 @@ func TestAccKeycloakOpenidClient_excludeSessionStateFromAuthResponse(t *testing.
 	})
 }
 
+func TestAccKeycloakOpenidClient_authenticationFlowBindingOverrides(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	clientId := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOpenidClientDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOpenidClient_authenticationFlowBindingOverrides(realmName, clientId),
+				Check:  testAccCheckKeycloakOpenidClientAuthenticationFlowBindingOverrides("keycloak_openid_client.client", "keycloak_authentication_flow.another_flow"),
+			},
+			{
+				Config: testKeycloakOpenidClient_withoutAuthenticationFlowBindingOverrides(realmName, clientId),
+				Check:  testAccCheckKeycloakOpenidClientAuthenticationFlowBindingOverrides("keycloak_openid_client.client", ""),
+			},
+		},
+	})
+}
+
 func testAccCheckKeycloakOpenidClientExistsWithCorrectProtocol(resourceName string) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		client, err := getOpenidClientFromState(s, resourceName)
@@ -630,6 +636,56 @@ func testAccCheckKeycloakOpenidClientHasExcludeSessionStateFromAuthResponse(reso
 	}
 }
 
+func testAccCheckKeycloakOpenidClientAdminUrl(resourceName string, adminUrl string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		client, err := getOpenidClientFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		if client.AdminUrl != adminUrl {
+			return fmt.Errorf("expected openid client to have adminUrl set to %s, but got %s", adminUrl, client.AdminUrl)
+		}
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakOpenidClientAuthenticationFlowBindingOverrides(resourceName, flowResourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		client, err := getOpenidClientFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		if flowResourceName == "" {
+			if client.AuthenticationFlowBindingOverrides.BrowserId != "" {
+				return fmt.Errorf("expected openid client to have browserId set to empty, but got %s", client.AuthenticationFlowBindingOverrides.BrowserId)
+			}
+
+			if client.AuthenticationFlowBindingOverrides.DirectGrantId != "" {
+				return fmt.Errorf("expected openid client to have directGrantId set to empty, but got %s", client.AuthenticationFlowBindingOverrides.DirectGrantId)
+			}
+
+		} else {
+			flow, err := getAuthenticationFlowFromState(s, flowResourceName)
+			if err != nil {
+				return err
+			}
+
+			if client.AuthenticationFlowBindingOverrides.BrowserId != flow.Id {
+				return fmt.Errorf("expected openid client to have browserId set to %s, but got %s", flow.Id, client.AuthenticationFlowBindingOverrides.BrowserId)
+			}
+
+			if client.AuthenticationFlowBindingOverrides.DirectGrantId != flow.Id {
+				return fmt.Errorf("expected openid client to have directGrantId set to %s, but got %s", flow.Id, client.AuthenticationFlowBindingOverrides.DirectGrantId)
+			}
+		}
+
+		return nil
+	}
+}
+
 func getOpenidClientFromState(s *terraform.State, resourceName string) (*keycloak.OpenidClient, error) {
 	keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
 
@@ -915,3 +971,47 @@ resource "keycloak_openid_client" "client" {
 }
 	`, realm, clientId, standardFlowEnabled, implicitFlowEnabled, directAccessGrantsEnabled, serviceAccountsEnabled)
 }
+
+func testKeycloakOpenidClient_authenticationFlowBindingOverrides(realm, clientId string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_authentication_flow" "another_flow" {
+  alias    = "anotherFlow"
+  realm_id = "${keycloak_realm.realm.id}"
+  description = "this is another flow"
+}
+
+resource "keycloak_openid_client" "client" {
+	client_id   = "%s"
+	realm_id    = "${keycloak_realm.realm.id}"
+	access_type = "PUBLIC"
+	authentication_flow_binding_overrides {
+		browser_id = "${keycloak_authentication_flow.another_flow.id}"
+		direct_grant_id = "${keycloak_authentication_flow.another_flow.id}"
+	}
+}
+	`, realm, clientId)
+}
+
+func testKeycloakOpenidClient_withoutAuthenticationFlowBindingOverrides(realm, clientId string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_authentication_flow" "another_flow" {
+  alias    = "anotherFlow"
+  realm_id = "${keycloak_realm.realm.id}"
+  description = "this is another flow"
+}
+
+resource "keycloak_openid_client" "client" {
+	client_id   = "%s"
+	realm_id    = "${keycloak_realm.realm.id}"
+	access_type = "PUBLIC"
+}
+	`, realm, clientId)
+}
diff --git a/provider/resource_keycloak_saml_client_test.go b/provider/resource_keycloak_saml_client_test.go
index fe548f8..f86d850 100644
--- a/provider/resource_keycloak_saml_client_test.go
+++ b/provider/resource_keycloak_saml_client_test.go
@@ -158,6 +158,7 @@ func TestAccKeycloakSamlClient_updateInPlace(t *testing.T) {
 			SignAssertions:                  randomBoolAsStringPointer(),
 			ClientSignatureRequired:         &clientSignatureRequired,
 			ForcePostBinding:                randomBoolAsStringPointer(),
+			ForceNameIdFormat:               randomBoolAsStringPointer(),
 			NameIdFormat:                    randomStringInSlice(keycloakSamlClientNameIdFormats),
 			SigningCertificate:              &signingCertificateBefore,
 			SigningPrivateKey:               &signingPrivateKeyBefore,
@@ -193,6 +194,7 @@ func TestAccKeycloakSamlClient_updateInPlace(t *testing.T) {
 			SignAssertions:                  randomBoolAsStringPointer(),
 			ClientSignatureRequired:         &clientSignatureRequired,
 			ForcePostBinding:                randomBoolAsStringPointer(),
+			ForceNameIdFormat:               randomBoolAsStringPointer(),
 			NameIdFormat:                    randomStringInSlice(keycloakSamlClientNameIdFormats),
 			SigningCertificate:              &signingCertificateAfter,
 			SigningPrivateKey:               &signingPrivateKeyAfter,
@@ -399,7 +401,12 @@ func testAccCheckKeycloakSamlClientHasDefaultBooleanAttributes(resourceName stri
 			return err
 		}
 
-		if !includeAuthnStatement && !signDocuments && !signAssertions && !clientSignatureRequired && !forcePostBinding {
+		forceNameIdFormat, err := parseBoolAndTreatEmptyStringAsFalse(rs.Primary.Attributes["force_name_id_format"])
+		if err != nil {
+			return err
+		}
+
+		if !includeAuthnStatement && !signDocuments && !signAssertions && !clientSignatureRequired && !forcePostBinding && !forceNameIdFormat {
 			return fmt.Errorf("expected saml client with id %s to have some defaults set by Keycloak", rs.Primary.ID)
 		}
 
@@ -487,6 +494,7 @@ resource "keycloak_saml_client" "saml_client" {
 	sign_assertions            = %s
 	client_signature_required  = %s
 	force_post_binding         = %s
+	force_name_id_format       = %s
 
 	front_channel_logout       = %t
 	name_id_format             = "%s"
@@ -516,6 +524,7 @@ resource "keycloak_saml_client" "saml_client" {
 		*client.Attributes.SignAssertions,
 		*client.Attributes.ClientSignatureRequired,
 		*client.Attributes.ForcePostBinding,
+		*client.Attributes.ForceNameIdFormat,
 		client.FrontChannelLogout,
 		client.Attributes.NameIdFormat,
 		client.RootUrl,