diff --git a/docs/data-sources/authentication_execution.md b/docs/data-sources/authentication_execution.md new file mode 100644 index 000000000..182a2bba2 --- /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 000000000..9b59fd8d3 --- /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 b8b2d6b7a..840d955f6 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 000000000..a4cc1c337 --- /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_authentication_execution_test.go b/provider/data_source_keycloak_authentication_execution_test.go new file mode 100644 index 000000000..405981401 --- /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.go b/provider/data_source_keycloak_user.go new file mode 100644 index 000000000..902f285b5 --- /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/data_source_keycloak_user_test.go b/provider/data_source_keycloak_user_test.go new file mode 100644 index 000000000..180da239d --- /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/provider.go b/provider/provider.go index 016e5ef52..d8ca5a7c2 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 65ab4920d..e1f4857ad 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_authentication_execution_test.go b/provider/resource_keycloak_authentication_execution_test.go index a98bdcd3b..3842f97d7 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) { diff --git a/provider/resource_keycloak_group.go b/provider/resource_keycloak_group.go index 038d5bf34..21d7ecf7f 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 {