Skip to content

Commit

Permalink
Merge pull request #1216 from hashicorp/userpass-update
Browse files Browse the repository at this point in the history
Userpass: Update the password and policies associated to user
  • Loading branch information
vishalnayak committed Mar 16, 2016
2 parents 2fc3c23 + 4ae83b7 commit b9b4f45
Show file tree
Hide file tree
Showing 8 changed files with 569 additions and 46 deletions.
2 changes: 2 additions & 0 deletions builtin/credential/userpass/backend.go
Expand Up @@ -29,6 +29,8 @@ func Backend() *framework.Backend {

Paths: append([]*framework.Path{
pathUsers(&b),
pathUserPolicies(&b),
pathUserPassword(&b),
},
mfa.MFAPaths(b.Backend, pathLogin(&b))...,
),
Expand Down
110 changes: 107 additions & 3 deletions builtin/credential/userpass/backend_test.go
Expand Up @@ -81,7 +81,7 @@ func TestBackend_basic(t *testing.T) {
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepUser(t, "web", "password", "foo"),
testAccStepLogin(t, "web", "password"),
testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
},
})
}
Expand Down Expand Up @@ -109,6 +109,98 @@ func TestBackend_userCrud(t *testing.T) {
})
}

func TestBackend_userCreateOperation(t *testing.T) {
b, err := Factory(&logical.BackendConfig{
Logger: nil,
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: testSysTTL,
MaxLeaseTTLVal: testSysMaxTTL,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}

logicaltest.Test(t, logicaltest.TestCase{
Backend: b,
Steps: []logicaltest.TestStep{
testUserCreateOperation(t, "web", "password", "foo"),
testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
},
})
}

func TestBackend_passwordUpdate(t *testing.T) {
b, err := Factory(&logical.BackendConfig{
Logger: nil,
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: testSysTTL,
MaxLeaseTTLVal: testSysMaxTTL,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}

logicaltest.Test(t, logicaltest.TestCase{
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepUser(t, "web", "password", "foo"),
testAccStepReadUser(t, "web", "foo"),
testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
testUpdatePassword(t, "web", "newpassword"),
testAccStepLogin(t, "web", "newpassword", []string{"default", "foo"}),
},
})

}

func TestBackend_policiesUpdate(t *testing.T) {
b, err := Factory(&logical.BackendConfig{
Logger: nil,
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: testSysTTL,
MaxLeaseTTLVal: testSysMaxTTL,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}

logicaltest.Test(t, logicaltest.TestCase{
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepUser(t, "web", "password", "foo"),
testAccStepReadUser(t, "web", "foo"),
testAccStepLogin(t, "web", "password", []string{"default", "foo"}),
testUpdatePolicies(t, "web", "foo,bar"),
testAccStepReadUser(t, "web", "foo,bar"),
testAccStepLogin(t, "web", "password", []string{"bar", "default", "foo"}),
},
})

}

func testUpdatePassword(t *testing.T, user, password string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "users/" + user + "/password",
Data: map[string]interface{}{
"password": password,
},
}
}

func testUpdatePolicies(t *testing.T, user, policies string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "users/" + user + "/policies",
Data: map[string]interface{}{
"policies": policies,
},
}
}

func testUsersWrite(t *testing.T, user string, data map[string]interface{}, expectError bool) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Expand Down Expand Up @@ -139,7 +231,7 @@ func testLoginWrite(t *testing.T, user string, data map[string]interface{}, expe
}
}

func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestStep {
func testAccStepLogin(t *testing.T, user string, pass string, policies []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Expand All @@ -148,7 +240,19 @@ func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestSt
},
Unauthenticated: true,

Check: logicaltest.TestCheckAuth([]string{"foo"}),
Check: logicaltest.TestCheckAuth(policies),
}
}

func testUserCreateOperation(
t *testing.T, name string, password string, policies string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.CreateOperation,
Path: "users/" + name,
Data: map[string]interface{}{
"password": password,
"policies": policies,
},
}
}

Expand Down
17 changes: 11 additions & 6 deletions builtin/credential/userpass/path_login.go
Expand Up @@ -2,6 +2,7 @@ package userpass

import (
"crypto/subtle"
"fmt"
"strings"

"github.com/hashicorp/vault/logical"
Expand All @@ -11,9 +12,9 @@ import (

func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: "login/" + framework.GenericNameRegex("name"),
Pattern: "login/" + framework.GenericNameRegex("username"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
"username": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Username of the user.",
},
Expand All @@ -35,16 +36,20 @@ func pathLogin(b *backend) *framework.Path {

func (b *backend) pathLogin(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := strings.ToLower(d.Get("name").(string))
username := strings.ToLower(d.Get("username").(string))

password := d.Get("password").(string)
if password == "" {
return nil, fmt.Errorf("missing password")
}

// Get the user and validate auth
user, err := b.User(req.Storage, username)
user, err := b.user(req.Storage, username)
if err != nil {
return nil, err
}
if user == nil {
return logical.ErrorResponse("unknown username or password"), nil
return logical.ErrorResponse("username does not exist"), nil
}

// Check for a password match. Check for a hash collision for Vault 0.2+,
Expand Down Expand Up @@ -78,7 +83,7 @@ func (b *backend) pathLogin(
func (b *backend) pathLoginRenew(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// Get the user
user, err := b.User(req.Storage, req.Auth.Metadata["username"])
user, err := b.user(req.Storage, req.Auth.Metadata["username"])
if err != nil {
return nil, err
}
Expand Down
85 changes: 85 additions & 0 deletions builtin/credential/userpass/path_user_password.go
@@ -0,0 +1,85 @@
package userpass

import (
"fmt"

"golang.org/x/crypto/bcrypt"

"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)

func pathUserPassword(b *backend) *framework.Path {
return &framework.Path{
Pattern: "users/" + framework.GenericNameRegex("username") + "/password$",
Fields: map[string]*framework.FieldSchema{
"username": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Username for this user.",
},

"password": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Password for this user.",
},
},

ExistenceCheck: b.userPasswordExistenceCheck,

Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathUserPasswordUpdate,
},

HelpSynopsis: pathUserPasswordHelpSyn,
HelpDescription: pathUserPasswordHelpDesc,
}
}

// By always returning true, this endpoint will be enforced to be invoked only upon UpdateOperation.
// The existence of user will be checked in the operation handler.
func (b *backend) userPasswordExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
return true, nil
}

func (b *backend) pathUserPasswordUpdate(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {

username := d.Get("username").(string)

userEntry, err := b.user(req.Storage, username)
if err != nil {
return nil, err
}
if userEntry == nil {
return nil, fmt.Errorf("username does not exist")
}

err = b.updateUserPassword(req, d, userEntry)
if err != nil {
return nil, err
}

return nil, b.setUser(req.Storage, username, userEntry)
}

func (b *backend) updateUserPassword(req *logical.Request, d *framework.FieldData, userEntry *UserEntry) error {
password := d.Get("password").(string)
if password == "" {
return fmt.Errorf("missing password")
}
// Generate a hash of the password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
userEntry.PasswordHash = hash
return nil
}

const pathUserPasswordHelpSyn = `
Reset user's password.
`

const pathUserPasswordHelpDesc = `
This endpoint allows resetting the user's password.
`
78 changes: 78 additions & 0 deletions builtin/credential/userpass/path_user_policies.go
@@ -0,0 +1,78 @@
package userpass

import (
"fmt"
"strings"

"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)

func pathUserPolicies(b *backend) *framework.Path {
return &framework.Path{
Pattern: "users/" + framework.GenericNameRegex("username") + "/policies$",
Fields: map[string]*framework.FieldSchema{
"username": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Username for this user.",
},
"policies": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Comma-separated list of policies",
},
},

ExistenceCheck: b.userPoliciesExistenceCheck,

Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathUserPoliciesUpdate,
},

HelpSynopsis: pathUserPoliciesHelpSyn,
HelpDescription: pathUserPoliciesHelpDesc,
}
}

// By always returning true, this endpoint will be enforced to be invoked only upon UpdateOperation.
// The existence of user will be checked in the operation handler.
func (b *backend) userPoliciesExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
return true, nil
}

func (b *backend) pathUserPoliciesUpdate(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {

username := d.Get("username").(string)

userEntry, err := b.user(req.Storage, username)
if err != nil {
return nil, err
}
if userEntry == nil {
return nil, fmt.Errorf("username does not exist")
}

err = b.updateUserPolicies(req, d, userEntry)
if err != nil {
return nil, err
}

return nil, b.setUser(req.Storage, username, userEntry)
}

func (b *backend) updateUserPolicies(req *logical.Request, d *framework.FieldData, userEntry *UserEntry) error {
policies := strings.Split(d.Get("policies").(string), ",")
for i, p := range policies {
policies[i] = strings.TrimSpace(p)
}
userEntry.Policies = policies
return nil
}

const pathUserPoliciesHelpSyn = `
Update the policies associated with the username.
`

const pathUserPoliciesHelpDesc = `
This endpoint allows updating the policies associated with the username.
`

0 comments on commit b9b4f45

Please sign in to comment.