Commit 4e34034b authored by ale's avatar ale

First stage of refactor targeting simplicity

Structure flow around requests themselves and composition rather than
handlers and wrappers, the results are likely more readable (and
shorter).

Move all the user auth management business logic to a smart RawUser
object, to separate it from details of API handling. The result should
be more understandable: all critical changes are contained within a
single type.

Also, with all the workflow driven by Requests, we can get rid of the
boilerplate in the HTTP API server and replace it with a tiny tiny
layer of reflection.
parent 6f16cef4
This diff is collapsed.
package accountserver
import (
"context"
"errors"
"log"
"git.autistici.org/ai3/go-common/pwhash"
)
// CreateResourcesRequest lets administrators create one or more resources.
type CreateResourcesRequest struct {
AdminRequestBase
Resources []*Resource `json:"resources"`
}
// CreateResourcesResponse is the response type for CreateResourcesRequest.
type CreateResourcesResponse struct {
// Resources to create. All must either be global resources
// (no user ownership), or belong to the same user.
Resources []*Resource `json:"resources"`
}
func (r *CreateResourcesRequest) getOwner(rctx *RequestContext) (*RawUser, error) {
// Fetch the user associated with the first resource (if
// any). Since resource validation might reference other
// resources, we need to provide it with a view of what the
// future resources will be. So we merge the resources from
// the database with those from the request, using a local
// copy of the User object.
if len(r.Resources) > 0 {
if owner := r.Resources[0].ID.User(); owner != "" {
u, err := getUserOrDie(rctx.Context, rctx.TX, owner)
if err != nil {
return nil, err
}
user := *u
user.Resources = mergeResources(u.Resources, r.Resources)
return &user, nil
}
}
return nil, nil
}
// Validate the request.
func (r *CreateResourcesRequest) Validate(rctx *RequestContext) error {
var owner string
user, err := r.getOwner(rctx)
if err != nil {
return err
}
var tplUser *User
if user != nil {
owner = user.Name
tplUser = &user.User
}
for _, rsrc := range r.Resources {
rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, tplUser)
// Check same-user ownership.
if rsrc.ID.User() != owner {
return errors.New("resources owned by different users")
}
// Validate the resource.
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, tplUser); err != nil {
log.Printf("validation error while creating resource %+v: %v", rsrc, err)
return err
}
}
return nil
}
// Serve the request.
func (r *CreateResourcesRequest) Serve(rctx *RequestContext) (interface{}, error) {
var resp CreateResourcesResponse
for _, rsrc := range r.Resources {
if err := rctx.TX.CreateResource(rctx.Context, rsrc); err != nil {
return nil, err
}
//s.audit.Log(ctx, r.ID, "resource created")
resp.Resources = append(resp.Resources, rsrc)
}
return &resp, nil
}
// Merge two resource lists by ID (the second one wins), return a new list.
func mergeResources(a, b []*Resource) []*Resource {
tmp := make(map[string]*Resource)
for _, l := range [][]*Resource{a, b} {
for _, r := range l {
tmp[r.ID.String()] = r
}
}
out := make([]*Resource, 0, len(tmp))
for _, r := range tmp {
out = append(out, r)
}
return out
}
// CreateUserRequest lets administrators create a new user along with the
// associated resources.
type CreateUserRequest struct {
AdminRequestBase
User *User `json:"user"`
}
// applyTemplate fills in default values for the resources in the request.
func (r *CreateUserRequest) applyTemplate(rctx *RequestContext) error {
// Some fields should be always unset because there are
// specific methods to modify them.
r.User.Has2FA = false
r.User.HasOTP = false
r.User.HasEncryptionKeys = true // set to true so that resetPassword will create keys.
r.User.PasswordRecoveryHint = ""
r.User.AppSpecificPasswords = nil
r.User.U2FRegistrations = nil
if r.User.Lang == "" {
r.User.Lang = "en"
}
// Allocate a new user ID.
uid, err := rctx.TX.NextUID(rctx.Context)
if err != nil {
return err
}
r.User.UID = uid
// Apply templates to all resources in the request.
for _, rsrc := range r.User.Resources {
rctx.resourceTemplates.applyTemplate(rctx.Context, rsrc, r.User)
}
return nil
}
// Validate the request.
func (r *CreateUserRequest) Validate(rctx *RequestContext) error {
if err := r.applyTemplate(rctx); err != nil {
return err
}
// Validate the user *and* all resources.
if err := rctx.userValidator(rctx.Context, r.User); err != nil {
log.Printf("validation error while creating user %+v: %v", r.User, err)
return err
}
for _, rsrc := range r.User.Resources {
if err := rctx.resourceValidator.validateResource(rctx.Context, rsrc, r.User); err != nil {
log.Printf("validation error while creating resource %+v: %v", rsrc, err)
return err
}
}
return nil
}
// CreateUserResponse is the response type for CreateUserRequest.
type CreateUserResponse struct {
User *User `json:"user,omitempty"`
Password string `json:"password"`
}
// Serve the request
func (r *CreateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
var resp CreateUserResponse
// Create the user first, along with all the resources.
if err := rctx.TX.CreateUser(rctx.Context, r.User); err != nil {
return nil, err
}
resp.User = r.User
// Now set a password for the user and return it, and
// set random passwords for all the resources
// (currently, we don't care about those, the user
// will reset them later). However, we could return
// them in the response as well, if necessary.
u := &RawUser{User: *r.User}
newPassword := randomPassword()
if err := u.resetPassword(rctx.Context, rctx.TX, newPassword); err != nil {
return nil, err
}
resp.Password = newPassword
//s.audit.Log(ctx, ResourceID{}, "user created")
for _, rsrc := range r.User.Resources {
//rctx.audit.Log(ctx, r.ID, "resource created")
if resourceHasPassword(rsrc) {
if _, err := doResetResourcePassword(rctx.Context, rctx.TX, rsrc); err != nil {
// Just log, don't fail.
log.Printf("can't set random password for resource %s: %v", rsrc.ID, err)
}
}
}
return &resp, nil
}
func doResetResourcePassword(ctx context.Context, tx TX, rsrc *Resource) (string, error) {
newPassword := randomPassword()
encPassword := pwhash.Encrypt(newPassword)
// TODO: this needs a resource type-switch.
if err := tx.SetResourcePassword(ctx, rsrc, encPassword); err != nil {
return "", err
}
return newPassword, nil
}
package accountserver
import (
"errors"
"fmt"
)
// setResourceStatus sets the status of a single resource (shared
// logic between enable / disable resource methods).
func setResourceStatus(rctx *RequestContext, status string) error {
rsrc := rctx.Resource
rsrc.Status = status
if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
return err
}
rctx.audit.Log(rctx, rsrc.ID, fmt.Sprintf("status set to %s", status))
return nil
}
// DisableResourceRequest disables a resource belonging to the user.
type DisableResourceRequest struct {
ResourceRequestBase
}
// Serve the request.
func (r *DisableResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
return nil, setResourceStatus(rctx, ResourceStatusInactive)
}
// EnableResourceRequest enables a resource belonging to the user (admin-only).
type EnableResourceRequest struct {
AdminResourceRequestBase
}
// Serve the request.
func (r *EnableResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
return nil, setResourceStatus(rctx, ResourceStatusActive)
}
// ResetResourcePasswordRequest will reset the password associated
// with a resource (if the resource type supports it). It will
// generate a random password and return it to the caller.
type ResetResourcePasswordRequest struct {
ResourceRequestBase
}
// ResetResourcePasswordResponse is the response type for
// ResetResourcePasswordRequest.
type ResetResourcePasswordResponse struct {
Password string `json:"password"`
}
func resourceHasPassword(r *Resource) bool {
switch r.ID.Type() {
case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
return true
default:
return false
}
}
// Validate the request.
func (r *ResetResourcePasswordRequest) Validate(_ *RequestContext) error {
switch r.ResourceID.Type() {
case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList:
case ResourceTypeEmail:
return errors.New("can't reset email passwords with this API")
default:
return errors.New("can't reset password on this resource type")
}
return nil
}
// Serve the request.
func (r *ResetResourcePasswordRequest) Serve(rctx *RequestContext) (interface{}, error) {
// TODO: this needs a resource-type switch, because in some
// cases we may want to call out to other backends in order to
// reset credentials for certain resources that have their own
// secondary authentication databases (lists, mysql).
password, err := doResetResourcePassword(rctx.Context, rctx.TX, rctx.Resource)
if err != nil {
return nil, err
}
return &ResetResourcePasswordResponse{
Password: password,
}, nil
}
// MoveResourceRequest is an administrative operation to move resources
// between shards. Resources that are part of a group are moved all at
// once regardless of which individual ResourceID is provided as long
// as it belongs to the group.
type MoveResourceRequest struct {
AdminResourceRequestBase
Shard string `json:"shard"`
}
// Validate the request.
func (r *MoveResourceRequest) Validate(rctx *RequestContext) error {
// TODO: check shard
return nil
}
// MoveResourceResponse is the response type for MoveResourceRequest.
type MoveResourceResponse struct {
MovedIDs []string `json:"moved_ids"`
}
// Serve the request.
func (r *MoveResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
resources := []*Resource{rctx.Resource}
// If we have an associated user, collect all related
// resources, as they should all be moved at once.
if rctx.User != nil && rctx.Resource.Group != "" {
resources = append(resources, rctx.User.GetResourcesByGroup(rctx.Resource.Group)...)
}
var resp MoveResourceResponse
for _, rsrc := range resources {
rsrc.Shard = r.Shard
if err := rctx.TX.UpdateResource(rctx.Context, rsrc); err != nil {
return nil, err
}
resp.MovedIDs = append(resp.MovedIDs, rsrc.ID.String())
}
return &resp, nil
}
// AddEmailAliasRequest adds an alias (additional address) to an email resource.
type AddEmailAliasRequest struct {
ResourceRequestBase
Addr string `json:"addr"`
}
// Validate the request.
func (r *AddEmailAliasRequest) Validate(rctx *RequestContext) error {
if err := rctx.fieldValidators.email(rctx.Context, r.Addr); err != nil {
return newValidationError(nil, "addr", err.Error())
}
return nil
}
const maxEmailAliases = 5
// Serve the request.
func (r *AddEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error) {
if rctx.Resource.ID.Type() != ResourceTypeEmail {
return nil, errors.New("this operation only works on email resources")
}
// Allow at most 5 aliases.
if len(rctx.Resource.Email.Aliases) >= maxEmailAliases {
return nil, errors.New("too many aliases")
}
rctx.Resource.Email.Aliases = append(rctx.Resource.Email.Aliases, r.Addr)
if err := rctx.TX.UpdateResource(rctx.Context, rctx.Resource); err != nil {
return nil, err
}
rctx.audit.Log(rctx, r.ResourceID, fmt.Sprintf("added alias %s", r.Addr))
return nil, nil
}
// DeleteEmailAliasRequest removes an alias from an email resource.
type DeleteEmailAliasRequest struct {
ResourceRequestBase
Addr string `json:"addr"`
}
// Serve the request.
func (r *DeleteEmailAliasRequest) Serve(rctx *RequestContext) (interface{}, error) {
if rctx.Resource.ID.Type() != ResourceTypeEmail {
return nil, errors.New("this operation only works on email resources")
}
var aliases []string
for _, a := range rctx.Resource.Email.Aliases {
if a != r.Addr {
aliases = append(aliases, a)
}
}
rctx.Resource.Email.Aliases = aliases
if err := rctx.TX.UpdateResource(rctx.Context, rctx.Resource); err != nil {
return nil, err
}
rctx.audit.Log(rctx, r.ResourceID, fmt.Sprintf("removed alias %s", r.Addr))
return nil, nil
}
This diff is collapsed.
This diff is collapsed.
package accountserver package accountserver
import ( import (
"context"
"encoding/json" "encoding/json"
"log" "log"
) )
type auditLogger interface { type auditLogger interface {
Log(context.Context, ResourceID, string) Log(*RequestContext, ResourceID, string)
} }
type auditLogEntry struct { type auditLogEntry struct {
...@@ -21,18 +20,27 @@ type auditLogEntry struct { ...@@ -21,18 +20,27 @@ type auditLogEntry struct {
type syslogAuditLogger struct{} type syslogAuditLogger struct{}
func (l *syslogAuditLogger) Log(ctx context.Context, resourceID ResourceID, what string) { func (l *syslogAuditLogger) Log(rctx *RequestContext, rid ResourceID, what string) {
e := auditLogEntry{ e := auditLogEntry{
User: userFromContext(ctx),
By: authUserFromContext(ctx),
Message: what, Message: what,
Comment: commentFromContext(ctx), Comment: rctx.Comment,
}
if rctx.SSO != nil {
e.By = rctx.SSO.User
}
if rctx.User != nil {
e.User = rctx.User.Name
} }
if !resourceID.Empty() { // Fall back to resource from context if unspecified.
e.ResourceName = resourceID.Name() if rid.Empty() && rctx.Resource != nil {
e.ResourceType = resourceID.Type() rid = rctx.Resource.ID
if u := resourceID.User(); u != "" { }
if !rid.Empty() {
e.ResourceName = rid.Name()
e.ResourceType = rid.Type()
// TODO: redundant?
if u := rid.User(); u != "" {
e.User = u e.User = u
} }
} }
......
...@@ -112,7 +112,7 @@ func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) { ...@@ -112,7 +112,7 @@ func newLDAPBackendWithConn(conn ldapConn, base string) (*backend, error) {
}, nil }, nil
} }
func newUser(entry *ldap.Entry) (*as.User, error) { func newUser(entry *ldap.Entry) (*as.RawUser, error) {
// Note that some user-level attributes related to // Note that some user-level attributes related to
// authentication are stored on the uid= object, while others // authentication are stored on the uid= object, while others
// are on the email= object. We set the latter in the GetUser // are on the email= object. We set the latter in the GetUser
...@@ -122,13 +122,18 @@ func newUser(entry *ldap.Entry) (*as.User, error) { ...@@ -122,13 +122,18 @@ func newUser(entry *ldap.Entry) (*as.User, error) {
// the current schema has those on email=, but we'd like to // the current schema has those on email=, but we'd like to
// move them to uid=, so we currently have to support both. // move them to uid=, so we currently have to support both.
uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
user := &as.User{ user := &as.RawUser{
Name: entry.GetAttributeValue("uid"), User: as.User{
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr), Name: entry.GetAttributeValue("uid"),
UID: uidNumber, Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
PasswordRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr), UID: uidNumber,
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)), PasswordRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "", U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "",
},
// Remove the legacy LDAP {crypt} prefix on old passwords.
Password: strings.TrimPrefix(entry.GetAttributeValue(passwordLDAPAttr), "{crypt}"),
RecoveryPassword: strings.TrimPrefix(entry.GetAttributeValue(recoveryResponseLDAPAttr), "{crypt}"),
} }
// The user has 2FA enabled if it has a TOTP secret or U2F keys. // The user has 2FA enabled if it has a TOTP secret or U2F keys.
...@@ -226,7 +231,7 @@ func (tx *backendTX) UpdateUser(ctx context.Context, user *as.User) error { ...@@ -226,7 +231,7 @@ func (tx *backendTX) UpdateUser(ctx context.Context, user *as.User) error {
} }
// GetUser returns a user. // GetUser returns a user.
func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.User, error) { func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser, error) {
// First of all, find the main user object, and just that one. // First of all, find the main user object, and just that one.
vars := map[string]string{"user": username} vars := map[string]string{"user": username}
result, err := tx.search(ctx, tx.backend.userQuery.query(vars)) result, err := tx.search(ctx, tx.backend.userQuery.query(vars))
...@@ -264,6 +269,8 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.User, er ...@@ -264,6 +269,8 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.User, er
user.PasswordRecoveryHint = s user.PasswordRecoveryHint = s
} }
user.AppSpecificPasswords = getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr))) user.AppSpecificPasswords = getASPInfo(decodeAppSpecificPasswords(entry.GetAttributeValues(aspLDAPAttr)))
user.Keys = decodeUserEncryptionKeys(
entry.GetAttributeValues(storagePrivateKeyLDAPAttr))
user.HasEncryptionKeys = (entry.GetAttributeValue(storagePublicKeyLDAPAttr) != "") user.HasEncryptionKeys = (entry.GetAttributeValue(storagePublicKeyLDAPAttr) != "")
} }
...@@ -290,24 +297,6 @@ func (tx *backendTX) SetUserPassword(ctx context.Context, user *as.User, encrypt ...@@ -290,24 +297,6 @@ func (tx *backendTX) SetUserPassword(ctx context.Context, user *as.User, encrypt
return return
} }
func (tx *backendTX) GetUserEncryptedPassword(ctx context.Context, user *as.User) string {
values := tx.readAttributeValues(ctx, tx.getUserDN(user), passwordLDAPAttr)
if len(values) > 0 {
// Remove legacy LDAP encryption prefix.
return strings.TrimPrefix(values[0], "{crypt}")
}
return ""
}
func (tx *backendTX) GetUserRecoveryEncryptedPassword(ctx context.Context, user *as.User) string {
values := tx.readAttributeValues(ctx, tx.getUserDN(user), recoveryResponseLDAPAttr)
if len(values) > 0 {
// Remove legacy LDAP encryption prefix.
return strings.TrimPrefix(values[0], "{crypt}")
}
return ""
}
func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User, hint, response string) error { func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User, hint, response string) error {
// Write the password recovery attributes on the uid= object, // Write the password recovery attributes on the uid= object,
// as per the new schema. // as per the new schema.
...@@ -317,14 +306,13 @@ func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User, ...@@ -317,14 +306,13 @@ func (tx *backendTX) SetPasswordRecoveryHint(ctx context.Context, user *as.User,
return nil return nil
} }
func (tx *backendTX) GetUserEncryptionKeys(ctx context.Context, user *as.User) ([]*as.UserEncryptionKey, error) { func (tx *backendTX) DeletePasswordRecoveryHint(ctx context.Context, user *as.User) error {
r := user.GetSingleResourceByType(as.ResourceTypeEmail) // Write the password recovery attributes on the uid= object,
dn, err := tx.backend.resources.GetDN(r.ID) // as per the new schema.
if err != nil { dn := tx.getUserDN(user)
return nil, err tx.setAttr(dn, recoveryHintLDAPAttr)
} tx.setAttr(dn, recoveryResponseLDAPAttr)
rawKeys := tx.readAttributeValues(ctx, dn, storagePrivateKeyLDAPAttr) return nil
return decodeUserEncryptionKeys(rawKeys), nil
} }
func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *as.User, keys []*as.UserEncryptionKey) error { func (tx *backendTX) SetUserEncryptionKeys(ctx context.Context, user *as.User, keys []*as.UserEncryptionKey) error {
......
...@@ -17,11 +17,11 @@ const ( ...@@ -17,11 +17,11 @@ const (
testUser2 = "due@investici.org" testUser2 = "due@investici.org"
) )
func startServerAndGetUser(t testing.TB) (func(), as.Backend, *as.User) { func startServerAndGetUser(t testing.TB) (func(), as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser1) return startServerAndGetUserWithName(t, testUser1)
} }
func startServerAndGetUser2(t testing.TB) (func(), as.Backend, *as.User) { func startServerAndGetUser2(t testing.TB) (func(), as.Backend, *as.RawUser) {
return startServerAndGetUserWithName(t, testUser2) return startServerAndGetUserWithName(t, testUser2)
} }
...@@ -45,7 +45,7 @@ func startServer(t testing.TB) (func(), as.Backend) { ...@@ -45,7 +45,7 @@ func startServer(t testing.TB) (func(), as.Backend) {
return stop, b return stop, b
} }
func startServerAndGetUserWithName(t testing.TB, username string) (func(), as.Backend, *as.User) { func startServerAndGetUserWithName(t testing.TB, username string) (func(), as.Backend, *as.RawUser) {
stop, b := startServer(t) stop, b := startServer(t)
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
...@@ -233,7 +233,7 @@ func TestModel_SetUserPassword(t *testing.T) { ...@@ -233,7 +233,7 @@ func TestModel_SetUserPassword(t *testing.T) {
encPass := "encrypted password" encPass := "encrypted password"
tx, _ := b.NewTransaction() tx, _ := b.NewTransaction()
if err := tx.SetUserPassword(context.Background(), user, encPass); err != nil { if err := tx.SetUserPassword(context.Background(), &user.User, encPass); err != nil {
t.Fatal("SetUserPassword", err) t.Fatal("SetUserPassword", err)
} }
if err := tx.Commit(context.Background()); err != nil { if err := tx.Commit(context.Background()); err != nil {
...@@ -268,7 +268,7 @@ func TestModel_SetUserEncryptionKeys_Add(t *testing.T) { ...@@ -268,7 +268,7 @@ func TestModel_SetUserEncryptionKeys_Add(t *testing.T) {
Key: []byte("very secret key"), Key: []byte("very secret key"),
}, },
} }
if err := tx.SetUserEncryptionKeys(context.Background(), user, keys); err != nil { if err := tx.SetUserEncryptionKeys(context.Background(), &user.User, keys); err != nil {
t.Fatal("SetUserEncryptionKeys", err) t.Fatal("SetUserEncryptionKeys", err)
} }
if err := tx.Commit(context.Background()); err != nil { if err := tx.Commit(context.Background()); err != nil {
...@@ -287,7 +287,7 @@ func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) { ...@@ -287,7 +287,7 @@ func TestModel_SetUserEncryptionKeys_Replace(t *testing.T) {
Key: []byte("very secret key"), Key: []byte("very secret key"),
}, },
} }
if err := tx.SetUserEncryptionKeys(context.Background(), user, keys); err != nil { if err := tx.SetUserEncryptionKeys(context.Background(), &user.User, keys); err != nil {
t.Fatal("SetUserEncryptionKeys", err) t.Fatal("SetUserEncryptionKeys", err)
} }
if err := tx.Commit(context.Background()); err != nil { if err := tx.Commit(context.Background()); err != nil {
......
...@@ -18,9 +18,8 @@ givenName: Private ...@@ -18,9 +18,8 @@ givenName: Private
shadowLastChange: 12345 shadowLastChange: 12345
shadowWarning: 7 shadowWarning: 7
preferredLanguage: it preferredLanguage: it
userPassword:: e2NyeXB0fSQ2JG1wVzdTZHZROG52OFJabE8kcFNqbm1hLi9CZDNIWU1hL29sMGZ userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
FRmZrS3pWRjAxUkkzMnpISEoxNnc2V2xaajFuSFVzMmd4ZXZIVDdoemdELnJ5Lk1BZWM3REptZVZ5 TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
WEtkeDV0QTE=
dn: mail=uno@investici.org,uid=uno@investici.org,ou=People,dc=example,dc=com dn: mail=uno@investici.org,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active status: active
...@@ -28,9 +27,8 @@ recoverQuestion:: dGkgc2VpIG1haSDDuMOgdHRvIG1hbGUgY2FkZW5kbyBkYSB1biBwYWxhenpv ...@@ -28,9 +27,8 @@ recoverQuestion:: dGkgc2VpIG1haSDDuMOgdHRvIG1hbGUgY2FkZW5kbyBkYSB1biBwYWxhenpv
IGRpIG90dG8gcGlhbmk/ IGRpIG90dG8gcGlhbmk/
objectClass: top objectClass: top
objectClass: virtualMailUser objectClass: virtualMailUser
userPassword:: e2NyeXB0fSQ2JG1wVzdTZHZROG52OFJabE8kcFNqbm1hLi9CZDNIWU1hL29sMGZ userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
FRmZrS3pWRjAxUkkzMnpISEoxNnc2V2xaajFuSFVzMmd4ZXZIVDdoemdELnJ5Lk1BZWM3REptZVZ5 TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
WEtkeDV0QTE=
uidNumber: 19475 uidNumber: 19475
host: host2 host: host2
mailAlternateAddress: uno@anche.no mailAlternateAddress: uno@anche.no
...@@ -66,9 +64,8 @@ uid: uno ...@@ -66,9 +64,8 @@ uid: uno
creationDate: 01-08-2013 creationDate: 01-08-2013
shadowLastChange: 12345 shadowLastChange: 12345
originalHost: host2 originalHost: host2
userPassword:: e2NyeXB0fSQ2JElDYkx1WTI3QWl6bC5FeEgkUDhOZHJ3VEtxZ2UwQUp3QW9oNE1 userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
EYlUxU3EySGtuRkF1cEx2RUI0U28waEw5NWtpZ3dIeXQuQnYxS0J5SFM2MXd6RnZuLnJsMEN4eFpx TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
RVgzUnVxbDE=
dn: alias=uno,uid=uno@investici.org,ou=People,dc=example,dc=com dn: alias=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
status: active status: active
......
...@@ -42,7 +42,7 @@ type ldapTX struct { ...@@ -42,7 +42,7 @@ type ldapTX struct {
conn ldapConn conn ldapConn
cache map[string][]string cache map[string][]string
newDNs map[string]struct{} newDNs map[string]struct{} // nolint (it's plural DN, not DNS)
changes []ldapAttr changes []ldapAttr
} }
......
...@@ -115,7 +115,7 @@ func main() { ...@@ -115,7 +115,7 @@ func main() {
as := server.New(service, be) as := server.New(service, be)
if err := serverutil.Serve(as.Handler(), config.ServerConfig, *addr); err != nil { if err := serverutil.Serve(as, config.ServerConfig, *addr); err != nil {