Commit 21a62aa5 authored by ale's avatar ale
Browse files

Merge branch 'webauthn' into 'master'

Support Webauthn

See merge request !42
parents 3746d131 b3540bb1
Pipeline #24851 passed with stages
in 3 minutes and 25 seconds
......@@ -251,7 +251,10 @@ func randomAppSpecificPasswordID() string {
}
func generateTOTPSecret() (string, error) {
key, err := totp.Generate(totp.GenerateOpts{})
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "accountserver",
AccountName: "placeholder",
})
if err != nil {
return "", err
}
......
package accountserver
import (
"errors"
"fmt"
"log"
)
......@@ -196,7 +195,7 @@ func (r *CheckResourceAvailabilityRequest) Validate(rctx *RequestContext) error
func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interface{}, error) {
var check ValidatorFunc
switch r.Type {
case ResourceTypeEmail, ResourceTypeMailingList:
case ResourceTypeEmail, ResourceTypeMailingList, ResourceTypeNewsletter:
check = rctx.validationCtx.isAvailableEmailAddr()
case ResourceTypeDomain:
check = rctx.validationCtx.isAvailableDomain()
......@@ -207,7 +206,7 @@ func (r *CheckResourceAvailabilityRequest) Serve(rctx *RequestContext) (interfac
case ResourceTypeDatabase:
check = rctx.validationCtx.isAvailableDatabase()
default:
return nil, errors.New("unknown resource type")
return nil, newValidationError(nil, "type", "unknown resource type")
}
var resp CheckResourceAvailabilityResponse
......
......@@ -22,7 +22,7 @@ type fakeBackend struct {
passwords map[string]string
recoveryPasswords map[string]string
resourcePasswords map[string]string
appSpecificPasswords map[string][]*AppSpecificPasswordInfo
appSpecificPasswords map[string][]*ct.AppSpecificPassword
encryptionKeys map[string][]*ct.EncryptedKey
}
......@@ -175,7 +175,7 @@ func (b *fakeBackend) SetUserEncryptionPublicKey(_ context.Context, user *User,
return nil
}
func (b *fakeBackend) SetApplicationSpecificPassword(_ context.Context, user *User, info *AppSpecificPasswordInfo, _ string) error {
func (b *fakeBackend) SetApplicationSpecificPassword(_ context.Context, user *User, info *ct.AppSpecificPassword, _ string) error {
b.appSpecificPasswords[user.Name] = append(b.appSpecificPasswords[user.Name], info)
return nil
}
......@@ -268,7 +268,7 @@ func createFakeBackend() *fakeBackend {
passwords: make(map[string]string),
recoveryPasswords: make(map[string]string),
resourcePasswords: make(map[string]string),
appSpecificPasswords: make(map[string][]*AppSpecificPasswordInfo),
appSpecificPasswords: make(map[string][]*ct.AppSpecificPassword),
encryptionKeys: make(map[string][]*ct.EncryptedKey),
}
fb.addUser(&User{
......
......@@ -2,7 +2,9 @@ package accountserver
import (
"errors"
"strings"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
umdb "git.autistici.org/id/usermetadb"
)
......@@ -156,7 +158,6 @@ func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error {
return nil
}
// TODO: call out to auth-server for rate limiting and other features.
// Authenticate the secret recovery password.
if err := rctx.authorizeAccountRecovery(rctx.Context, rctx.User.Name, r.RecoveryPassword, r.RemoteAddr); err != nil {
return err
......@@ -296,7 +297,7 @@ func (r *CreateApplicationSpecificPasswordRequest) Serve(rctx *RequestContext) (
}
// Create a new application-specific password metadata.
asp := &AppSpecificPasswordInfo{
asp := &ct.AppSpecificPassword{
ID: randomAppSpecificPasswordID(),
Service: r.Service,
Comment: r.Notes,
......@@ -345,7 +346,7 @@ func (r *EnableOTPRequest) Sanitize() {
func (r *EnableOTPRequest) Validate(_ *RequestContext) error {
var err *ValidationError
// Only check if the client-side secret is set, skip otherwise.
if r.TOTPSecret == "" && len(r.TOTPSecret) != 16 {
if r.TOTPSecret != "" && len(r.TOTPSecret) != 16 {
err = newValidationError(err, "totp_secret", "bad value")
}
return err.orNil()
......@@ -402,23 +403,28 @@ func (r *DisableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
// UpdateUserRequest allows the caller to update a (very limited) selected set
// of fields on a User object. It is a catch-all function for very simple
// changes that don't justify their own specialized method.
// changes that don't justify their own specialized method. Fields are
// associated with a "set_field" attribute to allow for selective updates.
type UpdateUserRequest struct {
UserRequestBase
Lang string `json:"lang,omitempty"`
U2FRegistrations []*U2FRegistration `json:"u2f_registrations,omitempty"`
Lang string `json:"lang,omitempty"`
SetLang bool `json:"set_lang"`
U2FRegistrations []*ct.U2FRegistration `json:"u2f_registrations,omitempty"`
SetU2FRegistrations bool `json:"set_u2f_registrations"`
}
const maxU2FRegistrations = 20
// Validate the request.
func (r *UpdateUserRequest) Validate(rctx *RequestContext) error {
if len(r.U2FRegistrations) > maxU2FRegistrations {
if r.SetU2FRegistrations && len(r.U2FRegistrations) > maxU2FRegistrations {
return newValidationError(nil, "u2f_registrations", "too many U2F registrations")
}
// TODO: better validation of the language code!
if len(r.Lang) > 2 {
if r.SetLang && (r.Lang == "" || len(r.Lang) > 2) {
return newValidationError(nil, "lang", "invalid language code")
}
......@@ -427,16 +433,22 @@ func (r *UpdateUserRequest) Validate(rctx *RequestContext) error {
// Serve the request.
func (r *UpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
if r.Lang != "" {
rctx.User.Lang = r.Lang
if r.SetLang {
rctx.User.Lang = strings.ToLower(r.Lang)
}
// TODO: check if setU2FRegistration calls tx.UpdateUser, this is a bug otherwise.
return nil, rctx.User.setU2FRegistrations(rctx.Context, rctx.TX, r.U2FRegistrations)
if r.SetU2FRegistrations {
rctx.User.U2FRegistrations = r.U2FRegistrations
if err := rctx.User.check2FAState(rctx.Context, rctx.TX); err != nil {
return nil, err
}
}
return nil, rctx.TX.UpdateUser(rctx.Context, &rctx.User.User)
}
// AdminUpdateUserRequest is the privileged version of UpdateUser and
// allows to update many more attributes. It is a catch-all function
// allows to update privileged attributes. It is a catch-all function
// for very simple changes that don't justify their own specialized
// method.
type AdminUpdateUserRequest struct {
......
......@@ -166,7 +166,7 @@ func (c *cacheTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.User,
return err
}
func (c *cacheTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, asp *as.AppSpecificPasswordInfo, pw string) error {
func (c *cacheTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, asp *ct.AppSpecificPassword, pw string) error {
err := c.TX.SetApplicationSpecificPassword(ctx, user, asp, pw)
if err == nil {
c.invalidateUser(user.Name)
......
......@@ -110,7 +110,7 @@ func (tx instrumentedTX) SetUserEncryptionPublicKey(ctx context.Context, u *as.U
return tx.TX.SetUserEncryptionPublicKey(ctx, u, k)
}
func (tx instrumentedTX) SetApplicationSpecificPassword(ctx context.Context, u *as.User, a *as.AppSpecificPasswordInfo, s string) error {
func (tx instrumentedTX) SetApplicationSpecificPassword(ctx context.Context, u *as.User, a *ct.AppSpecificPassword, s string) error {
counters.WithLabelValues(tx.name, "SetApplicationSpecificPassword").Inc()
return tx.TX.SetApplicationSpecificPassword(ctx, u, a, s)
}
......
package ldapbackend
import (
"encoding/base64"
as "git.autistici.org/ai3/accountserver"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
)
......@@ -13,15 +10,6 @@ import (
// is an overall better strategy than crashing, but it's a bit gentler
// on the user.
func newAppSpecificPassword(info as.AppSpecificPasswordInfo, pw string) *ct.AppSpecificPassword {
return &ct.AppSpecificPassword{
ID: info.ID,
Service: info.Service,
Comment: info.Comment,
EncryptedPassword: pw,
}
}
func decodeAppSpecificPasswords(values []string) []*ct.AppSpecificPassword {
var out []*ct.AppSpecificPassword
for _, value := range values {
......@@ -50,16 +38,11 @@ func encodeAppSpecificPasswords(asps []*ct.AppSpecificPassword) []string {
return out
}
func getASPInfo(asps []*ct.AppSpecificPassword) []*as.AppSpecificPasswordInfo {
var out []*as.AppSpecificPasswordInfo
for _, asp := range asps {
out = append(out, &as.AppSpecificPasswordInfo{
ID: asp.ID,
Service: asp.Service,
Comment: asp.Comment,
})
func sanitizeAppSpecificPasswords(asps []*ct.AppSpecificPassword) []*ct.AppSpecificPassword {
for i := 0; i < len(asps); i++ {
asps[i].EncryptedPassword = ""
}
return out
return asps
}
func decodeUserEncryptionKeys(values []string) []*ct.EncryptedKey {
......@@ -82,41 +65,20 @@ func encodeUserEncryptionKeys(keys []*ct.EncryptedKey) []string {
return out
}
func decodeU2FRegistrations(encRegs []string) []*as.U2FRegistration {
var out []*as.U2FRegistration
func decodeU2FRegistrations(encRegs []string) []*ct.U2FRegistration {
var out []*ct.U2FRegistration
for _, enc := range encRegs {
if r, err := ct.UnmarshalU2FRegistration(enc); err == nil {
// Convert ct.U2FRegistration (internal) ->
// as.U2FRegistration (public) by
// base64-encoding the data.
out = append(out, &as.U2FRegistration{
KeyHandle: base64.StdEncoding.EncodeToString(r.KeyHandle),
PublicKey: base64.StdEncoding.EncodeToString(r.PublicKey),
})
out = append(out, r)
}
}
return out
}
func encodeU2FRegistrations(regs []*as.U2FRegistration) []string {
func encodeU2FRegistrations(regs []*ct.U2FRegistration) []string {
var out []string
for _, r := range regs {
// Convert as.U2FRegistration (public) ->
// ct.U2FRegistration (internal) by base64-decoding
// the data.
kh, err := base64.StdEncoding.DecodeString(r.KeyHandle)
if err != nil {
continue
}
pk, err := base64.StdEncoding.DecodeString(r.PublicKey)
if err != nil {
continue
}
ctr := ct.U2FRegistration{
KeyHandle: kh,
PublicKey: pk,
}
out = append(out, ctr.Marshal())
out = append(out, r.Marshal())
}
return out
}
......@@ -144,7 +144,7 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)),
AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
AppSpecificPasswords: getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr))),
AppSpecificPasswords: sanitizeAppSpecificPasswords(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr))),
HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "",
},
// Remove the legacy LDAP {crypt} prefix on old passwords.
......@@ -425,10 +425,20 @@ func (tx *backendTX) SetUserEncryptionPublicKey(ctx context.Context, user *as.Us
return nil
}
func (tx *backendTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, info *as.AppSpecificPasswordInfo, encryptedPassword string) error {
func (tx *backendTX) SetApplicationSpecificPassword(ctx context.Context, user *as.User, asp *ct.AppSpecificPassword, encryptedPassword string) error {
dn := tx.getUserDN(user)
asps := decodeAppSpecificPasswords(tx.readAttributeValues(ctx, dn, aspLDAPAttr))
asps = append(excludeASPFromList(asps, info.ID), newAppSpecificPassword(*info, encryptedPassword))
// Build a new AppSpecificPassword that includes the encrypted
// pw, without modifying the original object.
newASP := ct.AppSpecificPassword{
ID: asp.ID,
Service: asp.Service,
Comment: asp.Comment,
EncryptedPassword: encryptedPassword,
}
asps = append(excludeASPFromList(asps, asp.ID), &newASP)
outASPs := encodeAppSpecificPasswords(asps)
tx.setAttr(dn, aspLDAPAttr, outASPs...)
return nil
......
package ldapbackend
import (
"bytes"
"context"
"fmt"
"testing"
......@@ -14,35 +15,37 @@ import (
)
const (
testLDAPPort = 42871
testLDAPAddr = "ldap://127.0.0.1:42871"
testUser1 = "uno@investici.org"
testUser2 = "due@investici.org" // has encryption keys
testUser3 = "tre@investici.org" // has OTP
testUser4 = "quattro@investici.org" // has mailing lists
testBaseDN = "dc=example,dc=com"
testUser1 = "uno@investici.org"
testUser2 = "due@investici.org" // has encryption keys
testUser3 = "tre@investici.org" // has OTP
testUser4 = "quattro@investici.org" // has mailing lists
testBaseDN = "dc=example,dc=com"
)
func startServerAndGetUser(t testing.TB) (func(), as.Backend, *as.RawUser) {
func startServerAndGetUser(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser1)
}
func startServerAndGetUser2(t testing.TB) (func(), as.Backend, *as.RawUser) {
func startServerAndGetUser2(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser2)
}
func startServerAndGetUser3(t testing.TB) (func(), as.Backend, *as.RawUser) {
func startServerAndGetUser3(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser3)
}
func startServerAndGetUser4(t testing.TB) (func(), as.Backend, *as.RawUser) {
func startServerAndGetUser4(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser4)
}
func startServer(t testing.TB) (func(), as.Backend) {
stop := ldaptest.StartServer(t, &ldaptest.Config{
func startServer(t *testing.T) (*ldaptest.TestLDAPServer, as.Backend) {
// Tell the test runtime that we can run multiple integration tests in
// parallel. This just happens to be a convenient call site for all
// integration tests.
t.Parallel()
srv := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../../ldaptest",
Port: testLDAPPort,
Base: "dc=example,dc=com",
LDIFs: []string{
"testdata/base.ldif",
......@@ -53,16 +56,16 @@ func startServer(t testing.TB) (func(), as.Backend) {
},
})
b, err := NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
b, err := NewLDAPBackend(srv.Addr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
if err != nil {
t.Fatal("NewLDAPBackend", err)
}
return stop, b
return srv, b
}
func startServerAndGetUserWithName(t testing.TB, username string) (func(), as.Backend, *as.RawUser) {
stop, b := startServer(t)
func startServerAndGetUserWithName(t *testing.T, username string) (*ldaptest.TestLDAPServer, as.Backend, *as.RawUser) {
srv, b := startServer(t)
tx, _ := b.NewTransaction()
user, err := tx.GetUser(context.Background(), username)
......@@ -73,12 +76,12 @@ func startServerAndGetUserWithName(t testing.TB, username string) (func(), as.Ba
t.Fatalf("could not find test user %s", username)
}
return stop, b, user
return srv, b, user
}
func TestModel_GetUser_NotFound(t *testing.T) {
stop, b := startServer(t)
defer stop()
srv, b := startServer(t)
defer srv.Close()
tx, _ := b.NewTransaction()
user, err := tx.GetUser(context.Background(), "wrong_user")
......@@ -91,8 +94,8 @@ func TestModel_GetUser_NotFound(t *testing.T) {
}
func TestModel_GetUser(t *testing.T) {
stop, _, user := startServerAndGetUser(t)
defer stop()
srv, _, user := startServerAndGetUser(t)
defer srv.Close()
if user.Name != testUser1 {
t.Errorf("bad username: expected %s, got %s", testUser1, user.Name)
......@@ -126,8 +129,8 @@ func TestModel_GetUser(t *testing.T) {
}
func TestModel_GetUser_HasEncryptionKeys(t *testing.T) {
stop, _, user := startServerAndGetUser2(t)
defer stop()
srv, _, user := startServerAndGetUser2(t)
defer srv.Close()
if !user.HasEncryptionKeys {
t.Errorf("user %s does not appear to have encryption keys", user.Name)
......@@ -135,17 +138,35 @@ func TestModel_GetUser_HasEncryptionKeys(t *testing.T) {
}
func TestModel_GetUser_Has2FA(t *testing.T) {
stop, _, user := startServerAndGetUser3(t)
defer stop()
srv, _, user := startServerAndGetUser3(t)
defer srv.Close()
if !user.Has2FA {
t.Errorf("user %s does not appear to have 2FA enabled", user.Name)
}
}
func TestModel_GetUser_HasU2FRegistrations(t *testing.T) {
srv, _, user := startServerAndGetUser4(t)
defer srv.Close()
if n := len(user.U2FRegistrations); n != 2 {
t.Errorf("user %s has %d u2f registrations, expected 2", user.Name, n)
}
expectedKey := []byte{
164, 1, 2, 3, 38, 33, 88, 32, 182, 233, 26, 63, 41, 208, 70, 136, 89, 102, 192, 232, 56, 134, 225, 180, 18,
196, 51, 198, 91, 162, 121, 83, 86, 85, 224, 46, 64, 151, 99, 8, 34, 88, 32, 34, 176, 200, 116, 202, 44, 231,
42, 170, 189, 102, 70, 10, 9, 116, 206, 125, 3, 130, 59, 200, 44, 245, 249, 90, 172, 181, 184, 201, 81, 174, 182,
}
if !bytes.Equal(user.U2FRegistrations[0].PublicKey, expectedKey) {
t.Errorf("user %s has wrong public key for u2f registration: %v", user.Name, user.U2FRegistrations[0].PublicKey)
}
}
func TestModel_GetUser_Resources(t *testing.T) {
stop, b, user := startServerAndGetUser(t)
defer stop()
srv, b, user := startServerAndGetUser(t)
defer srv.Close()
// Ensure that the user *has* resources.
if len(user.Resources) < 1 {
......@@ -176,8 +197,8 @@ func TestModel_GetUser_Resources(t *testing.T) {
}
func TestModel_GetUser_MailingListsAndNewsletters(t *testing.T) {
stop, _, user := startServerAndGetUser4(t)
defer stop()
srv, _, user := startServerAndGetUser4(t)
defer srv.Close()
// Ensure that the user has the expected number of list resources.
// The backend should find two lists, one of which has an alias as the owner.
......@@ -194,8 +215,8 @@ func TestModel_GetUser_MailingListsAndNewsletters(t *testing.T) {
}
func TestModel_SearchUser(t *testing.T) {
stop, b := startServer(t)
defer stop()
srv, b := startServer(t)
defer srv.Close()
tx, _ := b.NewTransaction()
users, err := tx.SearchUser(context.Background(), "uno", 0)
if err != nil {
......@@ -210,15 +231,16 @@ func TestModel_SearchUser(t *testing.T) {
}
func TestModel_SetResourceStatus(t *testing.T) {
stop := ldaptest.StartServer(t, &ldaptest.Config{
t.Parallel()
srv := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../../ldaptest",
Port: testLDAPPort,
Base: "dc=example,dc=com",
LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
})
defer stop()
defer srv.Close()
b, err := NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
b, err := NewLDAPBackend(srv.Addr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
if err != nil {
t.Fatal("NewLDAPBackend", err)
}
......@@ -243,15 +265,16 @@ func TestModel_SetResourceStatus(t *testing.T) {
}
func TestModel_HasAnyResource(t *testing.T) {
stop := ldaptest.StartServer(t, &ldaptest.Config{
t.Parallel()
srv := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../../ldaptest",
Port: testLDAPPort,
Base: "dc=example,dc=com",
LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
})
defer stop()
defer srv.Close()
b, err := NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
b, err := NewLDAPBackend(srv.Addr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
if err != nil {
t.Fatal("NewLDAPBackend", err)
}
......@@ -283,8 +306,8 @@ func TestModel_HasAnyResource(t *testing.T) {
}
func TestModel_SearchResource(t *testing.T) {
stop, b := startServer(t)
defer stop()
srv, b := startServer(t)
defer srv.Close()
for _, pattern := range []string{"uno@investici.org", "uno*"} {
tx, _ := b.NewTransaction()
......@@ -302,8 +325,8 @@ func TestModel_SearchResource(t *testing.T) {
}
func TestModel_SetUserPassword(t *testing.T) {
stop, b, user := startServerAndGetUser(t)
defer stop()
srv, b, user := startServerAndGetUser(t)
defer srv.Close()
encPass := "encrypted password"
......@@ -333,8 +356,8 @@ func TestModel_SetUserPassword(t *testing.T) {
}
func TestModel_SetUserEncryptionKeys_Add(t *testing.T) {
stop, b, user := startServerAndGetUser(t)
defer stop()
srv, b, user := startServerAndGetUser(t)
defer srv.Close()
tx, _ := b.NewTransaction()
keys := []*ct.EncryptedKey{
......@@ -352,8 +375,8 @@ func TestModel_SetUserEncryptionKeys_Add(t *testing.T) {
}
func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) {
stop, b, user := startServerAndGetUser2(t)
defer stop()
srv, b, user := startServerAndGetUser2(t)
defer srv.Close()
tx, _ := b.NewTransaction()
keys := []*ct.EncryptedKey{
......@@ -371,8 +394,8 @@ func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) {
}
func TestModel_NextUID(t *testing.T) {
stop, b, user := startServerAndGetUser(t)
defer stop()
srv, b, user := startServerAndGetUser(t)
defer srv.Close()
tx, _ := b.NewTransaction()
// User UID should not be available.
......@@ -408,6 +431,8 @@ func TestModel_NextUID(t *testing.T) {
}
func TestSortResources(t *testing.T) {
t.Parallel()
rsrcs := []*as.Resource{
&as.Resource{
ID: "id1",
......@@ -437,6 +462,8 @@ func TestSortResources(t *testing.T) {
}
func TestSortResources_ExternalParentID(t *testing.T) {
t.Parallel()
rsrcs := []*as.Resource{
&as.Resource{
ID: "id1",
......
......@@ -100,7 +100,8 @@ func setCommonResourceAttrs(entry *ldap.Entry, rsrc *as.Resource) {
func (reg *resourceRegistry) FromLDAP(entry *ldap.Entry) (rsrc *as.Resource, err error) {
// Since we don't know what resource type to expect, we try
// all known handlers until one returns a valid Resource.
// This is slightly dangerous unless all
// This expects that all object types can be told apart by
// their DN or attributes.
for _, h := range reg.handlers {
rsrc, err = h.FromLDAP(entry)
if err == nil {
......