Skip to content
Snippets Groups Projects
Select Git revision
  • 932f534467c935f3f76968272aa7d29c1bfcccd7
  • master default protected
  • renovate/bootstrap-5.x
  • renovate/purgecss-webpack-plugin-7.x
  • renovate/mini-css-extract-plugin-2.x
  • renovate/html-webpack-plugin-5.x
  • renovate/golang-1.x
  • renovate/css-loader-6.x
8 results

convert.go

Blame
    • ale's avatar
      0def9013
      Switch to a self-hosted binary, add graph-related code · 0def9013
      ale authored
      The app is now self-hosted instead of relying on the static-content
      standalone server, so we can eventually add dynamic code for graph
      serving.
      
      The static content serving has improved, with more consistent cache
      header management, as well as the capability of serving pre-compressed
      content.
      
      Additional code to implement the generation of dependency (flow)
      graphs in dot format was added (not hooked to the HTTP server yet).
      0def9013
      History
      Switch to a self-hosted binary, add graph-related code
      ale authored
      The app is now self-hosted instead of relying on the static-content
      standalone server, so we can eventually add dynamic code for graph
      serving.
      
      The static content serving has improved, with more consistent cache
      header management, as well as the capability of serving pre-compressed
      content.
      
      Additional code to implement the generation of dependency (flow)
      graphs in dot format was added (not hooked to the HTTP server yet).
    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][]*AppSpecificPasswordInfo
    	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 *AppSpecificPasswordInfo, _ 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][]*AppSpecificPasswordInfo),
    		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)
    		}
    	}
    }