ldap.go 7.88 KB
Newer Older
ale's avatar
ale committed
1 2 3 4 5 6
package server

import (
	"context"
	"errors"
	"io/ioutil"
ale's avatar
ale committed
7
	"log"
ale's avatar
ale committed
8 9
	"strings"

10
	ldaputil "git.autistici.org/ai3/go-common/ldap"
11
	ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
12
	"github.com/tstranex/u2f"
ale's avatar
ale committed
13
	"gopkg.in/ldap.v3"
ale's avatar
ale committed
14
	"gopkg.in/yaml.v2"
ale's avatar
ale committed
15 16

	"git.autistici.org/id/auth/backend"
ale's avatar
ale committed
17 18
)

ale's avatar
ale committed
19
// ldapServiceParams defines a search to be performed when looking up
ale's avatar
ale committed
20
// a user for a service.
ale's avatar
ale committed
21
type ldapServiceParams struct {
ale's avatar
ale committed
22 23
	// SearchBase, SearchFilter and Scope define parameters for
	// the LDAP search. The search should return a single object.
24 25 26
	// SearchBase or SearchFilter should contain the string "%s",
	// which will be replaced with the username before performing
	// a query.
ale's avatar
ale committed
27 28
	SearchBase   string `yaml:"search_base"`
	SearchFilter string `yaml:"search_filter"`
ale's avatar
ale committed
29
	Scope        string `yaml:"scope"`
ale's avatar
ale committed
30 31 32 33 34 35 36 37 38

	// Attrs tells us which LDAP attributes to query to find user
	// attributes. It is encoded as a {user_attribute:
	// ldap_attribute} map, where user attributes include 'email',
	// 'password', 'app_specific_password', 'totp_secret', and
	// more).
	Attrs map[string]string `yaml:"attrs"`
}

39
// The default attribute mapping just happens to match our schema.
ale's avatar
ale committed
40 41 42 43
var defaultLDAPAttributeMap = map[string]string{
	"password":              "userPassword",
	"totp_secret":           "totpSecret",
	"app_specific_password": "appSpecificPassword",
44
	"u2f_registration":      "u2fRegistration",
ale's avatar
ale committed
45 46
}

47 48 49 50 51 52 53
func dropCryptPrefix(s string) string {
	if strings.HasPrefix(s, "{crypt}") || strings.HasPrefix(s, "{CRYPT}") {
		return s[7:]
	}
	return s
}

ale's avatar
ale committed
54 55 56 57 58 59 60 61 62 63 64 65 66 67
func getStringFromLDAPEntry(entry *ldap.Entry, attr string) string {
	if attr == "" {
		return ""
	}
	return entry.GetAttributeValue(attr)
}

func getListFromLDAPEntry(entry *ldap.Entry, attr string) []string {
	if attr == "" {
		return nil
	}
	return entry.GetAttributeValues(attr)
}

ale's avatar
ale committed
68 69
func decodeAppSpecificPasswordList(encodedAsps []string) []*backend.AppSpecificPassword {
	var out []*backend.AppSpecificPassword
ale's avatar
ale committed
70
	for _, enc := range encodedAsps {
71
		if p, err := ct.UnmarshalAppSpecificPassword(enc); err == nil {
ale's avatar
ale committed
72
			out = append(out, &backend.AppSpecificPassword{
73 74 75
				Service:           p.Service,
				EncryptedPassword: []byte(p.EncryptedPassword),
			})
ale's avatar
ale committed
76 77 78 79 80
		}
	}
	return out
}

81 82 83
func decodeU2FRegistrationList(encRegs []string) []u2f.Registration {
	var out []u2f.Registration
	for _, enc := range encRegs {
84 85 86 87
		if r, err := ct.UnmarshalU2FRegistration(enc); err == nil {
			if reg, err := r.Decode(); err == nil {
				out = append(out, *reg)
			}
88 89 90 91 92
		}
	}
	return out
}

ale's avatar
ale committed
93 94
// Global configuration for the LDAP user backend.
type ldapConfig struct {
ale's avatar
ale committed
95 96
	URI        string `yaml:"uri"`
	BindDN     string `yaml:"bind_dn"`
97
	BindPw     string `yaml:"bind_pw"`
ale's avatar
ale committed
98 99 100 101
	BindPwFile string `yaml:"bind_pw_file"`
}

// Valid returns an error if the configuration is invalid.
ale's avatar
ale committed
102
func (c *ldapConfig) valid() error {
ale's avatar
ale committed
103 104 105 106 107 108
	if c.URI == "" {
		return errors.New("empty uri")
	}
	if c.BindDN == "" {
		return errors.New("empty bind_dn")
	}
109 110
	if (c.BindPwFile == "" && c.BindPw == "") || (c.BindPwFile != "" && c.BindPw != "") {
		return errors.New("only one of bind_pw_file or bind_pw must be set")
ale's avatar
ale committed
111 112 113 114 115
	}
	return nil
}

type ldapBackend struct {
ale's avatar
ale committed
116
	config *ldapConfig
117
	pool   *ldaputil.ConnectionPool
ale's avatar
ale committed
118 119
}

ale's avatar
ale committed
120 121
// New returns a new LDAP backend.
func New(params yaml.MapSlice, configDir string) (backend.UserBackend, error) {
ale's avatar
ale committed
122 123
	// Unmarshal and validate configuration.
	var lc ldapConfig
ale's avatar
ale committed
124
	if err := backend.UnmarshalMapSlice(params, &lc); err != nil {
ale's avatar
ale committed
125 126
		return nil, err
	}
ale's avatar
ale committed
127
	if err := lc.valid(); err != nil {
ale's avatar
ale committed
128 129 130 131
		return nil, err
	}

	// Read the bind password.
ale's avatar
ale committed
132 133
	bindPw := lc.BindPw
	if lc.BindPwFile != "" {
ale's avatar
ale committed
134
		pwData, err := ioutil.ReadFile(backend.ResolvePath(lc.BindPwFile, configDir))
135 136 137
		if err != nil {
			return nil, err
		}
138
		bindPw = strings.TrimSpace(string(pwData))
ale's avatar
ale committed
139 140 141
	}

	// Initialize the connection pool.
ale's avatar
ale committed
142
	pool, err := ldaputil.NewConnectionPool(lc.URI, lc.BindDN, bindPw, 5)
ale's avatar
ale committed
143 144 145 146 147
	if err != nil {
		return nil, err
	}

	return &ldapBackend{
ale's avatar
ale committed
148
		config: &lc,
ale's avatar
ale committed
149 150 151 152 153 154 155 156
		pool:   pool,
	}, nil
}

func (b *ldapBackend) Close() {
	b.pool.Close()
}

ale's avatar
ale committed
157
func (b *ldapBackend) NewServiceBackend(spec *backend.Spec) (backend.ServiceBackend, error) {
ale's avatar
ale committed
158
	var params ldapServiceParams
ale's avatar
ale committed
159
	if err := backend.UnmarshalMapSlice(spec.Params, &params); err != nil {
ale's avatar
ale committed
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
		return nil, err
	}
	return newLDAPServiceBackend(b.pool, &params)
}

type ldapServiceBackend struct {
	pool     *ldaputil.ConnectionPool
	base     string
	filter   string
	scope    int
	attrList []string
	attrs    map[string]string
}

func newLDAPServiceBackend(pool *ldaputil.ConnectionPool, params *ldapServiceParams) (*ldapServiceBackend, error) {
	if params.SearchBase == "" {
		return nil, errors.New("empty search_base")
	}
	if params.SearchFilter == "" {
		return nil, errors.New("empty search_filter")
	}
	scope := ldap.ScopeWholeSubtree
	if params.Scope != "" {
		s, err := ldaputil.ParseScope(params.Scope)
		if err != nil {
			return nil, err
		}
		scope = s
	}

	// Merge in attributes from the default map if unset, and
	// convert them to a list to pass to NewSearchRequest.
	for attrDst, attrSrc := range defaultLDAPAttributeMap {
		if _, ok := params.Attrs[attrDst]; !ok {
			params.Attrs[attrDst] = attrSrc
		}
	}
	var attrList []string
	for _, attrSrc := range params.Attrs {
		attrList = append(attrList, attrSrc)
	}

	return &ldapServiceBackend{
		pool:     pool,
		base:     params.SearchBase,
		filter:   params.SearchFilter,
		scope:    scope,
		attrList: attrList,
		attrs:    params.Attrs,
	}, nil
}

// Build a SearchRequest for this username.
func (b *ldapServiceBackend) searchRequest(username string) *ldap.SearchRequest {
	base := strings.Replace(b.base, "%s", escapeDN(username), -1)
	filter := strings.Replace(b.filter, "%s", ldap.EscapeFilter(username), -1)
	return ldap.NewSearchRequest(
		base,
		b.scope,
		ldap.NeverDerefAliases,
		0,
		0,
		false,
		filter,
		b.attrList,
		nil,
	)
}

// Build a User object from a LDAP response.
ale's avatar
ale committed
230
func (b *ldapServiceBackend) userFromResponse(username string, result *ldap.SearchResult) (*backend.User, bool) {
ale's avatar
ale committed
231
	if len(result.Entries) < 1 {
ale's avatar
ale committed
232 233
		return nil, false
	}
ale's avatar
ale committed
234 235 236 237 238 239 240
	// TODO: return an error if more than one entry is returned.

	entry := result.Entries[0]

	// Apply the attribute map. We don't care if an attribute is
	// not defined in the map, as the get* functions will silently
	// ignore an empty attribute name.
ale's avatar
ale committed
241
	u := backend.User{
ale's avatar
ale committed
242 243 244 245 246 247 248 249 250 251 252
		Name:                 username,
		Email:                getStringFromLDAPEntry(entry, b.attrs["email"]),
		Shard:                getStringFromLDAPEntry(entry, b.attrs["shard"]),
		EncryptedPassword:    []byte(dropCryptPrefix(getStringFromLDAPEntry(entry, b.attrs["password"]))),
		TOTPSecret:           getStringFromLDAPEntry(entry, b.attrs["totp_secret"]),
		AppSpecificPasswords: decodeAppSpecificPasswordList(getListFromLDAPEntry(entry, b.attrs["app_specific_password"])),
		U2FRegistrations:     decodeU2FRegistrationList(getListFromLDAPEntry(entry, b.attrs["u2f_registration"])),
	}

	return &u, true
}
ale's avatar
ale committed
253

ale's avatar
ale committed
254
func (b *ldapServiceBackend) GetUser(ctx context.Context, name string) (*backend.User, bool) {
ale's avatar
ale committed
255
	result, err := b.pool.Search(ctx, b.searchRequest(name))
ale's avatar
ale committed
256
	if err != nil {
ale's avatar
ale committed
257
		log.Printf("LDAP error: %v", err)
ale's avatar
ale committed
258 259
		return nil, false
	}
ale's avatar
ale committed
260
	return b.userFromResponse(name, result)
ale's avatar
ale committed
261
}
262

263
var hexChars = "0123456789abcdef"
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290

func mustEscape(c byte) bool {
	return (c > 0x7f || c == '<' || c == '>' || c == '\\' || c == '*' ||
		c == '"' || c == ',' || c == '+' || c == ';' || c == 0)
}

// escapeDN escapes from the provided LDAP RDN value string the
// special characters in the 'escaped' set and those out of the range
// 0 < c < 0x80, as defined in RFC4515.
//
//  escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE
//
func escapeDN(s string) string {
	escape := 0
	for i := 0; i < len(s); i++ {
		if mustEscape(s[i]) {
			escape++
		}
	}
	if escape == 0 {
		return s
	}
	buf := make([]byte, len(s)+escape*2)
	for i, j := 0, 0; i < len(s); i++ {
		c := s[i]
		if mustEscape(c) {
			buf[j+0] = '\\'
291 292
			buf[j+1] = hexChars[c>>4]
			buf[j+2] = hexChars[c&0xf]
293 294 295 296 297 298 299 300
			j += 3
		} else {
			buf[j] = c
			j++
		}
	}
	return string(buf)
}