diff --git a/builtin/credential/userpass/backend.go b/builtin/credential/userpass/backend.go new file mode 100644 index 0000000000..36ee4324a8 --- /dev/null +++ b/builtin/credential/userpass/backend.go @@ -0,0 +1,47 @@ +package userpass + +import ( + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func Factory(map[string]string) (logical.Backend, error) { + return Backend(), nil +} + +func Backend() *framework.Backend { + var b backend + b.Backend = &framework.Backend{ + Help: backendHelp, + + PathsSpecial: &logical.Paths{ + Root: []string{ + "users/*", + }, + + Unauthenticated: []string{ + "login", + }, + }, + + Paths: append([]*framework.Path{ + pathUsers(&b), + }), + } + + return b.Backend +} + +type backend struct { + *framework.Backend +} + +const backendHelp = ` +The "userpass" credential provider allows authentication using +a combination of a username and password. No additional factors +are supported. + +The username/password combination is configured using the "users/" +endpoints by a user with root access. Authentication is then done +by suppying the two fields for "login". +` diff --git a/builtin/credential/userpass/backend_test.go b/builtin/credential/userpass/backend_test.go new file mode 100644 index 0000000000..02664c557b --- /dev/null +++ b/builtin/credential/userpass/backend_test.go @@ -0,0 +1,72 @@ +package userpass + +import ( + "fmt" + "testing" + + "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" + "github.com/mitchellh/mapstructure" +) + +func TestBackend_userCrud(t *testing.T) { + b := Backend() + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepUser(t, "web", "password", "foo"), + testAccStepReadUser(t, "web", "foo"), + testAccStepDeleteUser(t, "web"), + testAccStepReadUser(t, "web", ""), + }, + }) +} + +func testAccStepUser( + t *testing.T, name string, password string, policies string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "users/" + name, + Data: map[string]interface{}{ + "password": password, + "policies": policies, + }, + } +} + +func testAccStepDeleteUser(t *testing.T, n string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.DeleteOperation, + Path: "users/" + n, + } +} + +func testAccStepReadUser(t *testing.T, name string, policies string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "users/" + name, + Check: func(resp *logical.Response) error { + if resp == nil { + if policies == "" { + return nil + } + + return fmt.Errorf("bad: %#v", resp) + } + + var d struct { + Policies string `mapstructure:"policies"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + + if d.Policies != policies { + return fmt.Errorf("bad: %#v", resp) + } + + return nil + }, + } +} diff --git a/builtin/credential/userpass/path_users.go b/builtin/credential/userpass/path_users.go new file mode 100644 index 0000000000..6558a2a5c0 --- /dev/null +++ b/builtin/credential/userpass/path_users.go @@ -0,0 +1,120 @@ +package userpass + +import ( + "strings" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathUsers(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `users/(?P\w+)`, + 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.", + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Comma-separated list of policies", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.DeleteOperation: b.pathUserDelete, + logical.ReadOperation: b.pathUserRead, + logical.WriteOperation: b.pathUserWrite, + }, + + HelpSynopsis: pathUserHelpSyn, + HelpDescription: pathUserHelpDesc, + } +} + +func (b *backend) User(s logical.Storage, n string) (*UserEntry, error) { + entry, err := s.Get("user/" + n) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result UserEntry + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + return &result, nil +} + +func (b *backend) pathUserDelete( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + err := req.Storage.Delete("user/" + d.Get("name").(string)) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (b *backend) pathUserRead( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + user, err := b.User(req.Storage, d.Get("name").(string)) + if err != nil { + return nil, err + } + if user == nil { + return nil, nil + } + + return &logical.Response{ + Data: map[string]interface{}{ + "policies": strings.Join(user.Policies, ","), + }, + }, nil +} + +func (b *backend) pathUserWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + policies := strings.Split(d.Get("policies").(string), ",") + for i, p := range policies { + policies[i] = strings.TrimSpace(p) + } + + // Store it + entry, err := logical.StorageEntryJSON("user/"+name, &UserEntry{ + Password: d.Get("password").(string), + Policies: policies, + }) + if err != nil { + return nil, err + } + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +type UserEntry struct { + Password string + Policies []string +} + +const pathUserHelpSyn = ` +Manage users allowed to authenticate. +` + +const pathUserHelpDesc = ` +This endpoint allows you to create, read, update, and delete users +that are allowed to authenticate. +`