Initial implementation of the LDAP credential backend
This commit is contained in:
parent
843d9e6484
commit
7492c5712a
50
builtin/credential/ldap/backend.go
Normal file
50
builtin/credential/ldap/backend.go
Normal 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".
|
||||
`
|
62
builtin/credential/ldap/cli.go
Normal file
62
builtin/credential/ldap/cli.go
Normal 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)
|
||||
}
|
143
builtin/credential/ldap/path_config.go
Normal file
143
builtin/credential/ldap/path_config.go
Normal 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.
|
||||
`
|
117
builtin/credential/ldap/path_login.go
Normal file
117
builtin/credential/ldap/path_login.go
Normal 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.
|
||||
`
|
|
@ -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
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue