Repository: mrparkers/terraform-provider-keycloak
Pull request #360 (2020-08-07 14:02:27)

Data source for user and authentication execution
Hi everyone,

I didn't see any contribution guidelines, so I will just go ahead and open this PR. This Pull Request will add data source for User and Authentication Execution. Currently we need that for our process.

If there's anything lacking please do let me know.

Cheers,
RJ

rjmasikome: Hey thanks for the change. LGTM.

Patch:
diff --git a/docs/data-sources/authentication_execution.md b/docs/data-sources/authentication_execution.md
new file mode 100644
index 0000000..182a2bb
--- /dev/null
+++ b/docs/data-sources/authentication_execution.md
@@ -0,0 +1,33 @@
+---
+page_title: "keycloak_authentication_execution Data Source"
+---
+
+# keycloak\_authentication\_execution Data Source
+
+This data source can be used to fetch the ID of an authentication execution within Keycloak.
+
+## Example Usage
+
+```hcl
+resource "keycloak_realm" "realm" {
+    realm   = "my-realm"
+    enabled = true
+}
+
+data "keycloak_authentication_execution" "browser_auth_cookie" {
+  realm_id          = keycloak_realm.realm.id
+  parent_flow_alias = "browser"
+  provider_id       = "auth-cookie"
+}
+```
+
+## Argument Reference
+
+- `realm_id` - (Required) The realm the authentication execution exists in.
+- `parent_flow_alias` - (Required) The alias of the flow this execution is attached to.
+- `provider_id` - (Required) The name of the provider. This can be found by experimenting with the GUI and looking at HTTP requests within the network tab of your browser's development tools. This was previously known as the "authenticator".
+
+## Attributes Reference
+
+- `id` - (Computed) The unique ID of the authentication execution, which can be used as an argument to other resources supported by this provider.
+
diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md
new file mode 100644
index 0000000..9b59fd8
--- /dev/null
+++ b/docs/data-sources/user.md
@@ -0,0 +1,44 @@
+---
+page_title: "keycloak_user Data Source"
+---
+
+# keycloak\_user Data Source
+
+This data source can be used to fetch properties of a user within Keycloak.
+
+## Example Usage
+
+```hcl
+data "keycloak_realm" "master_realm" {
+  realm = "master"
+}
+
+// use the keycloak_user data source to grab the admin user's ID
+data "keycloak_user" "default_admin_user" {
+  realm_id = data.keycloak_realm.master_realm.id
+  name     = "keycloak"
+}
+
+output "keycloak_user_id" {
+  value = data.keycloak_user.default_admin_user.id
+}
+```
+
+## Argument Reference
+
+- `realm_id` - (Required) The realm this user belongs to.
+- `username` - (Required) The unique username of this user.
+
+## Attributes Reference
+
+- `id` - (Computed) The unique ID of the user, which can be used as an argument to other resources supported by this provider.
+- `enabled` - (Computed) When false, this user cannot log in. Defaults to `true`.
+- `email` - (Computed) The user's email.
+- `email_verified` - (Computed) Whether the email address was validated or not. Default to `false`.
+- `first_name` - (Computed) The user's first name.
+- `last_name` - (Computed) The user's last name.
+- `attributes` - (Computed) A map representing attributes for the user
+- `federated_identity` - (Computed) The user's federated identities, if applicable. This block has the following schema:
+  - `identity_provider` - (Computed) The name of the identity provider
+  - `user_id` - (Computed) The ID of the user defined in the identity provider
+  - `user_name` - (Computed) The user name of the user defined in the identity provider
diff --git a/keycloak/authentication_execution.go b/keycloak/authentication_execution.go
index b8b2d6b..840d955 100644
--- a/keycloak/authentication_execution.go
+++ b/keycloak/authentication_execution.go
@@ -2,6 +2,7 @@ package keycloak
 
 import (
 	"fmt"
+	"time"
 )
 
 // this is only used when creating an execution on a flow.
@@ -73,6 +74,49 @@ func (keycloakClient *KeycloakClient) ListAuthenticationExecutions(realmId, pare
 	return authenticationExecutions, err
 }
 
+func (keycloakClient *KeycloakClient) GetAuthenticationExecutionInfoFromProviderId(realmId, parentFlowAlias, providerId string) (*AuthenticationExecutionInfo, error) {
+	var authenticationExecutions []*AuthenticationExecutionInfo
+	var authenticationExecution AuthenticationExecutionInfo
+
+	err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", realmId, parentFlowAlias), &authenticationExecutions, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	// Retry 3 more times if not found, sometimes it took split milliseconds the Authentication Executions to populate
+	if len(authenticationExecutions) == 0 {
+		for i := 0; i < 3; i++ {
+			err := keycloakClient.get(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions", realmId, parentFlowAlias), &authenticationExecutions, nil)
+
+			if len(authenticationExecutions) > 0 {
+				break
+			}
+
+			if err != nil {
+				return nil, err
+			}
+
+			time.Sleep(time.Millisecond * 50)
+		}
+
+		if len(authenticationExecutions) == 0 {
+			return nil, fmt.Errorf("no authentication executions found for parent flow alias %s", parentFlowAlias)
+		}
+	}
+
+	for _, aExecution := range authenticationExecutions {
+		if aExecution != nil && aExecution.ProviderId == providerId {
+			authenticationExecution = *aExecution
+			authenticationExecution.RealmId = realmId
+			authenticationExecution.ParentFlowAlias = parentFlowAlias
+
+			return &authenticationExecution, nil
+		}
+	}
+
+	return nil, fmt.Errorf("no authentication execution under parent flow alias %s with provider id %s found", parentFlowAlias, providerId)
+}
+
 func (keycloakClient *KeycloakClient) NewAuthenticationExecution(execution *AuthenticationExecution) error {
 	_, location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/authentication/flows/%s/executions/execution", execution.RealmId, execution.ParentFlowAlias), &authenticationExecutionCreate{Provider: execution.Authenticator})
 	if err != nil {
diff --git a/provider/data_source_keycloak_authentication_execution.go b/provider/data_source_keycloak_authentication_execution.go
new file mode 100644
index 0000000..a4cc1c3
--- /dev/null
+++ b/provider/data_source_keycloak_authentication_execution.go
@@ -0,0 +1,43 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func dataSourceKeycloakAuthenticationExecution() *schema.Resource {
+	return &schema.Resource{
+		Read: dataSourceKeycloakAuthenticationExecutionRead,
+		Schema: map[string]*schema.Schema{
+			"realm_id": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"parent_flow_alias": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"provider_id": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+		},
+	}
+}
+
+func dataSourceKeycloakAuthenticationExecutionRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmID := data.Get("realm_id").(string)
+	parentFlowAlias := data.Get("parent_flow_alias").(string)
+	providerID := data.Get("provider_id").(string)
+
+	authenticationExecutionInfo, err := keycloakClient.GetAuthenticationExecutionInfoFromProviderId(realmID, parentFlowAlias, providerID)
+	if err != nil {
+		return err
+	}
+
+	mapFromAuthenticationExecutionInfoToData(data, authenticationExecutionInfo)
+
+	return nil
+}
diff --git a/provider/data_source_keycloak_user.go b/provider/data_source_keycloak_user.go
new file mode 100644
index 0000000..902f285
--- /dev/null
+++ b/provider/data_source_keycloak_user.go
@@ -0,0 +1,67 @@
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func dataSourceKeycloakUser() *schema.Resource {
+	return &schema.Resource{
+		Read: dataSourceKeycloakUserRead,
+		Schema: map[string]*schema.Schema{
+			"realm_id": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"username": {
+				Type:     schema.TypeString,
+				Required: true,
+			},
+			"email": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"email_verified": {
+				Type:     schema.TypeBool,
+				Computed: true,
+			},
+			"first_name": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"last_name": {
+				Type:     schema.TypeString,
+				Computed: true,
+			},
+			"attributes": {
+				Type:     schema.TypeMap,
+				Computed: true,
+			},
+			"federated_identity": {
+				Type:     schema.TypeSet,
+				Elem:     &schema.Schema{Type: schema.TypeString},
+				Computed: true,
+			},
+			"enabled": {
+				Type:     schema.TypeBool,
+				Computed: true,
+			},
+		},
+	}
+}
+
+func dataSourceKeycloakUserRead(data *schema.ResourceData, meta interface{}) error {
+	keycloakClient := meta.(*keycloak.KeycloakClient)
+
+	realmID := data.Get("realm_id").(string)
+	username := data.Get("username").(string)
+
+	user, err := keycloakClient.GetUserByUsername(realmID, username)
+	if err != nil {
+		return err
+	}
+
+	mapFromUserToData(data, user)
+
+	return nil
+}
diff --git a/provider/provider.go b/provider/provider.go
index 016e5ef..d8ca5a7 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"context"
 	"fmt"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
@@ -19,7 +20,9 @@ func KeycloakProvider() *schema.Provider {
 			"keycloak_realm":                              dataSourceKeycloakRealm(),
 			"keycloak_realm_keys":                         dataSourceKeycloakRealmKeys(),
 			"keycloak_role":                               dataSourceKeycloakRole(),
+			"keycloak_user":                               dataSourceKeycloakUser(),
 			"keycloak_saml_client_installation_provider":  dataSourceKeycloakSamlClientInstallationProvider(),
+			"keycloak_authentication_execution":           dataSourceKeycloakAuthenticationExecution(),
 		},
 		ResourcesMap: map[string]*schema.Resource{
 			"keycloak_realm":                                             resourceKeycloakRealm(),
diff --git a/provider/resource_keycloak_authentication_execution.go b/provider/resource_keycloak_authentication_execution.go
index 65ab492..e1f4857 100644
--- a/provider/resource_keycloak_authentication_execution.go
+++ b/provider/resource_keycloak_authentication_execution.go
@@ -2,10 +2,11 @@ package provider
 
 import (
 	"fmt"
+	"strings"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakAuthenticationExecution() *schema.Resource {
@@ -64,6 +65,13 @@ func mapFromAuthenticationExecutionToData(data *schema.ResourceData, authenticat
 	data.Set("requirement", authenticationExecution.Requirement)
 }
 
+func mapFromAuthenticationExecutionInfoToData(data *schema.ResourceData, authenticationExecutionInfo *keycloak.AuthenticationExecutionInfo) {
+	data.SetId(authenticationExecutionInfo.Id)
+
+	data.Set("realm_id", authenticationExecutionInfo.RealmId)
+	data.Set("parent_flow_alias", authenticationExecutionInfo.ParentFlowAlias)
+}
+
 func resourceKeycloakAuthenticationExecutionCreate(data *schema.ResourceData, meta interface{}) error {
 	keycloakClient := meta.(*keycloak.KeycloakClient)
 
diff --git a/provider/resource_keycloak_group.go b/provider/resource_keycloak_group.go
index 038d5bf..21d7ecf 100644
--- a/provider/resource_keycloak_group.go
+++ b/provider/resource_keycloak_group.go
@@ -2,9 +2,10 @@ package provider
 
 import (
 	"fmt"
+	"strings"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"strings"
 )
 
 func resourceKeycloakGroup() *schema.Resource {


Test patch:
diff --git a/provider/data_source_keycloak_authentication_execution_test.go b/provider/data_source_keycloak_authentication_execution_test.go
new file mode 100644
index 0000000..4059814
--- /dev/null
+++ b/provider/data_source_keycloak_authentication_execution_test.go
@@ -0,0 +1,171 @@
+package provider
+
+import (
+	"fmt"
+	"regexp"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func TestAccKeycloakDataSourceAuthenticationExecution_basic(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	parentFlowAlias := acctest.RandString(20)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testDataSourceKeycloakAuthenticationExecution_basic(realm, parentFlowAlias),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakAuthenticationExecutionExists("keycloak_authentication_execution.execution"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "id", "data.keycloak_authentication_execution.execution", "id"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "realm_id", "data.keycloak_authentication_execution.execution", "realm_id"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "parent_flow_alias", "data.keycloak_authentication_execution.execution", "parent_flow_alias"),
+					resource.TestCheckResourceAttrPair("keycloak_authentication_execution.execution", "authenticator", "data.keycloak_authentication_execution.execution", "provider_id"),
+					testAccCheckDataKeycloakAuthenticationExecution("data.keycloak_authentication_execution.execution"),
+				),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakDataSourceAuthenticationExecution_errorNoExecutions(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	parentFlowAlias := acctest.RandString(20)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config:      testDataSourceKeycloakAuthenticationExecution_errorNoExecutions(realm, parentFlowAlias),
+				ExpectError: regexp.MustCompile("no authentication executions found for parent flow alias .*"),
+			},
+		},
+	})
+}
+
+func TestAccKeycloakDataSourceAuthenticationExecution_errorWrongProviderId(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	parentFlowAlias := acctest.RandString(20)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakAuthenticationExecutionConfigDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config:      testDataSourceKeycloakAuthenticationExecution_errorWrongProviderId(realm, parentFlowAlias, acctest.RandString(10)),
+				ExpectError: regexp.MustCompile("no authentication execution under parent flow alias .* with provider id .* found"),
+			},
+		},
+	})
+}
+
+func testAccCheckDataKeycloakAuthenticationExecution(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return fmt.Errorf("resource not found: %s", resourceName)
+		}
+
+		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+		id := rs.Primary.ID
+		realmID := rs.Primary.Attributes["realm_id"]
+		parentFlowAlias := rs.Primary.Attributes["parent_flow_alias"]
+		providerID := rs.Primary.Attributes["provider_id"]
+
+		authenticationExecutionInfo, err := keycloakClient.GetAuthenticationExecutionInfoFromProviderId(realmID, parentFlowAlias, providerID)
+		if err != nil {
+			return err
+		}
+
+		if authenticationExecutionInfo.Id != id {
+			return fmt.Errorf("expected authenticationExecutionInfo with ID %s but got %s", id, authenticationExecutionInfo.Id)
+		}
+
+		return nil
+	}
+}
+
+func testDataSourceKeycloakAuthenticationExecution_basic(realm, parentFlowAlias string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm   = "%s"
+	enabled = true
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = keycloak_realm.realm.id
+	alias    = "%s"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = keycloak_realm.realm.id
+	parent_flow_alias = keycloak_authentication_flow.flow.alias
+	authenticator     = "identity-provider-redirector"
+	requirement       = "REQUIRED"
+}
+
+data "keycloak_authentication_execution" "execution" {
+	realm_id 			= keycloak_realm.realm.id
+	parent_flow_alias   = keycloak_authentication_flow.flow.alias
+	provider_id     	= "identity-provider-redirector"
+}
+	`, realm, parentFlowAlias)
+}
+
+func testDataSourceKeycloakAuthenticationExecution_errorNoExecutions(realm, parentFlowAlias string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm   = "%s"
+	enabled = true
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = keycloak_realm.realm.id
+	alias    = "%s"
+}
+
+data "keycloak_authentication_execution" "execution" {
+	realm_id 			= keycloak_realm.realm.id
+	parent_flow_alias   = keycloak_authentication_flow.flow.alias
+	provider_id     	= "foo"
+}
+	`, realm, parentFlowAlias)
+}
+
+func testDataSourceKeycloakAuthenticationExecution_errorWrongProviderId(realm, parentFlowAlias, providerId string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm   = "%s"
+	enabled = true
+}
+
+resource "keycloak_authentication_flow" "flow" {
+	realm_id = keycloak_realm.realm.id
+	alias    = "%s"
+}
+
+resource "keycloak_authentication_execution" "execution" {
+	realm_id          = keycloak_realm.realm.id
+	parent_flow_alias = keycloak_authentication_flow.flow.alias
+	authenticator     = "identity-provider-redirector"
+	requirement       = "REQUIRED"
+}
+
+data "keycloak_authentication_execution" "execution" {
+	realm_id 			= keycloak_realm.realm.id
+	parent_flow_alias   = keycloak_authentication_flow.flow.alias
+	provider_id     	= "%s"
+}
+	`, realm, parentFlowAlias, providerId)
+}
diff --git a/provider/data_source_keycloak_user_test.go b/provider/data_source_keycloak_user_test.go
new file mode 100644
index 0000000..180da23
--- /dev/null
+++ b/provider/data_source_keycloak_user_test.go
@@ -0,0 +1,83 @@
+package provider
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
+)
+
+func TestAccKeycloakDataSourceUser(t *testing.T) {
+	realm := "terraform-" + acctest.RandString(10)
+	username := acctest.RandString(10)
+
+	resource.Test(t, resource.TestCase{
+		ProviderFactories: testAccProviderFactories,
+		PreCheck:          func() { testAccPreCheck(t) },
+		CheckDestroy:      testAccCheckKeycloakUserDestroy(),
+		Steps: []resource.TestStep{
+			{
+				Config: testDataSourceKeycloakUser(realm, username),
+				Check: resource.ComposeTestCheckFunc(
+					testAccCheckKeycloakUserExists("keycloak_user.user"),
+					resource.TestCheckResourceAttrPair("keycloak_user.user", "id", "data.keycloak_user.user", "id"),
+					resource.TestCheckResourceAttrPair("keycloak_user.user", "realm_id", "data.keycloak_user.user", "realm_id"),
+					resource.TestCheckResourceAttrPair("keycloak_user.user", "username", "data.keycloak_user.user", "username"),
+					testAccCheckDataKeycloakUser("data.keycloak_user.user"),
+				),
+			},
+		},
+	})
+}
+
+func testAccCheckDataKeycloakUser(resourceName string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[resourceName]
+		if !ok {
+			return fmt.Errorf("resource not found: %s", resourceName)
+		}
+
+		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
+
+		id := rs.Primary.ID
+		realmID := rs.Primary.Attributes["realm_id"]
+		username := rs.Primary.Attributes["username"]
+
+		user, err := keycloakClient.GetUser(realmID, id)
+		if err != nil {
+			return err
+		}
+
+		if user.Username != username {
+			return fmt.Errorf("expected user with ID %s to have username %s, but got %s", id, username, user.Username)
+		}
+
+		return nil
+	}
+}
+
+func testDataSourceKeycloakUser(realm, username string) string {
+	return fmt.Sprintf(`
+resource "keycloak_realm" "realm" {
+	realm 		= "%s"
+}
+
+resource "keycloak_user" "user" {
+	username    = "%s"
+	realm_id 	= "${keycloak_realm.realm.id}"
+	enabled    	= true
+
+    email      	= "bob@domain.com"
+    first_name 	= "Bob"
+    last_name  	= "Bobson"
+}
+
+data "keycloak_user" "user" {
+	realm_id 	= "${keycloak_realm.realm.id}"
+	username    = "${keycloak_user.user.username}"
+}
+	`, realm, username)
+}
diff --git a/provider/resource_keycloak_authentication_execution_test.go b/provider/resource_keycloak_authentication_execution_test.go
index a98bdcd..3842f97 100644
--- a/provider/resource_keycloak_authentication_execution_test.go
+++ b/provider/resource_keycloak_authentication_execution_test.go
@@ -2,11 +2,12 @@ package provider
 
 import (
 	"fmt"
+	"testing"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
 	"github.com/mrparkers/terraform-provider-keycloak/keycloak"
-	"testing"
 )
 
 func TestAccKeycloakAuthenticationExecution_basic(t *testing.T) {