Repository: mrparkers/terraform-provider-keycloak
Pull request #240 (2020-02-27 14:25:47)

Support for google idp in a separate resource
Support for google idp in a separate resource

tomrutsaert: See  #231
tomrutsaert: See  #231
mrparkers: The code looks good to me!  I'll trust you on this working as expected, since I don't have the means to test it out myself.

Thanks for the PR!

Patch:
diff --git a/example/main.tf b/example/main.tf
index 6207cc1..9ece70f 100644
--- a/example/main.tf
+++ b/example/main.tf
@@ -461,6 +461,15 @@ resource keycloak_oidc_identity_provider oidc {
   client_secret     = "example_token"
 }
 
+resource keycloak_oidc_google_identity_provider google {
+  realm                                   = "${keycloak_realm.test.id}"
+  client_id                               = "myclientid.apps.googleusercontent.com"
+  client_secret                           = "myclientsecret"
+  hosted_domain                           = "mycompany.com"
+  request_refresh_token                   = true
+  accepts_prompt_none_forward_from_client = false
+}
+
 resource keycloak_oidc_identity_provider custom_oidc_idp {
   realm             = "${keycloak_realm.test.id}"
   provider_id       = "customIdp"
diff --git a/keycloak/identity_provider.go b/keycloak/identity_provider.go
index da7935b..9644a7d 100644
--- a/keycloak/identity_provider.go
+++ b/keycloak/identity_provider.go
@@ -39,6 +39,11 @@ type IdentityProviderConfig struct {
 	LoginHint                        string                 `json:"loginHint,omitempty"`
 	UILocales                        KeycloakBoolQuoted     `json:"uiLocales,omitempty"`
 	LogoutUrl                        string                 `json:"logoutUrl,omitempty"`
+	DefaultScope                     string                 `json:"defaultScope,omitempty"`
+	AcceptsPromptNoneForwFrmClt      KeycloakBoolQuoted     `json:"acceptsPromptNoneForwardFromClient,omitempty"`
+	HostedDomain                     string                 `json:"hostedDomain,omitempty"`
+	UserIp                           KeycloakBoolQuoted     `json:"userIp,omitempty"`
+	OfflineAccess                    KeycloakBoolQuoted     `json:"offlineAccess,omitempty"`
 	ExtraConfig                      map[string]interface{} `json:"-"`
 }
 
diff --git a/provider/provider.go b/provider/provider.go
index 522cb2e..58d5819 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -54,6 +54,7 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_attribute_to_role_identity_provider_mapper":      resourceKeycloakAttributeToRoleIdentityProviderMapper(),
 			"keycloak_user_template_importer_identity_provider_mapper": resourceKeycloakUserTemplateImporterIdentityProviderMapper(),
 			"keycloak_saml_identity_provider":                          resourceKeycloakSamlIdentityProvider(),
+			"keycloak_oidc_google_identity_provider":                   resourceKeycloakOidcGoogleIdentityProvider(),
 			"keycloak_oidc_identity_provider":                          resourceKeycloakOidcIdentityProvider(),
 			"keycloak_openid_client_authorization_resource":            resourceKeycloakOpenidClientAuthorizationResource(),
 			"keycloak_openid_client_authorization_scope":               resourceKeycloakOpenidClientAuthorizationScope(),
diff --git a/provider/resource_keycloak_oidc_google_identity_provider.go b/provider/resource_keycloak_oidc_google_identity_provider.go
new file mode 100644
index 0000000..02c76ec
--- /dev/null
+++ b/provider/resource_keycloak_oidc_google_identity_provider.go
@@ -0,0 +1,134 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func resourceKeycloakOidcGoogleIdentityProvider() *schema.Resource {
+	oidcGoogleSchema := map[string]*schema.Schema{
+		"alias": {
+			Type:        schema.TypeString,
+			Computed:    true,
+			Description: "The alias uniquely identifies an identity provider and it is also used to build the redirect uri. In case of google this is computed and always google",
+		},
+		"display_name": {
+			Type:        schema.TypeString,
+			Computed:    true,
+			Description: "Not used by this provider, Will be implicitly Google",
+		},
+		"provider_id": {
+			Type:        schema.TypeString,
+			Optional:    true,
+			Default:     "google",
+			Description: "provider id, is always google, unless you have a extended custom implementation",
+		},
+		"client_id": {
+			Type:        schema.TypeString,
+			Required:    true,
+			Description: "Client ID.",
+		},
+		"client_secret": {
+			Type:        schema.TypeString,
+			Required:    true,
+			Sensitive:   true,
+			Description: "Client Secret.",
+		},
+		"hosted_domain": { //hostedDomain
+			Type:        schema.TypeString,
+			Optional:    true,
+			Description: "Set 'hd' query parameter when logging in with Google. Google will list accounts only for this domain. Keycloak validates that the returned identity token has a claim for this domain. When '*' is entered, any hosted account can be used.",
+		},
+		"use_user_ip_param": { //userIp
+			Type:        schema.TypeBool,
+			Optional:    true,
+			Default:     false,
+			Description: "Set 'userIp' query parameter when invoking on Google's User Info service.  This will use the user's ip address.  Useful if Google is throttling access to the User Info service.",
+		},
+		"request_refresh_token": { //offlineAccess
+			Type:        schema.TypeBool,
+			Optional:    true,
+			Default:     false,
+			Description: "Set 'access_type' query parameter to 'offline' when redirecting to google authorization endpoint, to get a refresh token back. Useful if planning to use Token Exchange to retrieve Google token to access Google APIs when the user is not at the browser.",
+		},
+		"default_scopes": { //defaultScope
+			Type:        schema.TypeString,
+			Optional:    true,
+			Default:     "openid profile email",
+			Description: "The scopes to be sent when asking for authorization. See the documentation for possible values, separator and default value'. Default: 'openid profile email'",
+		},
+		"accepts_prompt_none_forward_from_client": { // acceptsPromptNoneForwardFromClient
+			Type:        schema.TypeBool,
+			Optional:    true,
+			Default:     false,
+			Description: "This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt=none will be forwarded to this identity provider.",
+		},
+		"disable_user_info": { //disableUserInfo
+			Type:        schema.TypeBool,
+			Optional:    true,
+			Default:     false,
+			Description: "Disable usage of User Info service to obtain additional user information?  Default is to use this OIDC service.",
+		},
+		"hide_on_login_page": {
+			Type:        schema.TypeBool,
+			Optional:    true,
+			Default:     false,
+			Description: "Hide On Login Page.",
+		},
+		"extra_config": {
+			Type:     schema.TypeMap,
+			Optional: true,
+		},
+	}
+	oidcResource := resourceKeycloakIdentityProvider()
+	oidcResource.Schema = mergeSchemas(oidcResource.Schema, oidcGoogleSchema)
+	oidcResource.Create = resourceKeycloakIdentityProviderCreate(getOidcGoogleIdentityProviderFromData, setOidcGoogleIdentityProviderData)
+	oidcResource.Read = resourceKeycloakIdentityProviderRead(setOidcGoogleIdentityProviderData)
+	oidcResource.Update = resourceKeycloakIdentityProviderUpdate(getOidcGoogleIdentityProviderFromData, setOidcGoogleIdentityProviderData)
+	return oidcResource
+}
+
+func getOidcGoogleIdentityProviderFromData(data *schema.ResourceData) (*keycloak.IdentityProvider, error) {
+	rec, _ := getIdentityProviderFromData(data)
+	rec.ProviderId = data.Get("provider_id").(string)
+	rec.Alias = "google"
+
+	extraConfig := map[string]interface{}{}
+	if v, ok := data.GetOk("extra_config"); ok {
+		for key, value := range v.(map[string]interface{}) {
+			extraConfig[key] = value
+		}
+	}
+
+	rec.Config = &keycloak.IdentityProviderConfig{
+		ClientId:                    data.Get("client_id").(string),
+		ClientSecret:                data.Get("client_secret").(string),
+		HideOnLoginPage:             keycloak.KeycloakBoolQuoted(data.Get("hide_on_login_page").(bool)),
+		HostedDomain:                data.Get("hosted_domain").(string),
+		UserIp:                      keycloak.KeycloakBoolQuoted(data.Get("use_user_ip_param").(bool)),
+		OfflineAccess:               keycloak.KeycloakBoolQuoted(data.Get("request_refresh_token").(bool)),
+		DefaultScope:                data.Get("default_scopes").(string),
+		AcceptsPromptNoneForwFrmClt: keycloak.KeycloakBoolQuoted(data.Get("accepts_prompt_none_forward_from_client").(bool)),
+		ExtraConfig:                 extraConfig,
+		UseJwksUrl:                  keycloak.KeycloakBoolQuoted(true),
+		DisableUserInfo:             keycloak.KeycloakBoolQuoted(data.Get("disable_user_info").(bool)),
+	}
+
+	return rec, nil
+}
+
+func setOidcGoogleIdentityProviderData(data *schema.ResourceData, identityProvider *keycloak.IdentityProvider) error {
+	setIdentityProviderData(data, identityProvider)
+	data.Set("provider_id", identityProvider.ProviderId)
+	data.Set("client_id", identityProvider.Config.ClientId)
+	data.Set("hide_on_login_page", identityProvider.Config.HideOnLoginPage)
+	data.Set("hosted_domain", identityProvider.Config.HostedDomain)
+	data.Set("use_user_ip_param", identityProvider.Config.UserIp)
+	data.Set("request_refresh_token", identityProvider.Config.OfflineAccess)
+	data.Set("default_scopes", identityProvider.Config.DefaultScope)
+	data.Set("accepts_prompt_none_forward_from_client", identityProvider.Config.AcceptsPromptNoneForwFrmClt)
+	data.Set("extra_config", identityProvider.Config.ExtraConfig)
+	data.Set("use_jwks_url", identityProvider.Config.UseJwksUrl)
+	data.Set("disable_user_info", identityProvider.Config.DisableUserInfo)
+	return nil
+}


Test patch:
diff --git a/provider/resource_keycloak_oidc_google_identity_provider_test.go b/provider/resource_keycloak_oidc_google_identity_provider_test.go
new file mode 100644
index 0000000..0d8fcf4
--- /dev/null
+++ b/provider/resource_keycloak_oidc_google_identity_provider_test.go
@@ -0,0 +1,278 @@
+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 TestAccKeycloakOidcGoogleIdentityProvider_basic(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOidcGoogleIdentityProviderDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOidcGoogleIdentityProvider_basic(realmName),
+				Check:  testAccCheckKeycloakOidcGoogleIdentityProviderExists("keycloak_oidc_google_identity_provider.google"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOidcGoogleIdentityProvider_custom(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	customConfigValue := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOidcGoogleIdentityProviderDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOidcGoogleIdentityProvider_custom(realmName, customConfigValue),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakOidcGoogleIdentityProviderExists("keycloak_oidc_google_identity_provider.google_custom"),
+					testAccCheckKeycloakOidcGoogleIdentityProviderHasCustomConfigValue("keycloak_oidc_google_identity_provider.google_custom", customConfigValue),
+				),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOidcGoogleIdentityProvider_createAfterManualDestroy(t *testing.T) {
+	var idp = &keycloak.IdentityProvider{}
+
+	realmName := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOidcGoogleIdentityProviderDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOidcGoogleIdentityProvider_basic(realmName),
+				Check:  testAccCheckKeycloakOidcGoogleIdentityProviderFetch("keycloak_oidc_google_identity_provider.google", idp),
+			},
+			{
+				PreConfig: func() {
+					keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+					err := keycloakClient.DeleteIdentityProvider(idp.Realm, idp.Alias)
+					if err != nil {
+						t.Fatal(err)
+					}
+				},
+				Config: testKeycloakOidcGoogleIdentityProvider_basic(realmName),
+				Check:  testAccCheckKeycloakOidcGoogleIdentityProviderExists("keycloak_oidc_google_identity_provider.google"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOidcGoogleIdentityProvider_basicUpdateRealm(t *testing.T) {
+	firstRealm := "terraform-" + acctest.RandString(10)
+	secondRealm := "terraform-" + acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOidcGoogleIdentityProviderDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOidcGoogleIdentityProvider_basic(firstRealm),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakOidcGoogleIdentityProviderExists("keycloak_oidc_google_identity_provider.google"),
+					resource.TestCheckResourceAttr("keycloak_oidc_google_identity_provider.google", "realm", firstRealm),
+				),
+			},
+			{
+				Config: testKeycloakOidcGoogleIdentityProvider_basic(secondRealm),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakOidcGoogleIdentityProviderExists("keycloak_oidc_google_identity_provider.google"),
+					resource.TestCheckResourceAttr("keycloak_oidc_google_identity_provider.google", "realm", secondRealm),
+				),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakOidcGoogleIdentityProvider_basicUpdateAll(t *testing.T) {
+	realmName := "terraform-" + acctest.RandString(10)
+	firstEnabled := randomBool()
+
+	firstOidc := &keycloak.IdentityProvider{
+		Realm:   realmName,
+		Alias:   acctest.RandString(10),
+		Enabled: firstEnabled,
+		Config: &keycloak.IdentityProviderConfig{
+			HostedDomain:                "mycompany.com",
+			AcceptsPromptNoneForwFrmClt: false,
+			ClientId:                    acctest.RandString(10),
+			ClientSecret:                acctest.RandString(10),
+		},
+	}
+
+	secondOidc := &keycloak.IdentityProvider{
+		Realm:   realmName,
+		Alias:   acctest.RandString(10),
+		Enabled: !firstEnabled,
+		Config: &keycloak.IdentityProviderConfig{
+			HostedDomain:                "mycompany.com",
+			AcceptsPromptNoneForwFrmClt: false,
+			ClientId:                    acctest.RandString(10),
+			ClientSecret:                acctest.RandString(10),
+		},
+	}
+
+	resource.Test(t, resource.TestCase{
+		Providers:    testAccProviders,
+		PreCheck:     func() { testAccPreCheck(t) },
+		CheckDestroy: testAccCheckKeycloakOidcGoogleIdentityProviderDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testKeycloakOidcGoogleIdentityProvider_basicFromInterface(firstOidc),
+				Check:  testAccCheckKeycloakOidcGoogleIdentityProviderExists("keycloak_oidc_google_identity_provider.google"),
+			},
+			{
+				Config: testKeycloakOidcGoogleIdentityProvider_basicFromInterface(secondOidc),
+				Check:  testAccCheckKeycloakOidcGoogleIdentityProviderExists("keycloak_oidc_google_identity_provider.google"),
+			},
+		},
+	})
+}
+
+func testAccCheckKeycloakOidcGoogleIdentityProviderExists(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		_, err := getKeycloakOidcGoogleIdentityProviderFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakOidcGoogleIdentityProviderFetch(resourceName string, idp *keycloak.IdentityProvider) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		fetchedOidc, err := getKeycloakOidcGoogleIdentityProviderFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		idp.Alias = fetchedOidc.Alias
+		idp.Realm = fetchedOidc.Realm
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakOidcGoogleIdentityProviderHasCustomConfigValue(resourceName, customConfigValue string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		fetchedOidc, err := getKeycloakOidcGoogleIdentityProviderFromState(s, resourceName)
+		if err != nil {
+			return err
+		}
+
+		if fetchedOidc.Config.ExtraConfig["dummyConfig"].(string) != customConfigValue {
+			return fmt.Errorf("expected custom oidc provider to have config with a custom key 'dummyConfig' with a value %s, but value was %s", customConfigValue, fetchedOidc.Config.ExtraConfig["dummyConfig"].(string))
+		}
+
+		return nil
+	}
+}
+
+func testAccCheckKeycloakOidcGoogleIdentityProviderDestroy() resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		for _, rs := range s.RootModule().Resources {
+			if rs.Type != "keycloak_oidc_google_identity_provider" {
+				continue
+			}
+
+			id := rs.Primary.ID
+			realm := rs.Primary.Attributes["realm"]
+
+			keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+			idp, _ := keycloakClient.GetIdentityProvider(realm, id)
+			if idp != nil {
+				return fmt.Errorf("oidc config with id %s still exists", id)
+			}
+		}
+
+		return nil
+	}
+}
+
+func getKeycloakOidcGoogleIdentityProviderFromState(s *terraform.State, resourceName string) (*keycloak.IdentityProvider, error) {
+	keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+	rs, ok := s.RootModule().Resources[resourceName]
+	if !ok {
+		return nil, fmt.Errorf("resource not found: %s", resourceName)
+	}
+
+	realm := rs.Primary.Attributes["realm"]
+	alias := rs.Primary.Attributes["alias"]
+
+	idp, err := keycloakClient.GetIdentityProvider(realm, alias)
+	if err != nil {
+		return nil, fmt.Errorf("error getting oidc identity provider config with alias %s: %s", alias, err)
+	}
+
+	return idp, nil
+}
+
+func testKeycloakOidcGoogleIdentityProvider_basic(realm string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_oidc_google_identity_provider" "google" {
+	realm             = "${keycloak_realm.realm.id}"
+	client_id         = "example_id"
+	client_secret     = "example_token"
+}
+	`, realm)
+}
+
+func testKeycloakOidcGoogleIdentityProvider_custom(realm, customConfigValue string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_oidc_google_identity_provider" "google_custom" {
+	realm             = "${keycloak_realm.realm.id}"
+	provider_id       = "customGoogleIdp"
+	client_id         = "example_id"
+	client_secret     = "example_token"
+	extra_config      = {
+		dummyConfig = "%s"
+	}
+}
+	`, realm, customConfigValue)
+}
+
+func testKeycloakOidcGoogleIdentityProvider_basicFromInterface(idp *keycloak.IdentityProvider) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm = "%s"
+}
+
+resource "keycloak_oidc_google_identity_provider" "google" {
+	realm             						= "${keycloak_realm.realm.id}"
+	enabled           						= %t
+	hosted_domain	  						= "%s"
+	accepts_prompt_none_forward_from_client	= %t
+	client_id         						= "%s"
+	client_secret     						= "%s"
+}
+	`, idp.Realm, idp.Enabled, idp.Config.HostedDomain, idp.Config.AcceptsPromptNoneForwFrmClt, idp.Config.ClientId, idp.Config.ClientSecret)
+}