Select Git revision
instrumentation.go
Forked from
ai3 / tools / acmeserver
Source project has a limited visibility.
-
ale authored
Add dns-01 support, make the code more readable, add a testing mode that will generate self-signed certificates (for test environments that are not reachable from outside).
ale authoredAdd dns-01 support, make the code more readable, add a testing mode that will generate self-signed certificates (for test environments that are not reachable from outside).
ldap.go NaN GiB
package server
import (
"context"
"errors"
"io/ioutil"
"log"
"strings"
ldaputil "git.autistici.org/ai3/go-common/ldap"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
"github.com/duo-labs/webauthn/webauthn"
"github.com/go-ldap/ldap/v3"
"gopkg.in/yaml.v3"
"git.autistici.org/id/auth/backend"
)
// ldapServiceParams defines a search to be performed when looking up
// a user for a service.
type ldapServiceParams struct {
// SearchBase, SearchFilter and Scope define parameters for
// the LDAP search. The search should return a single object.
// SearchBase or SearchFilter should contain the string "%s",
// which will be replaced with the username before performing
// a query.
SearchBase string `yaml:"search_base"`
SearchFilter string `yaml:"search_filter"`
Scope string `yaml:"scope"`
// 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"`
}
// The default attribute mapping just happens to match our schema.
var defaultLDAPAttributeMap = map[string]string{
"password": "userPassword",
"totp_secret": "totpSecret",
"app_specific_password": "appSpecificPassword",
"u2f_registration": "u2fRegistration",
}
func dropCryptPrefix(s string) string {
if strings.HasPrefix(s, "{crypt}") || strings.HasPrefix(s, "{CRYPT}") {
return s[7:]
}
return s
}
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)
}
func decodeAppSpecificPasswordList(encodedAsps []string) []*backend.AppSpecificPassword {
var out []*backend.AppSpecificPassword
for _, enc := range encodedAsps {
if p, err := ct.UnmarshalAppSpecificPassword(enc); err == nil {
out = append(out, &backend.AppSpecificPassword{
Service: p.Service,
EncryptedPassword: []byte(p.EncryptedPassword),
})
}
}
return out
}
func decodeU2FRegistrationList(encRegs []string) []webauthn.Credential {
var out []webauthn.Credential
for _, enc := range encRegs {
if r, err := ct.UnmarshalU2FRegistration(enc); err == nil {
if cred, err := r.Decode(); err == nil {
out = append(out, cred)
}
}
}
return out
}
// Global configuration for the LDAP user backend.
type ldapConfig struct {
URI string `yaml:"uri"`
BindDN string `yaml:"bind_dn"`
BindPw string `yaml:"bind_pw"`
BindPwFile string `yaml:"bind_pw_file"`
}
// Valid returns an error if the configuration is invalid.
func (c *ldapConfig) valid() error {
if c.URI == "" {
return errors.New("empty uri")
}
if c.BindDN == "" {
return errors.New("empty bind_dn")
}
if (c.BindPwFile == "" && c.BindPw == "") || (c.BindPwFile != "" && c.BindPw != "") {
return errors.New("only one of bind_pw_file or bind_pw must be set")
}
return nil
}
type ldapBackend struct {
config *ldapConfig
pool *ldaputil.ConnectionPool
}
// New returns a new LDAP backend.
func New(params *yaml.Node, configDir string) (backend.UserBackend, error) {
// Unmarshal and validate configuration.
var lc ldapConfig
if err := params.Decode(&lc); err != nil {
return nil, err
}
if err := lc.valid(); err != nil {
return nil, err
}
// Read the bind password.
bindPw := lc.BindPw
if lc.BindPwFile != "" {
pwData, err := ioutil.ReadFile(backend.ResolvePath(lc.BindPwFile, configDir))
if err != nil {
return nil, err
}
bindPw = strings.TrimSpace(string(pwData))
}
// Initialize the connection pool.
pool, err := ldaputil.NewConnectionPool(lc.URI, lc.BindDN, bindPw, 5)
if err != nil {
return nil, err
}
return &ldapBackend{
config: &lc,
pool: pool,
}, nil
}
func (b *ldapBackend) Close() {
b.pool.Close()
}
func (b *ldapBackend) NewServiceBackend(spec *backend.Spec) (backend.ServiceBackend, error) {
var params ldapServiceParams
if err := spec.Params.Decode(¶ms); err != nil {
return nil, err
}
return newLDAPServiceBackend(b.pool, ¶ms)
}
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.
attrs := make(map[string]string)
for k, v := range defaultLDAPAttributeMap {
attrs[k] = v
}
for k, v := range params.Attrs {
attrs[k] = v
}
var attrList []string
for _, v := range attrs {
attrList = append(attrList, v)
}
return &ldapServiceBackend{
pool: pool,
base: params.SearchBase,
filter: params.SearchFilter,
scope: scope,
attrList: attrList,
attrs: 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.
func (b *ldapServiceBackend) userFromResponse(username string, result *ldap.SearchResult) (*backend.User, bool) {
if len(result.Entries) < 1 {
return nil, false
}
// 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.
u := backend.User{
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"])),
WebAuthnRegistrations: decodeU2FRegistrationList(getListFromLDAPEntry(entry, b.attrs["u2f_registration"])),
}
return &u, true
}
func (b *ldapServiceBackend) GetUser(ctx context.Context, name string) (*backend.User, bool) {
result, err := b.pool.Search(ctx, b.searchRequest(name))
if err != nil {
// Only log unexpected errors.
if !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
log.Printf("LDAP error: %v", err)
}
return nil, false
}
return b.userFromResponse(name, result)
}
var hexChars = "0123456789abcdef"
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] = '\\'
buf[j+1] = hexChars[c>>4]
buf[j+2] = hexChars[c&0xf]
j += 3
} else {
buf[j] = c
j++
}
}
return string(buf)
}