diff --git a/builtin/credential/ldap/backend.go b/builtin/credential/ldap/backend.go index db4b9fb09..941e1860d 100644 --- a/builtin/credential/ldap/backend.go +++ b/builtin/credential/ldap/backend.go @@ -21,6 +21,7 @@ func Backend() *framework.Backend { Root: []string{ "config", "groups/*", + "users/*", }, Unauthenticated: []string{ @@ -32,6 +33,7 @@ func Backend() *framework.Backend { pathLogin(&b), pathConfig(&b), pathGroups(&b), + pathUsers(&b), }), AuthRenew: b.pathLoginRenew, @@ -135,6 +137,12 @@ func (b *backend) Login(req *logical.Request, username string, password string) var allgroups []string var policies []string + + user, err := b.User(req.Storage, username) + if err == nil && user != nil { + policies = append(policies, user.Policies...) + } + for _, e := range sresult.Entries { dn, err := ldap.ParseDN(e.DN) if err != nil || len(dn.RDNs) == 0 || len(dn.RDNs[0].Attributes) == 0 { diff --git a/builtin/credential/ldap/backend_test.go b/builtin/credential/ldap/backend_test.go index c1a6f53b1..1cdc32ad7 100644 --- a/builtin/credential/ldap/backend_test.go +++ b/builtin/credential/ldap/backend_test.go @@ -17,6 +17,7 @@ func TestBackend_basic(t *testing.T) { Steps: []logicaltest.TestStep{ testAccStepConfigUrl(t), testAccStepGroup(t, "scientists", "foo"), + testAccStepUser(t, "tesla", "bar"), testAccStepLogin(t, "tesla", "password"), }, }) @@ -96,6 +97,65 @@ func testAccStepDeleteGroup(t *testing.T, group string) logicaltest.TestStep { } } +func TestBackend_userCrud(t *testing.T) { + b := Backend() + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepUser(t, "g1", "bar"), + testAccStepReadUser(t, "g1", "bar"), + testAccStepDeleteUser(t, "g1"), + testAccStepReadUser(t, "g1", ""), + }, + }) +} + +func testAccStepUser(t *testing.T, user string, policies string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "users/" + user, + Data: map[string]interface{}{ + "policies": policies, + }, + } +} + +func testAccStepReadUser(t *testing.T, user string, policies string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "users/" + user, + 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 + }, + } +} + +func testAccStepDeleteUser(t *testing.T, user string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.DeleteOperation, + Path: "users/" + user, + } +} + func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.WriteOperation, @@ -105,7 +165,7 @@ func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestSt }, Unauthenticated: true, - Check: logicaltest.TestCheckAuth([]string{"foo"}), + Check: logicaltest.TestCheckAuth([]string{"foo", "bar"}), } } diff --git a/builtin/credential/ldap/path_users.go b/builtin/credential/ldap/path_users.go new file mode 100644 index 000000000..288fe49db --- /dev/null +++ b/builtin/credential/ldap/path_users.go @@ -0,0 +1,118 @@ +package ldap + +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.+)`, + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the LDAP user.", + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Comma-separated list of policies associated to the user.", + }, + }, + + 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{ + 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 { + Policies []string +} + +const pathUserHelpSyn = ` +Manage users allowed to authenticate. +` + +const pathUserHelpDesc = ` +This endpoint allows you to create, read, update, and delete configuration +for LDAP users that are allowed to authenticate, and associate policies to +them. + +Deleting a user will not revoke auth for prior authenticated users in that +user. To do this, do a revoke on "login/" for +the usernames you want revoked. +`