1package ldap 2 3import ( 4 "context" 5 "fmt" 6 "strings" 7 8 "github.com/hashicorp/vault/helper/mfa" 9 "github.com/hashicorp/vault/sdk/framework" 10 "github.com/hashicorp/vault/sdk/helper/ldaputil" 11 "github.com/hashicorp/vault/sdk/helper/strutil" 12 "github.com/hashicorp/vault/sdk/logical" 13) 14 15func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { 16 b := Backend() 17 if err := b.Setup(ctx, conf); err != nil { 18 return nil, err 19 } 20 return b, nil 21} 22 23func Backend() *backend { 24 var b backend 25 b.Backend = &framework.Backend{ 26 Help: backendHelp, 27 28 PathsSpecial: &logical.Paths{ 29 Root: mfa.MFARootPaths(), 30 31 Unauthenticated: []string{ 32 "login/*", 33 }, 34 35 SealWrapStorage: []string{ 36 "config", 37 }, 38 }, 39 40 Paths: append([]*framework.Path{ 41 pathConfig(&b), 42 pathGroups(&b), 43 pathGroupsList(&b), 44 pathUsers(&b), 45 pathUsersList(&b), 46 }, 47 mfa.MFAPaths(b.Backend, pathLogin(&b))..., 48 ), 49 50 AuthRenew: b.pathLoginRenew, 51 BackendType: logical.TypeCredential, 52 } 53 54 return &b 55} 56 57type backend struct { 58 *framework.Backend 59} 60 61func (b *backend) Login(ctx context.Context, req *logical.Request, username string, password string) ([]string, *logical.Response, []string, error) { 62 63 cfg, err := b.Config(ctx, req) 64 if err != nil { 65 return nil, nil, nil, err 66 } 67 if cfg == nil { 68 return nil, logical.ErrorResponse("ldap backend not configured"), nil, nil 69 } 70 71 if cfg.DenyNullBind && len(password) == 0 { 72 return nil, logical.ErrorResponse("password cannot be of zero length when passwordless binds are being denied"), nil, nil 73 } 74 75 ldapClient := ldaputil.Client{ 76 Logger: b.Logger(), 77 LDAP: ldaputil.NewLDAP(), 78 } 79 80 c, err := ldapClient.DialLDAP(cfg.ConfigEntry) 81 if err != nil { 82 return nil, logical.ErrorResponse(err.Error()), nil, nil 83 } 84 if c == nil { 85 return nil, logical.ErrorResponse("invalid connection returned from LDAP dial"), nil, nil 86 } 87 88 // Clean connection 89 defer c.Close() 90 91 userBindDN, err := ldapClient.GetUserBindDN(cfg.ConfigEntry, c, username) 92 if err != nil { 93 if b.Logger().IsDebug() { 94 b.Logger().Debug("error getting user bind DN", "error", err) 95 } 96 return nil, logical.ErrorResponse("ldap operation failed"), nil, nil 97 } 98 99 if b.Logger().IsDebug() { 100 b.Logger().Debug("user binddn fetched", "username", username, "binddn", userBindDN) 101 } 102 103 // Try to bind as the login user. This is where the actual authentication takes place. 104 if len(password) > 0 { 105 err = c.Bind(userBindDN, password) 106 } else { 107 err = c.UnauthenticatedBind(userBindDN) 108 } 109 if err != nil { 110 if b.Logger().IsDebug() { 111 b.Logger().Debug("ldap bind failed", "error", err) 112 } 113 return nil, logical.ErrorResponse("ldap operation failed"), nil, nil 114 } 115 116 // We re-bind to the BindDN if it's defined because we assume 117 // the BindDN should be the one to search, not the user logging in. 118 if cfg.BindDN != "" && cfg.BindPassword != "" { 119 if err := c.Bind(cfg.BindDN, cfg.BindPassword); err != nil { 120 if b.Logger().IsDebug() { 121 b.Logger().Debug("error while attempting to re-bind with the BindDN User", "error", err) 122 } 123 return nil, logical.ErrorResponse("ldap operation failed"), nil, nil 124 } 125 if b.Logger().IsDebug() { 126 b.Logger().Debug("re-bound to original binddn") 127 } 128 } 129 130 userDN, err := ldapClient.GetUserDN(cfg.ConfigEntry, c, userBindDN) 131 if err != nil { 132 return nil, logical.ErrorResponse(err.Error()), nil, nil 133 } 134 135 ldapGroups, err := ldapClient.GetLdapGroups(cfg.ConfigEntry, c, userDN, username) 136 if err != nil { 137 return nil, logical.ErrorResponse(err.Error()), nil, nil 138 } 139 if b.Logger().IsDebug() { 140 b.Logger().Debug("groups fetched from server", "num_server_groups", len(ldapGroups), "server_groups", ldapGroups) 141 } 142 143 ldapResponse := &logical.Response{ 144 Data: map[string]interface{}{}, 145 } 146 if len(ldapGroups) == 0 { 147 errString := fmt.Sprintf( 148 "no LDAP groups found in groupDN '%s'; only policies from locally-defined groups available", 149 cfg.GroupDN) 150 ldapResponse.AddWarning(errString) 151 } 152 153 var allGroups []string 154 canonicalUsername := username 155 cs := *cfg.CaseSensitiveNames 156 if !cs { 157 canonicalUsername = strings.ToLower(username) 158 } 159 // Import the custom added groups from ldap backend 160 user, err := b.User(ctx, req.Storage, canonicalUsername) 161 if err == nil && user != nil && user.Groups != nil { 162 if b.Logger().IsDebug() { 163 b.Logger().Debug("adding local groups", "num_local_groups", len(user.Groups), "local_groups", user.Groups) 164 } 165 allGroups = append(allGroups, user.Groups...) 166 } 167 // Merge local and LDAP groups 168 allGroups = append(allGroups, ldapGroups...) 169 170 canonicalGroups := allGroups 171 // If not case sensitive, lowercase all 172 if !cs { 173 canonicalGroups = make([]string, len(allGroups)) 174 for i, v := range allGroups { 175 canonicalGroups[i] = strings.ToLower(v) 176 } 177 } 178 179 // Retrieve policies 180 var policies []string 181 for _, groupName := range canonicalGroups { 182 group, err := b.Group(ctx, req.Storage, groupName) 183 if err == nil && group != nil { 184 policies = append(policies, group.Policies...) 185 } 186 } 187 if user != nil && user.Policies != nil { 188 policies = append(policies, user.Policies...) 189 } 190 // Policies from each group may overlap 191 policies = strutil.RemoveDuplicates(policies, true) 192 193 return policies, ldapResponse, allGroups, nil 194} 195 196const backendHelp = ` 197The "ldap" credential provider allows authentication querying 198a LDAP server, checking username and password, and associating groups 199to set of policies. 200 201Configuration of the server is done through the "config" and "groups" 202endpoints by a user with root access. Authentication is then done 203by supplying the two fields for "login". 204` 205