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"
|
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
|
||||||
credCert "github.com/hashicorp/vault/builtin/credential/cert"
|
credCert "github.com/hashicorp/vault/builtin/credential/cert"
|
||||||
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
|
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
|
||||||
|
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
|
||||||
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/builtin/logical/aws"
|
"github.com/hashicorp/vault/builtin/logical/aws"
|
||||||
|
@ -58,6 +59,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
||||||
"app-id": credAppId.Factory,
|
"app-id": credAppId.Factory,
|
||||||
"github": credGitHub.Factory,
|
"github": credGitHub.Factory,
|
||||||
"userpass": credUserpass.Factory,
|
"userpass": credUserpass.Factory,
|
||||||
|
"ldap": credLdap.Factory,
|
||||||
},
|
},
|
||||||
LogicalBackends: map[string]logical.Factory{
|
LogicalBackends: map[string]logical.Factory{
|
||||||
"aws": aws.Factory,
|
"aws": aws.Factory,
|
||||||
|
@ -81,6 +83,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
||||||
Handlers: map[string]command.AuthHandler{
|
Handlers: map[string]command.AuthHandler{
|
||||||
"github": &credGitHub.CLIHandler{},
|
"github": &credGitHub.CLIHandler{},
|
||||||
"userpass": &credUserpass.CLIHandler{},
|
"userpass": &credUserpass.CLIHandler{},
|
||||||
|
"ldap": &credLdap.CLIHandler{},
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue