Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Userpass: Update the password and policies associated to user #1216

Merged
merged 15 commits into from Mar 16, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 TestBacken_userCreateOperation(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestBacken -> TestBackend

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/password/" + user,
Data: map[string]interface{}{
"password": password,
},
}
}

func testUpdatePolicies(t *testing.T, user, policies string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "users/policies/" + user,
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
80 changes: 80 additions & 0 deletions builtin/credential/userpass/path_user_password.go
@@ -0,0 +1,80 @@
package userpass

import (
"fmt"
"strings"

"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/password/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Username for this user.",
},

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

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

HelpSynopsis: pathUserPasswordHelpSyn,
HelpDescription: pathUserPasswordHelpDesc,
}
}

func (b *backend) pathUserPasswordUpdate(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := strings.ToLower(d.Get("name").(string))
if username == "" {
return nil, fmt.Errorf("missing username")
}

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

userEntry, err := b.User(req.Storage, username)
if err != nil {
return nil, err
}
if userEntry == nil {
return nil, nil
}

// Generate a hash of the password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}

// Set the new password hash
userEntry.PasswordHash = hash

// Store the UserEntry
err = b.SetUser(req.Storage, username, userEntry)
if err != nil {
return nil, err
}
return nil, nil
}

const pathUserPasswordHelpSyn = `
Reset user's password.
`

const pathUserPasswordHelpDesc = `
This endpoint allows resetting the user's password.
`
69 changes: 69 additions & 0 deletions builtin/credential/userpass/path_user_policies.go
@@ -0,0 +1,69 @@
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/policies/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Username for this user.",
},
"policies": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Comma-separated list of policies",
},
},

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

HelpSynopsis: pathUserPoliciesHelpSyn,
HelpDescription: pathUserPoliciesHelpDesc,
}
}
func (b *backend) pathUserPoliciesUpdate(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := strings.ToLower(d.Get("name").(string))
if username == "" {
return nil, fmt.Errorf("missing username")
}

policies := strings.Split(d.Get("policies").(string), ",")
for i, p := range policies {
policies[i] = strings.TrimSpace(p)
}

userEntry, err := b.User(req.Storage, username)
if err != nil {
return nil, err
}
if userEntry == nil {
return nil, nil
}

userEntry.Policies = policies

// Store the UserEntry
err = b.SetUser(req.Storage, username, userEntry)
if err != nil {
return nil, err
}
return nil, nil
}

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

const pathUserPoliciesHelpDesc = `
This endpoint allows updating the policies associated with the username.
`
42 changes: 36 additions & 6 deletions builtin/credential/userpass/path_users.go
Expand Up @@ -43,14 +43,31 @@ func pathUsers(b *backend) *framework.Path {
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.DeleteOperation: b.pathUserDelete,
logical.ReadOperation: b.pathUserRead,
logical.UpdateOperation: b.pathUserWrite,
logical.UpdateOperation: b.pathUserWrite,
logical.CreateOperation: b.pathUserWrite,
},

ExistenceCheck: b.userExistenceCheck,

HelpSynopsis: pathUserHelpSyn,
HelpDescription: pathUserHelpDesc,
}
}

func (b *backend) userExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) {
username := data.Get("name").(string)
if username == "" {
return false, fmt.Errorf("name cannot be empty")
}

entry, err := req.Storage.Get("user/" + strings.ToLower(username))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use the same logic here that you're using in userCreateUpdate, just to be safe in case something diverges. So that probably means using b.user() here.

if err != nil {
return false, err
}

return entry != nil, nil
}

func (b *backend) User(s logical.Storage, n string) (*UserEntry, error) {
entry, err := s.Get("user/" + strings.ToLower(n))
if err != nil {
Expand All @@ -68,6 +85,15 @@ func (b *backend) User(s logical.Storage, n string) (*UserEntry, error) {
return &result, nil
}

func (b *backend) SetUser(s logical.Storage, username string, userEntry *UserEntry) error {
entry, err := logical.StorageEntryJSON("user/"+username, userEntry)
if err != nil {
return err
}

return s.Put(entry)
}

func (b *backend) pathUserDelete(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
err := req.Storage.Delete("user/" + strings.ToLower(d.Get("name").(string)))
Expand Down Expand Up @@ -98,7 +124,15 @@ func (b *backend) pathUserRead(
func (b *backend) pathUserWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := strings.ToLower(d.Get("name").(string))
if name == "" {
return nil, fmt.Errorf("missing username")
}

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

policies := strings.Split(d.Get("policies").(string), ",")
for i, p := range policies {
policies[i] = strings.TrimSpace(p)
Expand All @@ -117,8 +151,7 @@ func (b *backend) pathUserWrite(
return logical.ErrorResponse(fmt.Sprintf("err: %s", err)), nil
}

// Store it
entry, err := logical.StorageEntryJSON("user/"+name, &UserEntry{
err = b.SetUser(req.Storage, name, &UserEntry{
PasswordHash: hash,
Policies: policies,
TTL: ttl,
Expand All @@ -127,9 +160,6 @@ func (b *backend) pathUserWrite(
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}

return nil, nil
}
Expand Down