2018-05-10 21:12:42 +00:00
package ldaputil
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
2018-05-22 00:04:26 +00:00
"strings"
"text/template"
2018-05-10 21:12:42 +00:00
"github.com/hashicorp/vault/helper/tlsutil"
2018-05-22 00:04:26 +00:00
"github.com/hashicorp/vault/logical/framework"
"github.com/hashicorp/errwrap"
2018-05-10 21:12:42 +00:00
)
2018-05-22 00:04:26 +00:00
// ConfigFields returns all the config fields that can potentially be used by the LDAP client.
// Not all fields will be used by every integration.
func ConfigFields ( ) map [ string ] * framework . FieldSchema {
return map [ string ] * framework . FieldSchema {
"url" : {
Type : framework . TypeString ,
Default : "ldap://127.0.0.1" ,
Description : "LDAP URL to connect to (default: ldap://127.0.0.1). Multiple URLs can be specified by concatenating them with commas; they will be tried in-order." ,
} ,
"userdn" : {
Type : framework . TypeString ,
Description : "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)" ,
} ,
"binddn" : {
Type : framework . TypeString ,
Description : "LDAP DN for searching for the user DN (optional)" ,
} ,
"bindpass" : {
Type : framework . TypeString ,
Description : "LDAP password for searching for the user DN (optional)" ,
} ,
"groupdn" : {
Type : framework . TypeString ,
Description : "LDAP search base to use for group membership search (eg: ou=Groups,dc=example,dc=org)" ,
} ,
"groupfilter" : {
Type : framework . TypeString ,
Default : "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))" ,
Description : ` Go template for querying group membership of user ( optional )
The template can access the following context variables : UserDN , Username
Example : ( & ( objectClass = group ) ( member : 1.2 .840 .113556 .1 .4 .1941 := { { . UserDN } } ) )
Default : ( | ( memberUid = { { . Username } } ) ( member = { { . UserDN } } ) ( uniqueMember = { { . UserDN } } ) ) ` ,
} ,
"groupattr" : {
Type : framework . TypeString ,
Default : "cn" ,
Description : ` LDAP attribute to follow on objects returned by < groupfilter >
in order to enumerate user group membership .
Examples : "cn" or "memberOf" , etc .
Default : cn ` ,
} ,
"upndomain" : {
Type : framework . TypeString ,
Description : "Enables userPrincipalDomain login with [username]@UPNDomain (optional)" ,
} ,
"userattr" : {
Type : framework . TypeString ,
Default : "cn" ,
Description : "Attribute used for users (default: cn)" ,
} ,
"certificate" : {
Type : framework . TypeString ,
Description : "CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded (optional)" ,
} ,
"discoverdn" : {
Type : framework . TypeBool ,
Description : "Use anonymous bind to discover the bind DN of a user (optional)" ,
} ,
"insecure_tls" : {
Type : framework . TypeBool ,
Description : "Skip LDAP server SSL Certificate verification - VERY insecure (optional)" ,
} ,
"starttls" : {
Type : framework . TypeBool ,
Description : "Issue a StartTLS command after establishing unencrypted connection (optional)" ,
} ,
"tls_min_version" : {
Type : framework . TypeString ,
Default : "tls12" ,
Description : "Minimum TLS version to use. Accepted values are 'tls10', 'tls11' or 'tls12'. Defaults to 'tls12'" ,
} ,
"tls_max_version" : {
Type : framework . TypeString ,
Default : "tls12" ,
Description : "Maximum TLS version to use. Accepted values are 'tls10', 'tls11' or 'tls12'. Defaults to 'tls12'" ,
} ,
"deny_null_bind" : {
Type : framework . TypeBool ,
Default : true ,
Description : "Denies an unauthenticated LDAP bind request if the user's password is empty; defaults to true" ,
} ,
"case_sensitive_names" : {
Type : framework . TypeBool ,
Description : "If true, case sensitivity will be used when comparing usernames and groups for matching policies." ,
} ,
}
}
/ *
* Creates and initializes a ConfigEntry object with its default values ,
* as specified by the passed schema .
* /
func NewConfigEntry ( d * framework . FieldData ) ( * ConfigEntry , error ) {
cfg := new ( ConfigEntry )
url := d . Get ( "url" ) . ( string )
if url != "" {
cfg . Url = strings . ToLower ( url )
}
userattr := d . Get ( "userattr" ) . ( string )
if userattr != "" {
cfg . UserAttr = strings . ToLower ( userattr )
}
userdn := d . Get ( "userdn" ) . ( string )
if userdn != "" {
cfg . UserDN = userdn
}
groupdn := d . Get ( "groupdn" ) . ( string )
if groupdn != "" {
cfg . GroupDN = groupdn
}
groupfilter := d . Get ( "groupfilter" ) . ( string )
if groupfilter != "" {
// Validate the template before proceeding
_ , err := template . New ( "queryTemplate" ) . Parse ( groupfilter )
if err != nil {
return nil , errwrap . Wrapf ( "invalid groupfilter: {{err}}" , err )
}
cfg . GroupFilter = groupfilter
}
groupattr := d . Get ( "groupattr" ) . ( string )
if groupattr != "" {
cfg . GroupAttr = groupattr
}
upndomain := d . Get ( "upndomain" ) . ( string )
if upndomain != "" {
cfg . UPNDomain = upndomain
}
certificate := d . Get ( "certificate" ) . ( string )
if certificate != "" {
block , _ := pem . Decode ( [ ] byte ( certificate ) )
if block == nil || block . Type != "CERTIFICATE" {
return nil , fmt . Errorf ( "failed to decode PEM block in the certificate" )
}
_ , err := x509 . ParseCertificate ( block . Bytes )
if err != nil {
return nil , errwrap . Wrapf ( "failed to parse certificate: {{err}}" , err )
}
cfg . Certificate = certificate
}
insecureTLS := d . Get ( "insecure_tls" ) . ( bool )
if insecureTLS {
cfg . InsecureTLS = insecureTLS
}
cfg . TLSMinVersion = d . Get ( "tls_min_version" ) . ( string )
if cfg . TLSMinVersion == "" {
return nil , fmt . Errorf ( "failed to get 'tls_min_version' value" )
}
var ok bool
_ , ok = tlsutil . TLSLookup [ cfg . TLSMinVersion ]
if ! ok {
return nil , fmt . Errorf ( "invalid 'tls_min_version'" )
}
cfg . TLSMaxVersion = d . Get ( "tls_max_version" ) . ( string )
if cfg . TLSMaxVersion == "" {
return nil , fmt . Errorf ( "failed to get 'tls_max_version' value" )
}
_ , ok = tlsutil . TLSLookup [ cfg . TLSMaxVersion ]
if ! ok {
return nil , fmt . Errorf ( "invalid 'tls_max_version'" )
}
if cfg . TLSMaxVersion < cfg . TLSMinVersion {
return nil , fmt . Errorf ( "'tls_max_version' must be greater than or equal to 'tls_min_version'" )
}
startTLS := d . Get ( "starttls" ) . ( bool )
if startTLS {
cfg . StartTLS = startTLS
}
bindDN := d . Get ( "binddn" ) . ( string )
if bindDN != "" {
cfg . BindDN = bindDN
}
bindPass := d . Get ( "bindpass" ) . ( string )
if bindPass != "" {
cfg . BindPassword = bindPass
}
denyNullBind := d . Get ( "deny_null_bind" ) . ( bool )
if denyNullBind {
cfg . DenyNullBind = denyNullBind
}
discoverDN := d . Get ( "discoverdn" ) . ( bool )
if discoverDN {
cfg . DiscoverDN = discoverDN
}
caseSensitiveNames , ok := d . GetOk ( "case_sensitive_names" )
if ok {
cfg . CaseSensitiveNames = new ( bool )
* cfg . CaseSensitiveNames = caseSensitiveNames . ( bool )
}
return cfg , nil
}
2018-05-10 21:12:42 +00:00
type ConfigEntry struct {
Url string ` json:"url" `
UserDN string ` json:"userdn" `
GroupDN string ` json:"groupdn" `
GroupFilter string ` json:"groupfilter" `
GroupAttr string ` json:"groupattr" `
UPNDomain string ` json:"upndomain" `
UserAttr string ` json:"userattr" `
Certificate string ` json:"certificate" `
InsecureTLS bool ` json:"insecure_tls" `
StartTLS bool ` json:"starttls" `
BindDN string ` json:"binddn" `
BindPassword string ` json:"bindpass" `
DenyNullBind bool ` json:"deny_null_bind" `
DiscoverDN bool ` json:"discoverdn" `
TLSMinVersion string ` json:"tls_min_version" `
TLSMaxVersion string ` json:"tls_max_version" `
// This json tag deviates from snake case because there was a past issue
// where the tag was being ignored, causing it to be jsonified as "CaseSensitiveNames".
// To continue reading in users' previously stored values,
// we chose to carry that forward.
CaseSensitiveNames * bool ` json:"CaseSensitiveNames,omitempty" `
}
2018-05-22 00:04:26 +00:00
func ( c * ConfigEntry ) Map ( ) map [ string ] interface { } {
m := c . PasswordlessMap ( )
m [ "bindpass" ] = c . BindPassword
return m
}
func ( c * ConfigEntry ) PasswordlessMap ( ) map [ string ] interface { } {
2018-05-22 19:10:36 +00:00
m := map [ string ] interface { } {
"url" : c . Url ,
"userdn" : c . UserDN ,
"groupdn" : c . GroupDN ,
"groupfilter" : c . GroupFilter ,
"groupattr" : c . GroupAttr ,
"upndomain" : c . UPNDomain ,
"userattr" : c . UserAttr ,
"certificate" : c . Certificate ,
"insecure_tls" : c . InsecureTLS ,
"starttls" : c . StartTLS ,
"binddn" : c . BindDN ,
"deny_null_bind" : c . DenyNullBind ,
"discoverdn" : c . DiscoverDN ,
"tls_min_version" : c . TLSMinVersion ,
"tls_max_version" : c . TLSMaxVersion ,
2018-05-22 00:04:26 +00:00
}
2018-05-22 19:10:36 +00:00
if c . CaseSensitiveNames != nil {
m [ "case_sensitive_names" ] = * c . CaseSensitiveNames
}
return m
2018-05-22 00:04:26 +00:00
}
2018-05-10 21:12:42 +00:00
func ( c * ConfigEntry ) Validate ( ) error {
if len ( c . Url ) == 0 {
return errors . New ( "at least one url must be provided" )
}
// Note: This logic is driven by the logic in GetUserBindDN.
// If updating this, please also update the logic there.
if ! c . DiscoverDN && ( c . BindDN == "" || c . BindPassword == "" ) && c . UPNDomain == "" && c . UserDN == "" {
return errors . New ( "cannot derive UserBindDN" )
}
tlsMinVersion , ok := tlsutil . TLSLookup [ c . TLSMinVersion ]
if ! ok {
return errors . New ( "invalid 'tls_min_version' in config" )
}
tlsMaxVersion , ok := tlsutil . TLSLookup [ c . TLSMaxVersion ]
if ! ok {
return errors . New ( "invalid 'tls_max_version' in config" )
}
if tlsMaxVersion < tlsMinVersion {
return errors . New ( "'tls_max_version' must be greater than or equal to 'tls_min_version'" )
}
if c . Certificate != "" {
block , _ := pem . Decode ( [ ] byte ( c . Certificate ) )
if block == nil || block . Type != "CERTIFICATE" {
return errors . New ( "failed to decode PEM block in the certificate" )
}
_ , err := x509 . ParseCertificate ( block . Bytes )
if err != nil {
return fmt . Errorf ( "failed to parse certificate %s" , err . Error ( ) )
}
}
return nil
}