Skip to content

Commit

Permalink
feat: adds initial_password attribute for keycloak_user (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
Floby authored and mrparkers committed Jan 7, 2019
1 parent 97930e5 commit 2d90243
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 4 deletions.
27 changes: 23 additions & 4 deletions docs/resources/keycloak_user.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,26 @@ resource "keycloak_realm" "realm" {
resource "keycloak_user" "user" {
realm_id = "${keycloak_realm.realm.id}"
username = "bob"
enabled = true
enabled = true
email = "bob@domain.com"
first_name = "Bob"
last_name = "Bobson"
}
resource "keycloak_user" "user_with_initial_password" {
realm_id = "${keycloak_realm.realm.id}"
username = "alice"
enabled = true
email = "alice@domain.com"
first_name = "Alice"
last_name = "Aliceberg"
email = "bob@domain.com"
first_name = "Bob"
last_name = "Bobson"
initial_password {
value = "some password"
temporary = true
}
}
```

Expand All @@ -31,6 +46,10 @@ The following arguments are supported:

- `realm_id` - (Required) The realm this user belongs to.
- `username` - (Required) The unique username of this user.
- `initial_password` (Optional) When given, the user's initial password will be set.
This attribute is only respected during initial user creation.
- `value` (Required) The initial password.
- `temporary` (Optional) If set to `true`, the initial password is set up for renewal on first use. Default to `false`.
- `enabled` - (Optional) When false, this user cannot log in. Defaults to `true`.
- `email` - (Optional) The user's email.
- `first_name` - (Optional) The user's first name.
Expand Down
14 changes: 14 additions & 0 deletions example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ resource "keycloak_user" "another_user" {
last_name = "Tester"
}

resource "keycloak_user" "user_with_password" {
realm_id = "${keycloak_realm.test.id}"
username = "user-with-password"

email = "user-with-password@fakedomain.com"
first_name = "Testy"
last_name = "Tester"
initial_password {
value = "my password"
temporary = false
}
}


resource "keycloak_group_memberships" "foo_members" {
realm_id = "${keycloak_realm.test.id}"
group_id = "${keycloak_group.foo.id}"
Expand Down
20 changes: 20 additions & 0 deletions keycloak/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ type User struct {
Enabled bool `json:"enabled"`
}

type PasswordCredentials struct {
Value string `json:"value"`
Type string `json:"type"`
Temporary bool `json:"temporary"`
}

func (keycloakClient *KeycloakClient) NewUser(user *User) error {
location, err := keycloakClient.post(fmt.Sprintf("/realms/%s/users", user.RealmId), user)
if err != nil {
Expand All @@ -27,6 +33,20 @@ func (keycloakClient *KeycloakClient) NewUser(user *User) error {
return nil
}

func (keycloakClient *KeycloakClient) ResetUserPassword(realmId, userId string, newPassword string, isTemporary bool) error {
resetCredentials := &PasswordCredentials{
Value: newPassword,
Type: "password",
Temporary: isTemporary,
}

err := keycloakClient.put(fmt.Sprintf("/realms/%s/users/%s/reset-password", realmId, userId), resetCredentials)
if err != nil {
return err
}
return nil
}

func (keycloakClient *KeycloakClient) GetUser(realmId, id string) (*User, error) {
var user User

Expand Down
35 changes: 35 additions & 0 deletions provider/keycloak_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,26 @@ func resourceKeycloakUser() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"initial_password": {
Type: schema.TypeList,
Optional: true,
DiffSuppressFunc: onlyDiffOnCreate,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"value": {
Type: schema.TypeString,
Required: true,
Sensitive: true,
},
"temporary": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
},
},
"enabled": {
Type: schema.TypeBool,
Optional: true,
Expand All @@ -48,6 +68,10 @@ func resourceKeycloakUser() *schema.Resource {
}
}

func onlyDiffOnCreate(k, old, new string, d *schema.ResourceData) bool {
return d.Id() != ""
}

func mapFromDataToUser(data *schema.ResourceData) *keycloak.User {
return &keycloak.User{
Id: data.Id(),
Expand Down Expand Up @@ -81,6 +105,17 @@ func resourceKeycloakUserCreate(data *schema.ResourceData, meta interface{}) err
return err
}

v, isInitialPasswordSet := data.GetOk("initial_password")
if isInitialPasswordSet {
passwordBlock := v.([]interface{})[0].(map[string]interface{})
passwordValue := passwordBlock["value"].(string)
isPasswordTemporary := passwordBlock["temporary"].(bool)
err := keycloakClient.ResetUserPassword(user.RealmId, user.Id, passwordValue, isPasswordTemporary)
if err != nil {
return err
}
}

mapFromUserToData(data, user)

return resourceKeycloakUserRead(data, meta)
Expand Down
118 changes: 118 additions & 0 deletions provider/keycloak_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import (
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"testing"
)

Expand Down Expand Up @@ -34,6 +39,30 @@ func TestAccKeycloakUser_basic(t *testing.T) {
})
}

func TestAccKeycloakUser_withInitialPassword(t *testing.T) {
realmName := "terraform-" + acctest.RandString(10)
username := "terraform-user-" + acctest.RandString(10)
password := "terraform-password-" + acctest.RandString(10)
clientId := "terraform-client-" + acctest.RandString(10)

resourceName := "keycloak_user.user"

resource.Test(t, resource.TestCase{
Providers: testAccProviders,
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testAccCheckKeycloakUserDestroy(),
Steps: []resource.TestStep{
{
Config: testKeycloakUser_initialPassword(realmName, username, password, clientId),
Check: resource.ComposeTestCheckFunc(
testAccCheckKeycloakUserExists(resourceName),
testAccCheckKeycloakUserInitialPasswordLogin(realmName, username, password, clientId),
),
},
},
})
}

func TestAccKeycloakUser_createAfterManualDestroy(t *testing.T) {
var user = &keycloak.User{}

Expand Down Expand Up @@ -130,6 +159,34 @@ func TestAccKeycloakUser_updateUsername(t *testing.T) {
})
}

func TestAccKeycloakUser_updateWithInitialPasswordChangeDoesNotReset(t *testing.T) {
realmName := "terraform-" + acctest.RandString(10)
username := "terraform-user-" + acctest.RandString(10)
passwordOne := "terraform-password1-" + acctest.RandString(10)
passwordTwo := "terraform-password2-" + acctest.RandString(10)
clientId := "terraform-client-" + acctest.RandString(10)

resource.Test(t, resource.TestCase{
Providers: testAccProviders,
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testAccCheckKeycloakUserDestroy(),
Steps: []resource.TestStep{
{
Config: testKeycloakUser_initialPassword(realmName, username, passwordOne, clientId),
Check: resource.ComposeTestCheckFunc(
testAccCheckKeycloakUserInitialPasswordLogin(realmName, username, passwordOne, clientId),
),
},
{
Config: testKeycloakUser_initialPassword(realmName, username, passwordTwo, clientId),
Check: resource.ComposeTestCheckFunc(
testAccCheckKeycloakUserInitialPasswordLogin(realmName, username, passwordOne, clientId),
),
},
},
})
}

func TestAccKeycloakUser_updateInPlace(t *testing.T) {
userOne := &keycloak.User{
RealmId: "terraform-" + acctest.RandString(10),
Expand Down Expand Up @@ -227,6 +284,39 @@ func testAccCheckKeycloakUserFetch(resourceName string, user *keycloak.User) res
}
}

func testAccCheckKeycloakUserInitialPasswordLogin(realmName string, username string, password string, clientId string) resource.TestCheckFunc {
return func(s *terraform.State) error {
httpClient := &http.Client{}

resourceUrl := fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/token", os.Getenv("KEYCLOAK_URL"), realmName)

form := url.Values{}
form.Add("username", username)
form.Add("password", password)
form.Add("client_id", clientId)
form.Add("grant_type", "password")

request, err := http.NewRequest(http.MethodPost, resourceUrl, strings.NewReader(form.Encode()))
if err != nil {
return err
}
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")

response, err := httpClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()

if response.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(response.Body)
return fmt.Errorf("user with username %s cannot login with password %s\n body: %s", username, password, string(body))
}

return nil
}
}

func testAccCheckKeycloakUserDestroy() resource.TestCheckFunc {
return func(s *terraform.State) error {
for _, rs := range s.RootModule().Resources {
Expand Down Expand Up @@ -281,6 +371,34 @@ resource "keycloak_user" "user" {
`, realm, username)
}

func testKeycloakUser_initialPassword(realm, username string, password string, clientId string) string {
return fmt.Sprintf(`
resource "keycloak_realm" "realm" {
realm = "%s"
}
resource "keycloak_openid_client" "client" {
realm_id = "${keycloak_realm.realm.id}"
client_id = "%s"
name = "test client"
enabled = true
access_type = "PUBLIC"
direct_access_grants_enabled = true
}
resource "keycloak_user" "user" {
realm_id = "${keycloak_realm.realm.id}"
username = "%s"
initial_password {
value = "%s"
temporary = false
}
}
`, realm, clientId, username, password)
}

func testKeycloakUser_updateRealmBefore(realmOne, realmTwo, username string) string {
return fmt.Sprintf(`
resource "keycloak_realm" "realm_1" {
Expand Down

0 comments on commit 2d90243

Please sign in to comment.