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" 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 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) ([]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) SearchResource(_ context.Context, pattern string) ([]*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.passwords[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 } 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), appSpecificPasswords: make(map[string][]*AppSpecificPasswordInfo), encryptionKeys: make(map[string][]*ct.EncryptedKey), } fb.addUser(&User{ Name: testUser, Status: UserStatusActive, Shard: "1", UID: 4242, Resources: []*Resource{ { ID: makeResourceID(testUser, ResourceTypeEmail, testUser), Name: testUser, Type: ResourceTypeEmail, Status: ResourceStatusActive, Shard: "1", 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, _ := newAccountServiceWithSSO(be, testConfig(), &fakeValidator{admin}) return svc } func getUser(t testing.TB, svc *AccountService, username string) *User { req := &GetUserRequest{ UserRequestBase: UserRequestBase{ RequestBase: RequestBase{ SSO: testUser, }, Username: testUser, }, } 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, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{}) 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, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{}) 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_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).passwords[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) } }