diff --git a/builtin/credential/userpass/backend.go b/builtin/credential/userpass/backend.go index d2e62ab52a6f..5505a1ab5e58 100644 --- a/builtin/credential/userpass/backend.go +++ b/builtin/credential/userpass/backend.go @@ -29,6 +29,8 @@ func Backend() *framework.Backend { Paths: append([]*framework.Path{ pathUsers(&b), + pathUserPolicies(&b), + pathUserPassword(&b), }, mfa.MFAPaths(b.Backend, pathLogin(&b))..., ), diff --git a/builtin/credential/userpass/backend_test.go b/builtin/credential/userpass/backend_test.go index c0fc8bea41e4..cdf62d52f7db 100644 --- a/builtin/credential/userpass/backend_test.go +++ b/builtin/credential/userpass/backend_test.go @@ -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"}), }, }) } @@ -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, @@ -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, @@ -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, + }, } } diff --git a/builtin/credential/userpass/path_login.go b/builtin/credential/userpass/path_login.go index 3b6c67b724f2..458d35e8bb98 100644 --- a/builtin/credential/userpass/path_login.go +++ b/builtin/credential/userpass/path_login.go @@ -2,6 +2,7 @@ package userpass import ( "crypto/subtle" + "fmt" "strings" "github.com/hashicorp/vault/logical" @@ -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.", }, @@ -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+, @@ -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 } diff --git a/builtin/credential/userpass/path_user_password.go b/builtin/credential/userpass/path_user_password.go new file mode 100644 index 000000000000..22c08239a800 --- /dev/null +++ b/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. +` diff --git a/builtin/credential/userpass/path_user_policies.go b/builtin/credential/userpass/path_user_policies.go new file mode 100644 index 000000000000..73b9fe6d6a2f --- /dev/null +++ b/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. +` diff --git a/builtin/credential/userpass/path_users.go b/builtin/credential/userpass/path_users.go index abf4a44751f8..21fe4162559a 100644 --- a/builtin/credential/userpass/path_users.go +++ b/builtin/credential/userpass/path_users.go @@ -7,14 +7,13 @@ import ( "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical/framework" - "golang.org/x/crypto/bcrypt" ) func pathUsers(b *backend) *framework.Path { return &framework.Path{ - Pattern: "users/" + framework.GenericNameRegex("name"), + Pattern: "users/" + framework.GenericNameRegex("username"), Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ + "username": &framework.FieldSchema{ Type: framework.TypeString, Description: "Username for this user.", }, @@ -43,16 +42,32 @@ 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) User(s logical.Storage, n string) (*UserEntry, error) { - entry, err := s.Get("user/" + strings.ToLower(n)) +func (b *backend) userExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { + userEntry, err := b.user(req.Storage, data.Get("username").(string)) + if err != nil { + return false, err + } + + return userEntry != nil, nil +} + +func (b *backend) user(s logical.Storage, username string) (*UserEntry, error) { + if username == "" { + return nil, fmt.Errorf("missing username") + } + + entry, err := s.Get("user/" + strings.ToLower(username)) if err != nil { return nil, err } @@ -68,9 +83,18 @@ 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))) + err := req.Storage.Delete("user/" + strings.ToLower(d.Get("username").(string))) if err != nil { return nil, err } @@ -80,7 +104,7 @@ func (b *backend) pathUserDelete( func (b *backend) pathUserRead( req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - user, err := b.User(req.Storage, strings.ToLower(d.Get("name").(string))) + user, err := b.user(req.Storage, strings.ToLower(d.Get("username").(string))) if err != nil { return nil, err } @@ -95,43 +119,56 @@ func (b *backend) pathUserRead( }, nil } -func (b *backend) pathUserWrite( - req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := strings.ToLower(d.Get("name").(string)) - password := d.Get("password").(string) - policies := strings.Split(d.Get("policies").(string), ",") - for i, p := range policies { - policies[i] = strings.TrimSpace(p) - } - - // Generate a hash of the password - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) +func (b *backend) userCreateUpdate(req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + username := strings.ToLower(d.Get("username").(string)) + userEntry, err := b.user(req.Storage, username) if err != nil { return nil, err } + // Due to existence check, user will only be nil if it's a create operation + if userEntry == nil { + userEntry = &UserEntry{} + } - ttlStr := d.Get("ttl").(string) - maxTTLStr := d.Get("max_ttl").(string) - ttl, maxTTL, err := b.SanitizeTTL(ttlStr, maxTTLStr) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("err: %s", err)), nil + if _, ok := d.GetOk("password"); ok { + err = b.updateUserPassword(req, d, userEntry) + if err != nil { + return nil, err + } } - // Store it - entry, err := logical.StorageEntryJSON("user/"+name, &UserEntry{ - PasswordHash: hash, - Policies: policies, - TTL: ttl, - MaxTTL: maxTTL, - }) - if err != nil { - return nil, err + if _, ok := d.GetOk("policies"); ok { + err = b.updateUserPolicies(req, d, userEntry) + if err != nil { + return nil, err + } } - if err := req.Storage.Put(entry); err != nil { - return nil, err + + ttlStr := userEntry.TTL.String() + if ttlStrRaw, ok := d.GetOk("ttl"); ok { + ttlStr = ttlStrRaw.(string) } - return nil, nil + maxTTLStr := userEntry.MaxTTL.String() + if maxTTLStrRaw, ok := d.GetOk("max_ttl"); ok { + maxTTLStr = maxTTLStrRaw.(string) + } + + userEntry.TTL, userEntry.MaxTTL, err = b.SanitizeTTL(ttlStr, maxTTLStr) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf("err: %s", err)), nil + } + + return nil, b.setUser(req.Storage, username, userEntry) +} + +func (b *backend) pathUserWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + password := d.Get("password").(string) + if req.Operation == logical.CreateOperation && password == "" { + return nil, fmt.Errorf("missing password") + } + return b.userCreateUpdate(req, d) } type UserEntry struct { diff --git a/logical/framework/backend.go b/logical/framework/backend.go index 73d77e741b5e..e6749935aabb 100644 --- a/logical/framework/backend.go +++ b/logical/framework/backend.go @@ -249,7 +249,7 @@ func (b *Backend) SanitizeTTL(ttlStr, maxTTLStr string) (ttl, maxTTL time.Durati return 0, 0, fmt.Errorf("\"max_ttl\" value must be less than allowed max lease TTL value '%s'", sysMaxTTL.String()) } } - if ttl > maxTTL { + if ttl > maxTTL && maxTTL != 0 { ttl = maxTTL } return diff --git a/website/source/docs/auth/userpass.html.md b/website/source/docs/auth/userpass.html.md index 2e4a929b51f9..769628353410 100644 --- a/website/source/docs/auth/userpass.html.md +++ b/website/source/docs/auth/userpass.html.md @@ -92,3 +92,215 @@ $ vault write auth/userpass/users/mitchellh \ The above creates a new user "mitchellh" with the password "foo" that will be associated with the "root" policy. This is the only configuration necessary. + +## API + +### /auth/userpass/users/[username] +#### POST + +
+
Description
+
+ Create a new user or update an existing user. + This path honors the distinction between the `create` and `update` capabilities inside ACL policies. +
+ +
Method
+
POST
+ +
URL
+
`/auth/userpass/users/`
+ +
Parameters
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
Returns
+
`204` response code. +
+
+ +### /auth/userpass/users/[username]/password +#### POST +
+
Description
+
+ Update the password for an existing user. +
+ +
Method
+
POST
+ +
URL
+
`/auth/userpass/users//password`
+ +
Parameters
+
+ +
+
+ +
+ +
Returns
+
`204` response code. +
+
+ +### /auth/userpass/users/[username]/policies +#### POST +
+
Description
+
+ Update the policies associated with an existing user. +
+ +
Method
+
POST
+ +
URL
+
`/auth/userpass/users//policies`
+ +
Parameters
+
+ +
+
+ +
+ +
Returns
+
`204` response code. +
+
+ + +### /auth/userpass/login/[username] +#### POST +
+
Description
+
+ Update the policies associated with an existing user. +
+ +
Method
+
POST
+ +
URL
+
`/auth/userpass/users//policies`
+ +
Parameters
+
+ +
+
+ +
+ +
Returns
+
+ + ```javascript + { + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": null, + "warnings": null, + "auth": { + "client_token": "64d2a8f2-2a2f-5688-102b-e6088b76e344", + "accessor": "18bb8f89-826a-56ee-c65b-1736dc5ea27d", + "policies": ["default"], + "metadata": { + "username": "vishal" + }, + "lease_duration": 7200, + "renewable": true + } + } + ``` + +
+