Initial implementation of the LDAP credential backend

This commit is contained in:
Giovanni Bajo 2015-05-06 03:54:27 +02:00
parent 843d9e6484
commit 7492c5712a
5 changed files with 375 additions and 0 deletions

View File

@ -0,0 +1,50 @@
package ldap
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{
"config",
},
Unauthenticated: []string{
"login",
},
},
Paths: append([]*framework.Path{
pathLogin(&b),
pathConfig(&b),
}),
// AuthRenew: b.pathLoginRenew,
}
return b.Backend
}
type backend struct {
*framework.Backend
}
const backendHelp = `
The "ldap" credential provider allows authentication querying
a LDAP server, checking username and password, and associating groups
to set of policies.
Configuration of the server is done through the "config" and "groups"
endpoints by a user with root access. Authentication is then done
by suppying the two fields for "login".
`

View File

@ -0,0 +1,62 @@
package ldap
import (
"fmt"
"os"
"strings"
"github.com/hashicorp/vault/api"
pwd "github.com/hashicorp/vault/helper/password"
)
type CLIHandler struct{}
func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) {
mount, ok := m["mount"]
if !ok {
mount = "ldap"
}
username, ok := m["username"]
if !ok {
return "", fmt.Errorf("'username' var must be set")
}
password, ok := m["password"]
if !ok {
fmt.Printf("Password (will be hidden): ")
var err error
password, err = pwd.Read(os.Stdin)
fmt.Println()
if err != nil {
return "", err
}
}
path := fmt.Sprintf("auth/%s/login", mount)
secret, err := c.Logical().Write(path, map[string]interface{}{
"username": username,
"password": password,
})
if err != nil {
return "", err
}
if secret == nil {
return "", fmt.Errorf("empty response from credential provider")
}
return secret.Auth.ClientToken, nil
}
func (h *CLIHandler) Help() string {
help := `
The LDAP credential provider allows you to authenticate with LDAP.
To use it, first configure it through the "config" endpoint, and then
login by specifying username and password. If password is not provided
on the command line, it will be read from stdin.
Example: vault auth -method=ldap username=john
`
return strings.TrimSpace(help)
}

View File

@ -0,0 +1,143 @@
package ldap
import (
"net/url"
"strings"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfig(b *backend) *framework.Path {
return &framework.Path{
Pattern: `config`,
Fields: map[string]*framework.FieldSchema{
"url": &framework.FieldSchema{
Type: framework.TypeString,
Description: "ldap URL to connect to (default: ldap://127.0.0.1)",
},
"domain": &framework.FieldSchema{
Type: framework.TypeString,
Description: "LDAP domain to use (eg: dc=example,dc=org)",
},
"userattr": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Attribute used for users (default: cn)",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathConfigRead,
logical.WriteOperation: b.pathConfigWrite,
},
HelpSynopsis: pathConfigHelpSyn,
HelpDescription: pathConfigHelpDesc,
}
}
func (b *backend) Config(req *logical.Request) (*ConfigEntry, error) {
entry, err := req.Storage.Get("config")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result ConfigEntry
result.SetDefaults()
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathConfigRead(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg, err := b.Config(req)
if err != nil {
return nil, err
}
if cfg == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"url": cfg.Url,
"domain": cfg.Domain,
"userattr": cfg.UserAttr,
},
}, nil
}
func (b *backend) pathConfigWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg := &ConfigEntry{}
url := d.Get("url").(string)
if url != "" {
cfg.Url = strings.ToLower(url)
}
userattr := d.Get("userattr").(string)
if url != "" {
cfg.UserAttr = strings.ToLower(userattr)
}
domain := d.Get("domain").(string)
if url != "" {
cfg.Domain = domain
}
if !cfg.ValidateURL() {
return logical.ErrorResponse("LDAP URL is malformed"), nil
}
entry, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
type ConfigEntry struct {
Url string
Domain string
UserAttr string
}
func (c *ConfigEntry) ValidateURL() bool {
u, err := url.Parse(c.Url)
if err != nil {
return false
}
if u.Scheme != "ldap" && u.Scheme != "ldaps" {
return false
}
if u.Path != "" {
return false
}
return true
}
func (c *ConfigEntry) SetDefaults() {
c.Url = "ldap://127.0.0.1"
c.UserAttr = "cn"
}
const pathConfigHelpSyn = `
Configure the LDAP server to connect to.
`
const pathConfigHelpDesc = `
This endpoint allows you to configure the LDAP server to connect to, and give
basic information of the schema of that server.
The LDAP URL can use either the "ldap://" or "ldaps://" schema. In the former
case, an unencrypted connection will be done, with default port 389; in the latter
case, a SSL connection will be done, with default port 636.
`

View File

@ -0,0 +1,117 @@
package ldap
import (
"fmt"
"net"
"net/url"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
"github.com/mmitton/ldap"
)
func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: `login`,
Fields: map[string]*framework.FieldSchema{
"username": &framework.FieldSchema{
Type: framework.TypeString,
Description: "DN (distinguished name) to be used for login.",
},
"password": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Password for this user.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathLogin,
},
HelpSynopsis: pathLoginSyn,
HelpDescription: pathLoginDesc,
}
}
func (b *backend) pathLogin(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := d.Get("username").(string)
password := d.Get("password").(string)
cfg, err := b.Config(req)
if err != nil {
return nil, err
}
if cfg == nil {
return logical.ErrorResponse("ldap backend not configured"), nil
}
u, err := url.Parse(cfg.Url)
if err != nil {
return nil, err
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host = u.Host
}
var c *ldap.Conn
var cerr *ldap.Error
switch u.Scheme {
case "ldap":
if port == "" {
port = "389"
}
c, cerr = ldap.Dial("tcp", host+":"+port)
case "ldaps":
if port == "" {
port = "636"
}
c, cerr = ldap.DialSSL("tcp", host+":"+port)
default:
return logical.ErrorResponse("invalid LDAP URL scheme"), nil
}
if cerr != nil {
return nil, cerr
}
binddn := fmt.Sprintf("%s=%s,%s", cfg.UserAttr, username, cfg.Domain)
cerr = c.Bind(binddn, password)
if cerr != nil {
return logical.ErrorResponse(fmt.Sprintf("LDAP bind failed: %s", cerr.Error())), nil
}
return &logical.Response{
Auth: &logical.Auth{
Policies: []string{"root"},
Metadata: map[string]string{
"username": username,
},
DisplayName: username,
},
}, nil
}
// func (b *backend) pathLoginRenew(
// req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// // Get the user and validate auth
// user, err := b.User(req.Storage, req.Auth.Metadata["username"])
// if err != nil {
// return nil, err
// }
// if user == nil {
// // User no longer exists, do not renew
// return nil, nil
// }
// return framework.LeaseExtend(1*time.Hour, 0)(req, d)
// }
const pathLoginSyn = `
Log in with a username and password.
`
const pathLoginDesc = `
This endpoint authenticates using a username and password.
`

View File

@ -9,6 +9,7 @@ import (
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/builtin/logical/aws"
@ -58,6 +59,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
"app-id": credAppId.Factory,
"github": credGitHub.Factory,
"userpass": credUserpass.Factory,
"ldap": credLdap.Factory,
},
LogicalBackends: map[string]logical.Factory{
"aws": aws.Factory,
@ -81,6 +83,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
Handlers: map[string]command.AuthHandler{
"github": &credGitHub.CLIHandler{},
"userpass": &credUserpass.CLIHandler{},
"ldap": &credLdap.CLIHandler{},
},
}, nil
},