Skip to content
Snippets Groups Projects
Select Git revision
1 result Searching

udp.go

Blame
  • actions_test.go 23.45 KiB
    package accountserver
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"path/filepath"
    	"strings"
    	"testing"
    
    	ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
    	"git.autistici.org/ai3/go-common/pwhash"
    	"git.autistici.org/id/auth"
    	sso "git.autistici.org/id/go-sso"
    )
    
    const testUser = "testuser@example.com"
    
    type fakeBackend struct {
    	users                map[string]*User
    	resources            map[string]*Resource
    	passwords            map[string]string
    	recoveryPasswords    map[string]string
    	resourcePasswords    map[string]string
    	appSpecificPasswords map[string][]*ct.AppSpecificPassword
    	encryptionKeys       map[string][]*ct.EncryptedKey
    }
    
    func (b *fakeBackend) NewTransaction() (TX, error) {
    	return b, nil
    }
    
    func (b *fakeBackend) Commit(_ context.Context) error {
    	return nil
    }
    
    func (b *fakeBackend) NextUID(_ context.Context) (int, error) {
    	return 42, nil
    }
    
    func (b *fakeBackend) CanAccessResource(_ context.Context, username string, rsrc *Resource) bool {
    	owner := strings.Split(string(rsrc.ID), "/")[0]
    	return owner == username
    }
    
    func (b *fakeBackend) GetUser(_ context.Context, username string) (*RawUser, error) {
    	u, ok := b.users[username]
    	if !ok {
    		return nil, errors.New("user not found in fake backend")
    	}
    	return &RawUser{
    		User:             *u,
    		Password:         b.passwords[username],
    		RecoveryPassword: b.recoveryPasswords[username],
    		Keys:             b.encryptionKeys[username],
    	}, nil
    }
    
    func (b *fakeBackend) SearchUser(_ context.Context, pattern string, limit int) ([]string, error) {
    	var out []string
    	for username := range b.users {
    		if strings.HasPrefix(username, pattern) {
    			out = append(out, username)
    		}
    	}
    	return out, nil
    }
    
    func (b *fakeBackend) UpdateUser(_ context.Context, user *User) error {
    	b.users[user.Name] = user
    	return nil
    }
    
    func (b *fakeBackend) CreateUser(_ context.Context, user *User) (*User, error) {
    	for _, r := range user.Resources {
    		r.ID = makeResourceID(user.Name, r.Type, r.Name)
    	}
    	b.users[user.Name] = user
    	return user, nil
    }
    
    func (b *fakeBackend) GetResource(_ context.Context, resourceID ResourceID) (*RawResource, error) {
    	owner := strings.Split(resourceID.String(), "/")[0]
    	r := b.resources[resourceID.String()]
    	return &RawResource{Resource: *r, Owner: owner}, nil
    }
    
    func (b *fakeBackend) FindResource(_ context.Context, req FindResourceRequest) (*RawResource, error) {
    	for id, r := range b.resources {
    		if r.Type == req.Type && r.Name == req.Name {
    			owner := strings.Split(id, "/")[0]
    			return &RawResource{Resource: *r, Owner: owner}, nil
    		}
    	}
    	return nil, nil
    }
    
    func (b *fakeBackend) SearchResource(_ context.Context, pattern string, limit int) ([]*RawResource, error) {
    	var out []*RawResource
    	for id, r := range b.resources {
    		owner := strings.Split(id, "/")[0]
    		// Emulate LDAP wildcard syntax.
    		if ok, _ := filepath.Match(pattern, r.Name); ok {
    			out = append(out, &RawResource{Resource: *r, Owner: owner})
    		}
    	}
    	return out, nil
    }
    
    func (b *fakeBackend) UpdateResource(_ context.Context, r *Resource) error {
    	b.resources[r.ID.String()] = r
    	return nil
    }
    
    func makeResourceID(owner, rtype, rname string) ResourceID {
    	return ResourceID(fmt.Sprintf("%s/%s/%s", owner, rtype, rname))
    }
    
    func (b *fakeBackend) CreateResources(_ context.Context, u *User, rsrcs []*Resource) ([]*Resource, error) {
    	var out []*Resource
    	for _, r := range rsrcs {
    		if !r.ID.Empty() {
    			return nil, errors.New("resource ID not empty")
    		}
    
    		var username string
    		if u != nil {
    			username = u.Name
    		}
    		r.ID = makeResourceID(username, r.Type, r.Name)
    
    		if _, ok := b.resources[r.ID.String()]; ok {
    			return nil, errors.New("resource already exists")
    		}
    
    		b.resources[r.ID.String()] = r
    		out = append(out, r)
    	}
    	return out, nil
    }
    
    func (b *fakeBackend) SetUserPassword(_ context.Context, user *User, password string) error {
    	b.passwords[user.Name] = password
    	return nil
    }
    
    func (b *fakeBackend) SetAccountRecoveryHint(_ context.Context, user *User, hint, response string) error {
    	b.users[user.Name].AccountRecoveryHint = hint
    	b.recoveryPasswords[user.Name] = response
    	return nil
    }
    
    func (b *fakeBackend) DeleteAccountRecoveryHint(_ context.Context, user *User) error {
    	b.users[user.Name].AccountRecoveryHint = ""
    	delete(b.recoveryPasswords, user.Name)
    	return nil
    }
    
    func (b *fakeBackend) SetResourcePassword(_ context.Context, r *Resource, password string) error {
    	b.resourcePasswords[r.ID.String()] = password
    	return nil
    }
    
    func (b *fakeBackend) GetUserEncryptionKeys(_ context.Context, user *User) ([]*ct.EncryptedKey, error) {
    	return b.encryptionKeys[user.Name], nil
    }
    
    func (b *fakeBackend) SetUserEncryptionKeys(_ context.Context, user *User, keys []*ct.EncryptedKey) error {
    	b.encryptionKeys[user.Name] = keys
    	b.users[user.Name].HasEncryptionKeys = true
    	return nil
    }
    
    func (b *fakeBackend) SetUserEncryptionPublicKey(_ context.Context, user *User, pub []byte) error {
    	return nil
    }
    
    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
    }
    
    func (b *fakeBackend) DeleteApplicationSpecificPassword(_ context.Context, user *User, id string) error {
    	return nil
    }
    
    func (b *fakeBackend) SetUserTOTPSecret(_ context.Context, user *User, secret string) error {
    	return nil
    }
    
    func (b *fakeBackend) DeleteUserTOTPSecret(_ context.Context, user *User) error {
    	return nil
    }
    
    func (b *fakeBackend) HasAnyResource(_ context.Context, rsrcs []FindResourceRequest) (bool, error) {
    	for _, fr := range rsrcs {
    		for _, r := range b.resources {
    			if r.Type == fr.Type && r.Name == fr.Name {
    				return true, nil
    			}
    		}
    	}
    	return false, nil
    }
    
    // Make the fakeBackend match the auth.Client interface.
    func (b *fakeBackend) Authenticate(_ context.Context, req *auth.Request) (*auth.Response, error) {
    	var pw string
    	var ok bool
    
    	switch req.Service {
    	case defaultUserAuthService:
    		pw, ok = b.passwords[req.Username]
    	case defaultAccountRecoveryAuthService:
    		pw, ok = b.recoveryPasswords[req.Username]
    	default:
    		return nil, errors.New("unknown service")
    	}
    	if !ok {
    		return nil, errors.New("no such user")
    	}
    	if !pwhash.ComparePassword(pw, string(req.Password)) {
    		return &auth.Response{Status: auth.StatusError}, nil
    	}
    	return &auth.Response{Status: auth.StatusOK}, nil
    }
    
    const testAdminGroupName = "admins"
    
    // Fake SSO validator: the sso ticket username is just the ticket
    // itself. The only invalid value is the empty string.
    type fakeValidator struct {
    	adminUser string
    }
    
    func (v *fakeValidator) Validate(tkt, nonce, service string, _ []string) (*sso.Ticket, error) {
    	if tkt == "" {
    		return nil, errors.New("empty sso ticket")
    	}
    	var groups []string
    	if tkt == v.adminUser {
    		groups = []string{testAdminGroupName}
    	}
    	return &sso.Ticket{
    		User:    tkt,
    		Service: service,
    		Domain:  "test",
    		Groups:  groups,
    	}, nil
    }
    
    func (b *fakeBackend) addUser(user *User, pw, rpw string) {
    	b.users[user.Name] = user
    	//b.resources[user.Name] = make(map[string]*Resource)
    	b.passwords[user.Name] = pwhash.Encrypt(pw)
    	if rpw != "" {
    		b.recoveryPasswords[user.Name] = pwhash.Encrypt(rpw)
    	}
    	for _, r := range user.Resources {
    		b.resources[r.ID.String()] = r
    	}
    }
    
    func createFakeBackend() *fakeBackend {
    	fb := &fakeBackend{
    		users:                make(map[string]*User),
    		resources:            make(map[string]*Resource),
    		passwords:            make(map[string]string),
    		recoveryPasswords:    make(map[string]string),
    		resourcePasswords:    make(map[string]string),
    		appSpecificPasswords: make(map[string][]*ct.AppSpecificPassword),
    		encryptionKeys:       make(map[string][]*ct.EncryptedKey),
    	}
    	fb.addUser(&User{
    		Name:   testUser,
    		Status: UserStatusActive,
    		Shard:  "host1",
    		UID:    4242,
    		Resources: []*Resource{
    			{
    				ID:            makeResourceID(testUser, ResourceTypeEmail, testUser),
    				Name:          testUser,
    				Type:          ResourceTypeEmail,
    				Status:        ResourceStatusActive,
    				Shard:         "host1",
    				OriginalShard: "host1",
    				Email: &Email{
    					Maildir: "example.com/testuser",
    				},
    			},
    			{
    				ID:     makeResourceID(testUser, ResourceTypeDAV, "dav1"),
    				Name:   "dav1",
    				Type:   ResourceTypeDAV,
    				Status: ResourceStatusActive,
    				DAV: &WebDAV{
    					UID:     4242,
    					Homedir: "/home/dav1",
    				},
    			},
    		},
    	}, "password", "recoverypassword")
    	return fb
    }
    
    func testConfig() *Config {
    	var c Config
    	c.Validation.ForbiddenUsernames = []string{"root"}
    	c.Validation.AvailableDomains = map[string][]string{
    		ResourceTypeEmail:       []string{"example.com"},
    		ResourceTypeMailingList: []string{"example.com"},
    	}
    	c.SSO.Domain = "mydomain"
    	c.SSO.Service = "service/"
    	c.SSO.AdminGroup = testAdminGroupName
    	c.Shards.Available = map[string][]string{
    		ResourceTypeEmail:       []string{"host1", "host2", "host3"},
    		ResourceTypeMailingList: []string{"host1", "host2", "host3"},
    		ResourceTypeWebsite:     []string{"host1", "host2", "host3"},
    		ResourceTypeDomain:      []string{"host1", "host2", "host3"},
    		ResourceTypeDAV:         []string{"host1", "host2", "host3"},
    	}
    	c.Shards.Allowed = c.Shards.Available
    	return &c
    }
    
    func testService(admin string) *AccountService {
    	be := createFakeBackend()
    	svc, _ := newAccountServiceInternal(be, testConfig(), &fakeValidator{admin}, be)
    	return svc
    }
    
    func getUser(t testing.TB, svc *AccountService, username string) *User {
    	req := &GetUserRequest{
    		UserRequestBase: UserRequestBase{
    			RequestBase: RequestBase{
    				SSO: testUser,
    			},
    			Username: testUser,
    		},
    		IncludeInactive: true,
    	}
    	resp, err := svc.Handle(context.TODO(), req)
    	if err != nil {
    		t.Fatal(err)
    	}
    	return resp.(*User)
    }
    
    func TestService_GetUser(t *testing.T) {
    	svc := testService("")
    
    	user := getUser(t, svc, testUser)
    	if user.Name != testUser {
    		t.Fatalf("bad response: %+v", user)
    	}
    }
    
    func TestService_GetUser_ResourceGroups(t *testing.T) {
    	fb := createFakeBackend()
    	svc, _ := newAccountServiceInternal(fb, testConfig(), &fakeValidator{}, nil)
    
    	fb.addUser(&User{
    		Name:   "testuser2",
    		Status: UserStatusActive,
    		Resources: []*Resource{
    			{
    				ID:   makeResourceID("testuser2", ResourceTypeDAV, "dav1"),
    				Type: ResourceTypeDAV,
    				Name: "dav1",
    				DAV: &WebDAV{
    					Homedir: "/home/users/investici.org/dav1",
    				},
    			},
    			{
    				ID:   makeResourceID("testuser2", ResourceTypeDAV, "dav1-domain2"),
    				Type: ResourceTypeDAV,
    				Name: "dav1-domain2",
    				DAV: &WebDAV{
    					Homedir: "/home/users/investici.org/dav1/html-domain2.com/subdir",
    				},
    			},
    			{
    				ID:   makeResourceID("testuser2", ResourceTypeDomain, "domain1.com"),
    				Type: ResourceTypeDomain,
    				Name: "domain1.com",
    				Website: &Website{
    					DocumentRoot: "/home/users/investici.org/dav1/html-domain1.com",
    				},
    			},
    			{
    				ID:   makeResourceID("testuser2", ResourceTypeDomain, "domain2.com"),
    				Type: ResourceTypeDomain,
    				Name: "domain2.com",
    				Website: &Website{
    					DocumentRoot: "/home/users/investici.org/dav1/html-domain2.com",
    				},
    			},
    			{
    				ID:       makeResourceID("testuser2", ResourceTypeDatabase, "db2"),
    				ParentID: makeResourceID("testuser2", ResourceTypeDomain, "domain2.com"),
    				Type:     ResourceTypeDatabase,
    				Name:     "db2",
    				Database: &Database{},
    			},
    		},
    	}, "", "")
    
    	req := &GetUserRequest{
    		UserRequestBase: UserRequestBase{
    			RequestBase: RequestBase{
    				SSO: "testuser2",
    			},
    			Username: "testuser2",
    		},
    	}
    	resp, err := svc.Handle(context.TODO(), req)
    	if err != nil {
    		t.Fatal(err)
    	}
    	user := resp.(*User)
    
    	var grouped []*Resource
    	for _, r := range user.Resources {
    		switch r.Type {
    		case ResourceTypeWebsite, ResourceTypeDomain, ResourceTypeDAV, ResourceTypeDatabase:
    			grouped = append(grouped, r)
    		}
    	}
    
    	var group string
    	for _, r := range grouped {
    		if r.Group == "" {
    			t.Errorf("group not set on %s", r.ID)
    			continue
    		}
    		if group == "" {
    			group = r.Group
    		} else if group != r.Group {
    			t.Errorf("wrong group on %s (%s, expected %s)", r.ID, r.Group, group)
    		}
    	}
    
    }
    
    func TestService_Auth(t *testing.T) {
    	svc := testService("adminuser")
    
    	for _, td := range []struct {
    		sso        string
    		expectedOk bool
    	}{
    		{testUser, true},
    		{"otheruser", false},
    		{"adminuser", true},
    	} {
    		req := &GetUserRequest{
    			UserRequestBase: UserRequestBase{
    				RequestBase: RequestBase{
    					SSO: td.sso,
    				},
    				Username: testUser,
    			},
    		}
    		_, err := svc.Handle(context.TODO(), req)
    		if err != nil {
    			if !IsAuthError(err) {
    				t.Errorf("error for sso_user=%s is not an auth error: %v", td.sso, err)
    			} else if td.expectedOk {
    				t.Errorf("error with sso_user=%s: %v", td.sso, err)
    			}
    		} else if !td.expectedOk {
    			t.Errorf("no error with sso_user=%s", td.sso)
    		}
    	}
    }
    
    func TestService_ChangePassword(t *testing.T) {
    	fb := createFakeBackend()
    	svc, _ := newAccountServiceInternal(fb, testConfig(), &fakeValidator{}, fb)
    
    	testdata := []struct {
    		password    string
    		newPassword string
    		expectedOk  bool
    	}{
    		// First, fail cur_password authentication.
    		{"BADPASS", "new_password", false},
    
    		// Ordering is important as it is meant to emulate
    		// setting the password, failing to reset it, then
    		// succeeding.
    		{"password", "new_password", true},
    		{"BADPASS", "new_password_2", false},
    		{"new_password", "new_password_2", true},
    	}
    	for _, td := range testdata {
    		req := &ChangeUserPasswordRequest{
    			PrivilegedRequestBase: PrivilegedRequestBase{
    				UserRequestBase: UserRequestBase{
    					RequestBase: RequestBase{
    						SSO: testUser,
    					},
    					Username: testUser,
    				},
    				CurPassword: td.password,
    			},
    			Password: td.newPassword,
    		}
    		_, err := svc.Handle(context.TODO(), req)
    		if err == nil && !td.expectedOk {
    			t.Fatalf("ChangeUserPassword(old=%s new=%s) should have failed but didn't", td.password, td.newPassword)
    		} else if err != nil && td.expectedOk {
    			t.Fatalf("ChangeUserPassword(old=%s new=%s) failed: %v", td.password, td.newPassword, err)
    		}
    	}
    
    	if _, ok := fb.passwords[testUser]; !ok {
    		t.Error("password was not set on the backend")
    	}
    	// if len(fb.encryptionKeys[testUser]) != 1 {
    	// 	t.Errorf("no encryption keys were set")
    	// }
    }
    
    // Lower level test that basically corresponds to the same operations
    // as TestService_ChangePassword above, but exercises the
    // initializeUserEncryptionKeys / updateUserEncryptionKeys code path
    // directly.
    // func TestService_EncryptionKeys(t *testing.T) {
    // 	fb := createFakeBackend()
    // 	svc, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
    // 	tx, _ := fb.NewTransaction()
    // 	ctx := context.Background()
    
    // 	user, _ := getUserOrDie(ctx, tx, testUser)
    
    // 	// Set the keys to something.
    // 	keys, _, err := svc.initializeEncryptionKeys(ctx, tx, user, "password")
    // 	if err != nil {
    // 		t.Fatal("init", err)
    // 	}
    // 	if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil {
    // 		t.Fatal("SetUserEncryptionKeys", err)
    // 	}
    // 	if n := len(fb.encryptionKeys[testUser]); n != 1 {
    // 		t.Fatalf("found %d encryption keys, expected 1", n)
    // 	}
    
    // 	// Try to read (decrypt) them again using bad / good passwords.
    // 	if _, _, err := svc.readOrInitializeEncryptionKeys(ctx, tx, user, "BADPASS", "new_password"); err == nil {
    // 		t.Fatal("read with bad password did not fail")
    // 	}
    // 	if _, _, err := svc.readOrInitializeEncryptionKeys(ctx, tx, user, "password", "new_password"); err != nil {
    // 		t.Fatal("readOrInitialize", err)
    // 	}
    // }
    
    // Try adding aliases to the email resource.
    func TestService_AddEmailAlias(t *testing.T) {
    	svc := testService("")
    
    	// Find the resource ID.
    	user := getUser(t, svc, testUser)
    	emailID := user.GetSingleResourceByType(ResourceTypeEmail).ID
    
    	testdata := []struct {
    		addr       string
    		expectedOk bool
    	}{
    		{"alias@example.com", true},
    		{"another-example-address@example.com", true},
    		{"root@example.com", false},
    		{"alias@other-domain.com", false},
    	}
    	for _, td := range testdata {
    		req := &AddEmailAliasRequest{
    			ResourceRequestBase: ResourceRequestBase{
    				RequestBase: RequestBase{
    					SSO: testUser,
    				},
    				ResourceID: emailID,
    			},
    			Addr: td.addr,
    		}
    		_, err := svc.Handle(context.TODO(), req)
    		if err != nil && td.expectedOk {
    			t.Errorf("AddEmailAlias(%s) failed: %v", td.addr, err)
    		} else if err == nil && !td.expectedOk {
    			t.Errorf("AddEmailAlias(%s) did not fail but should have", td.addr)
    		}
    	}
    }
    
    func TestService_CreateResource(t *testing.T) {
    	svc := testService("admin")
    
    	req := &CreateResourcesRequest{
    		AdminRequestBase: AdminRequestBase{
    			RequestBase: RequestBase{
    				SSO: "admin",
    			},
    		},
    		Username: testUser,
    		Resources: []*Resource{
    			&Resource{
    				Type:          ResourceTypeDAV,
    				Name:          "dav2",
    				Status:        ResourceStatusActive,
    				Shard:         "host2",
    				OriginalShard: "host2",
    				DAV: &WebDAV{
    					Homedir: "/home/dav2",
    				},
    			},
    		},
    	}
    
    	// The request should succeed the first time around.
    	obj, err := svc.Handle(context.TODO(), req)
    	if err != nil {
    		t.Fatal("CreateResources", err)
    	}
    	resp := obj.(*CreateResourcesResponse)
    
    	// Check that a ResourceID has been set.
    	if len(resp.Resources) != 1 {
    		t.Fatalf("bad response, expected 1 resource, got %+v", resp)
    	}
    	rsrc := resp.Resources[0]
    	if rsrc.ID.Empty() {
    		t.Fatal("Resource ID is empty!")
    	}
    
    	// The object already exists, so the same request should fail now.
    	_, err = svc.Handle(context.TODO(), req)
    	if err == nil {
    		t.Fatal("creating a duplicate resource did not fail")
    	}
    }
    
    func TestService_CreateResource_List(t *testing.T) {
    	svc := testService("admin")
    
    	// A list is an example of a user-less (global) resource.
    	req := &CreateResourcesRequest{
    		AdminRequestBase: AdminRequestBase{
    			RequestBase: RequestBase{
    				SSO: "admin",
    			},
    		},
    		Username: testUser,
    		Resources: []*Resource{
    			&Resource{
    				Type:          ResourceTypeMailingList,
    				Name:          "list@example.com",
    				Status:        ResourceStatusActive,
    				Shard:         "host2",
    				OriginalShard: "host2",
    				List: &MailingList{
    					Admins: []string{testUser},
    				},
    			},
    		},
    	}
    
    	// The request should succeed.
    	_, err := svc.Handle(context.TODO(), req)
    	if err != nil {
    		t.Fatal("CreateResources", err)
    	}
    }
    
    func TestService_UpdateResource(t *testing.T) {
    	svc := testService("admin")
    
    	// Find the resource, modify a copy of it.
    	user := getUser(t, svc, testUser)
    	email := user.GetSingleResourceByType(ResourceTypeEmail).Copy()
    	email.Email.QuotaLimit = 9000
    
    	req := &AdminUpdateResourceRequest{
    		AdminResourceRequestBase: AdminResourceRequestBase{
    			ResourceRequestBase: ResourceRequestBase{
    				RequestBase: RequestBase{
    					SSO: "admin",
    				},
    				ResourceID: email.ID,
    			},
    		},
    		Resource: email,
    	}
    
    	_, err := svc.Handle(context.TODO(), req)
    	if err != nil {
    		t.Fatal("UpdateResource", err)
    	}
    }
    
    func TestService_UpdateResource_FailsValidation(t *testing.T) {
    	svc := testService("admin")
    
    	// Find the resource, modify a copy of it, but set a parameter
    	// that will fail validation (i.e. a bad shard).
    	user := getUser(t, svc, testUser)
    	email := user.GetSingleResourceByType(ResourceTypeEmail).Copy()
    	email.Shard = "invalid-shard"
    
    	req := &AdminUpdateResourceRequest{
    		AdminResourceRequestBase: AdminResourceRequestBase{
    			ResourceRequestBase: ResourceRequestBase{
    				RequestBase: RequestBase{
    					SSO: "admin",
    				},
    				ResourceID: email.ID,
    			},
    		},
    		Resource: email,
    	}
    
    	_, err := svc.Handle(context.TODO(), req)
    	if err == nil {
    		t.Fatal("UpdateResource() accepted an invalid 'shard' attribute")
    	}
    }
    
    func TestService_CreateUser(t *testing.T) {
    	svc := testService("admin")
    
    	//emailResourceID := NewResourceID(ResourceTypeEmail, "testuser2@example.com", "testuser2@example.com")
    	req := &CreateUserRequest{
    		AdminRequestBase: AdminRequestBase{
    			RequestBase: RequestBase{
    				SSO: "admin",
    			},
    		},
    		User: &User{
    			Name: "testuser2@example.com",
    			Resources: []*Resource{
    				&Resource{
    					Type: ResourceTypeEmail,
    					Name: "testuser2@example.com",
    					//Status:        ResourceStatusActive,
    					//Shard:         "host2",
    					//OriginalShard: "host2",
    					Email: &Email{
    						Maildir: "example.com/testuser2",
    					},
    				},
    			},
    		},
    	}
    
    	// The request should succeed the first time around.
    	resp, err := svc.Handle(context.Background(), req)
    	if err != nil {
    		t.Fatal("CreateResources", err)
    	}
    	cresp := resp.(*CreateUserResponse)
    	if cresp.User.Name != "testuser2@example.com" {
    		t.Fatalf("unexpected user in response: got %s, expected testuser2", cresp.User.Name)
    	}
    
    	// Hit the database to verify that the object has been created.
    	tx, _ := svc.backend.NewTransaction()
    	user, _ := tx.GetUser(context.Background(), "testuser2@example.com")
    	if user == nil {
    		t.Fatal("GetUser returned nil")
    	}
    
    	// Verify that the new resource has default fields set.
    	resource := user.GetSingleResourceByType(ResourceTypeEmail)
    	if resource == nil {
    		t.Fatalf("no email resource in user %+v", user)
    	}
    	if resource.Shard == "" {
    		t.Fatalf("resource shard is unset: %+v", resource)
    	}
    }
    
    func TestService_CreateUser_FailIfNotAdmin(t *testing.T) {
    	svc := testService("admin")
    
    	req := &CreateUserRequest{
    		AdminRequestBase: AdminRequestBase{
    			RequestBase: RequestBase{
    				SSO: testUser,
    			},
    		},
    		User: &User{
    			Name: "testuser2@example.com",
    			Resources: []*Resource{
    				&Resource{
    					Type:          ResourceTypeEmail,
    					Name:          "testuser2@example.com",
    					Status:        ResourceStatusActive,
    					Shard:         "host2",
    					OriginalShard: "host2",
    					Email: &Email{
    						Maildir: "example.com/testuser2",
    					},
    				},
    			},
    		},
    	}
    
    	// The request should succeed the first time around.
    	_, err := svc.Handle(context.TODO(), req)
    	if err == nil {
    		t.Fatal("unauthorized CreateResources did not fail")
    	}
    }
    
    func TestService_ResetResourcePassword(t *testing.T) {
    	svc := testService("")
    
    	user := getUser(t, svc, testUser)
    	id := user.GetSingleResourceByType(ResourceTypeDAV).ID
    	req := &ResetResourcePasswordRequest{
    		ResourceRequestBase: ResourceRequestBase{
    			RequestBase: RequestBase{
    				SSO: testUser,
    			},
    			ResourceID: id,
    		},
    	}
    	resp, err := svc.Handle(context.TODO(), req)
    	if err != nil {
    		t.Fatal("ResetResourcePassword", err)
    	}
    	rresp := resp.(*ResetResourcePasswordResponse)
    	if len(rresp.Password) < 10 {
    		t.Fatalf("short password: %q", rresp.Password)
    	}
    	storedPw, ok := svc.backend.(*fakeBackend).resourcePasswords[id.String()]
    	if !ok {
    		t.Fatal("resource password was not actually set on the backend")
    	}
    	if storedPw == rresp.Password {
    		t.Fatal("oops, it appears that the password was stored in cleartext on the backend")
    	}
    }
    
    func TestService_AccountRecovery(t *testing.T) {
    	svc := testService("")
    
    	// Bad recovery response.
    	req := &AccountRecoveryRequest{
    		Username:         testUser,
    		RecoveryPassword: "BADPASS",
    		Password:         "new_password",
    	}
    	_, err := svc.Handle(context.TODO(), req)
    	if err == nil {
    		t.Fatal("oops, recovered account with bad password")
    	}
    
    	// Successful account recovery.
    	req = &AccountRecoveryRequest{
    		Username:         testUser,
    		RecoveryPassword: "recoverypassword",
    		Password:         "new_password",
    	}
    	_, err = svc.Handle(context.TODO(), req)
    	if err != nil {
    		t.Fatalf("account recovery failed: %v", err)
    	}
    }
    
    func TestService_DisableUser(t *testing.T) {
    	svc := testService("")
    
    	req := &DisableUserRequest{
    		UserRequestBase: UserRequestBase{
    			RequestBase: RequestBase{
    				SSO: testUser,
    			},
    			Username: testUser,
    		},
    	}
    	_, err := svc.Handle(context.Background(), req)
    	if err != nil {
    		t.Fatal("DisableUser", err)
    	}
    
    	// Verify that everything has been disabled.
    	user := getUser(t, svc, testUser)
    	if user.Status != UserStatusInactive {
    		t.Fatalf("user status is '%s', expected inactive", user.Status)
    	}
    	for _, rsrc := range user.Resources {
    		if rsrc.Status != ResourceStatusInactive {
    			t.Fatalf("resource %s status is '%s', expected inactive", rsrc.ID, rsrc.Status)
    		}
    	}
    }