diff --git a/actions.go b/actions.go
index 484a267a9f027a935eab328fe66f6cfb7dff57a9..28042d2005cd4b8ac31e29f4f3fceef686111c92 100644
--- a/actions.go
+++ b/actions.go
@@ -39,11 +39,12 @@ type Backend interface {
 type TX interface {
 	Commit(context.Context) error
 
+	GetResource(context.Context, ResourceID) (*Resource, error)
+	UpdateResource(context.Context, *Resource) error
+	SetResourcePassword(context.Context, *Resource, string) error
+
 	GetUser(context.Context, string) (*User, error)
-	GetResource(context.Context, string, string) (*Resource, error)
-	UpdateResource(context.Context, string, *Resource) error
 	SetUserPassword(context.Context, *User, string) error
-	SetResourcePassword(context.Context, string, *Resource, string) error
 	GetUserEncryptionKeys(context.Context, *User) ([]*UserEncryptionKey, error)
 	SetUserEncryptionKeys(context.Context, *User, []*UserEncryptionKey) error
 	SetUserEncryptionPublicKey(context.Context, *User, []byte) error
@@ -194,8 +195,8 @@ func (s *AccountService) GetUser(ctx context.Context, tx TX, req *GetUserRequest
 
 // setResourceStatus sets the status of a single resource (shared
 // logic between enable / disable resource methods).
-func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, username, resourceID, status string) error {
-	r, err := tx.GetResource(ctx, username, resourceID)
+func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, username string, resourceID ResourceID, status string) error {
+	r, err := tx.GetResource(ctx, resourceID)
 	if err != nil {
 		return newBackendError(err)
 	}
@@ -203,7 +204,7 @@ func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, username,
 		return ErrResourceNotFound
 	}
 	r.Status = status
-	if err := tx.UpdateResource(ctx, username, r); err != nil {
+	if err := tx.UpdateResource(ctx, r); err != nil {
 		return newBackendError(err)
 	}
 	return nil
@@ -211,7 +212,7 @@ func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, username,
 
 type DisableResourceRequest struct {
 	RequestBase
-	ResourceID string `json:"resource_id"`
+	ResourceID ResourceID `json:"resource_id"`
 }
 
 // DisableResource disables a resource belonging to the user.
@@ -224,7 +225,7 @@ func (s *AccountService) DisableResource(ctx context.Context, tx TX, req *Disabl
 
 type EnableResourceRequest struct {
 	RequestBase
-	ResourceID string `json:"resource_id"`
+	ResourceID ResourceID `json:"resource_id"`
 }
 
 // EnableResource enables a resource belonging to the user.
@@ -275,7 +276,7 @@ func (s *AccountService) ChangeUserPassword(ctx context.Context, tx TX, req *Cha
 		return newBackendError(err)
 	}
 	for _, r := range user.GetResourcesByType(ResourceTypeEmail) {
-		if err := tx.SetResourcePassword(ctx, user.Name, r, encPass); err != nil {
+		if err := tx.SetResourcePassword(ctx, r, encPass); err != nil {
 			return newBackendError(err)
 		}
 	}
@@ -466,8 +467,8 @@ func (s *AccountService) DeleteApplicationSpecificPassword(ctx context.Context,
 
 type ChangeResourcePasswordRequest struct {
 	RequestBase
-	ResourceID string `json:"resource_id"`
-	Password   string `json:"password"`
+	ResourceID ResourceID `json:"resource_id"`
+	Password   string     `json:"password"`
 }
 
 func (r *ChangeResourcePasswordRequest) Validate() error {
@@ -490,7 +491,7 @@ func (s *AccountService) ChangeResourcePassword(ctx context.Context, tx TX, req
 		return newRequestError(err)
 	}
 
-	r, err := tx.GetResource(ctx, req.Username, req.ResourceID)
+	r, err := tx.GetResource(ctx, req.ResourceID)
 	if err != nil {
 		return newBackendError(err)
 	}
@@ -499,7 +500,7 @@ func (s *AccountService) ChangeResourcePassword(ctx context.Context, tx TX, req
 	}
 
 	encPass := pwhash.Encrypt(req.Password)
-	if err := tx.SetResourcePassword(ctx, req.Username, r, encPass); err != nil {
+	if err := tx.SetResourcePassword(ctx, r, encPass); err != nil {
 		return newBackendError(err)
 	}
 	return nil
@@ -507,8 +508,8 @@ func (s *AccountService) ChangeResourcePassword(ctx context.Context, tx TX, req
 
 type MoveResourceRequest struct {
 	RequestBase
-	ResourceID string `json:"resource_id"`
-	Shard      string `json:"shard"`
+	ResourceID ResourceID `json:"resource_id"`
+	Shard      string     `json:"shard"`
 }
 
 type MoveResourceResponse struct {
@@ -526,7 +527,7 @@ func (s *AccountService) MoveResource(ctx context.Context, tx TX, req *MoveResou
 	}
 
 	// Collect all related resources, as they should all be moved at once.
-	r, err := tx.GetResource(ctx, req.Username, req.ResourceID)
+	r, err := tx.GetResource(ctx, req.ResourceID)
 	if err != nil {
 		return nil, err
 	}
@@ -540,7 +541,7 @@ func (s *AccountService) MoveResource(ctx context.Context, tx TX, req *MoveResou
 	var resp MoveResourceResponse
 	for _, r := range resources {
 		r.Shard = req.Shard
-		if err := tx.UpdateResource(ctx, req.Username, r); err != nil {
+		if err := tx.UpdateResource(ctx, r); err != nil {
 			return nil, err
 		}
 		resp.MovedIDs = append(resp.MovedIDs, r.ID.String())
diff --git a/actions_test.go b/actions_test.go
index a503a80c65ac125a3bbced063423e9cd111e5c98..0a9f7cf618ab566ae2d784acd69ce96cf2f6b14f 100644
--- a/actions_test.go
+++ b/actions_test.go
@@ -27,11 +27,11 @@ func (b *fakeBackend) GetUser(_ context.Context, username string) (*User, error)
 	return b.users[username], nil
 }
 
-func (b *fakeBackend) GetResource(_ context.Context, username, resourceID string) (*Resource, error) {
-	return b.resources[username][resourceID], nil
+func (b *fakeBackend) GetResource(_ context.Context, resourceID ResourceID) (*Resource, error) {
+	return b.resources[resourceID.User()][resourceID.String()], nil
 }
 
-func (b *fakeBackend) UpdateResource(_ context.Context, username string, r *Resource) error {
+func (b *fakeBackend) UpdateResource(_ context.Context, r *Resource) error {
 	return nil
 }
 
@@ -40,7 +40,7 @@ func (b *fakeBackend) SetUserPassword(_ context.Context, user *User, password st
 	return nil
 }
 
-func (b *fakeBackend) SetResourcePassword(_ context.Context, username string, r *Resource, password string) error {
+func (b *fakeBackend) SetResourcePassword(_ context.Context, r *Resource, password string) error {
 	return nil
 }
 
@@ -84,7 +84,7 @@ type fakeValidator struct {
 	adminUser string
 }
 
-func (v *fakeValidator) Validate(tkt string, nonce string, service string, _ []string) (*sso.Ticket, error) {
+func (v *fakeValidator) Validate(tkt, nonce, service string, _ []string) (*sso.Ticket, error) {
 	// The sso ticket username is just the ticket itself.
 	var groups []string
 	if tkt == v.adminUser {
diff --git a/backend/model.go b/backend/model.go
index bb1ea8b45287429e4e3be3b03070754c8d935fe8..32f4b1884c39141d4c14f6a05416402e368d4d66 100644
--- a/backend/model.go
+++ b/backend/model.go
@@ -337,7 +337,7 @@ func (tx *backendTX) DeleteUserTOTPSecret(ctx context.Context, user *accountserv
 	return nil
 }
 
-func (tx *backendTX) SetResourcePassword(ctx context.Context, username string, r *accountserver.Resource, encryptedPassword string) error {
+func (tx *backendTX) SetResourcePassword(ctx context.Context, r *accountserver.Resource, encryptedPassword string) error {
 	dn, _ := tx.backend.resources.GetDN(r.ID)
 	tx.setAttr(dn, "userPassword", encryptedPassword)
 	return nil
@@ -378,12 +378,7 @@ func (tx *backendTX) HasAnyResource(ctx context.Context, resourceIDs []accountse
 }
 
 // GetResource returns a ResourceWrapper, as part of a read-modify-update transaction.
-func (tx *backendTX) GetResource(ctx context.Context, username, resourceID string) (*accountserver.Resource, error) {
-	rsrcID, err := accountserver.ParseResourceID(resourceID)
-	if err != nil {
-		return nil, err
-	}
-
+func (tx *backendTX) GetResource(ctx context.Context, rsrcID accountserver.ResourceID) (*accountserver.Resource, error) {
 	// From the resource ID we can obtain the DN, and fetch it
 	// straight from LDAP without even doing a real search.
 	dn, err := tx.backend.resources.GetDN(rsrcID)
@@ -417,7 +412,7 @@ func (tx *backendTX) GetResource(ctx context.Context, username, resourceID strin
 }
 
 // UpdateResource updates a LDAP-backed resource that was obtained by a previous GetResource call.
-func (tx *backendTX) UpdateResource(ctx context.Context, username string, r *accountserver.Resource) error {
+func (tx *backendTX) UpdateResource(ctx context.Context, r *accountserver.Resource) error {
 	dn, err := tx.backend.resources.GetDN(r.ID)
 	if err != nil {
 		return err
@@ -464,7 +459,7 @@ func groupWebResourcesByHomedir(resources []*accountserver.Resource) {
 	webs := make(map[string]*accountserver.Resource)
 	for _, r := range resources {
 		switch r.ID.Type() {
-		case accountserver.ResourceTypeWebsite:
+		case accountserver.ResourceTypeWebsite, accountserver.ResourceTypeDomain:
 			r.Group = getHostingDir(r.Website.DocumentRoot)
 			webs[r.ID.String()] = r
 		case accountserver.ResourceTypeDAV:
diff --git a/backend/model_test.go b/backend/model_test.go
index cedc36e7c040430d731ff19ee311a0577e83ccdc..46f55fef45012dedbea1d3d07038b7f69d1e5051 100644
--- a/backend/model_test.go
+++ b/backend/model_test.go
@@ -2,7 +2,6 @@ package backend
 
 import (
 	"context"
-	"fmt"
 	"testing"
 
 	"git.autistici.org/ai3/accountserver"
@@ -15,13 +14,12 @@ const (
 	testUser1    = "uno@investici.org"
 )
 
-func TestModel_GetUser(t *testing.T) {
+func startServerAndGetUser(t testing.TB) (func(), accountserver.Backend, *accountserver.User) {
 	stop := startTestLDAPServer(t, &testLDAPServerConfig{
 		Port:  testLDAPPort,
 		Base:  "dc=example,dc=com",
 		LDIFs: []string{"testdata/base.ldif", "testdata/test1.ldif"},
 	})
-	defer stop()
 
 	b, err := NewLDAPBackend(testLDAPAddr, "cn=manager,dc=example,dc=com", "password", "dc=example,dc=com")
 	if err != nil {
@@ -37,13 +35,18 @@ func TestModel_GetUser(t *testing.T) {
 		t.Fatalf("could not find test user %s", testUser1)
 	}
 
-	//t.Logf("%+v", user)
+	return stop, b, user
+}
+
+func TestModel_GetUser(t *testing.T) {
+	stop, _, user := startServerAndGetUser(t)
+	defer stop()
 
 	if user.Name != testUser1 {
 		t.Fatalf("bad username: expected %s, got %s", testUser1, user.Name)
 	}
-	if len(user.Resources) != 4 {
-		t.Fatalf("expected 4 resources, got %d", len(user.Resources))
+	if len(user.Resources) != 5 {
+		t.Fatalf("expected 5 resources, got %d", len(user.Resources))
 	}
 
 	// Test a specific resource (the database).
@@ -75,6 +78,59 @@ func TestModel_GetUser(t *testing.T) {
 	}
 }
 
+func TestModel_GetUser_Group(t *testing.T) {
+	stop, _, user := startServerAndGetUser(t)
+	defer stop()
+
+	var grouped []*accountserver.Resource
+	for _, r := range user.Resources {
+		switch r.ID.Type() {
+		case accountserver.ResourceTypeWebsite, accountserver.ResourceTypeDomain, accountserver.ResourceTypeDAV, accountserver.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 TestModel_GetUser_Resources(t *testing.T) {
+	stop, b, user := startServerAndGetUser(t)
+	defer stop()
+
+	// Fetch individually all user resources, one by one, and
+	// check that they match what we have already.
+	tx2, _ := b.NewTransaction()
+	for _, r := range user.Resources {
+		fr, err := tx2.GetResource(context.Background(), r.ID)
+		if err != nil {
+			t.Errorf("could not fetch resource %s: %v", r.ID, err)
+			continue
+		}
+		if fr == nil {
+			t.Errorf("resource %s is missing", r.ID)
+			continue
+		}
+		// It's ok if Group is unset in the GetResource response.
+		rr := *r
+		rr.Group = ""
+		if err := deep.Equal(fr, &rr); err != nil {
+			t.Errorf("error in fetched resource %s: %v", r.ID, err)
+			continue
+		}
+	}
+}
+
 func TestModel_SetResourceStatus(t *testing.T) {
 	stop := startTestLDAPServer(t, &testLDAPServerConfig{
 		Port:  testLDAPPort,
@@ -89,8 +145,8 @@ func TestModel_SetResourceStatus(t *testing.T) {
 	}
 
 	tx, _ := b.NewTransaction()
-	rsrcID := fmt.Sprintf("email/%s/%s", testUser1, testUser1)
-	r, err := tx.GetResource(context.Background(), testUser1, rsrcID)
+	rsrcID := accountserver.NewResourceID(accountserver.ResourceTypeEmail, testUser1, testUser1)
+	r, err := tx.GetResource(context.Background(), rsrcID)
 	if err != nil {
 		t.Fatal("GetResource", err)
 	}
@@ -98,10 +154,8 @@ func TestModel_SetResourceStatus(t *testing.T) {
 		t.Fatalf("could not find test resource %s", rsrcID)
 	}
 
-	//t.Logf("%+v", r)
-
 	r.Status = accountserver.ResourceStatusInactive
-	if err := tx.UpdateResource(context.Background(), "uno@investici.org", r); err != nil {
+	if err := tx.UpdateResource(context.Background(), r); err != nil {
 		t.Fatal("UpdateResource", err)
 	}
 	if err := tx.Commit(context.Background()); err != nil {
diff --git a/backend/resources.go b/backend/resources.go
index ea48de46ef3f7b2d1d737637f0036ba4524bfbb3..3da6cea0a949de7bf4c8ba6dc78c3a3d2b92cc50 100644
--- a/backend/resources.go
+++ b/backend/resources.go
@@ -368,7 +368,7 @@ func (h *webdavResourceHandler) GetDN(id accountserver.ResourceID) (string, erro
 		return "", errors.New("unqualified resource id")
 	}
 
-	dn := replaceVars("cn=${resource},uid=${user},ou=People", map[string]string{
+	dn := replaceVars("ftpname=${resource},uid=${user},ou=People", map[string]string{
 		"user":     id.User(),
 		"resource": id.Name(),
 	})
@@ -425,7 +425,7 @@ type databaseResourceHandler struct {
 	baseDN string
 }
 
-func makeDatabaseResourceID(dn string) (rsrcID accountserver.ResourceID, parentID accountserver.ResourceID, err error) {
+func makeDatabaseResourceID(dn string) (rsrcID, parentID accountserver.ResourceID, err error) {
 	parsed, perr := ldap.ParseDN(dn)
 	if perr != nil {
 		err = perr
diff --git a/backend/testdata/test1.ldif b/backend/testdata/test1.ldif
index 308e52447a670a0d6770d23fa9ecfe4cb49e0b50..9bf6880683b247b165f7b48ccd62866470c24af8 100644
--- a/backend/testdata/test1.ldif
+++ b/backend/testdata/test1.ldif
@@ -82,6 +82,18 @@ creationDate: 01-08-2013
 originalHost: host2
 statsId: 2191
 
+dn: cn=example.com,uid=uno@investici.org,ou=People,dc=example,dc=com
+status: active
+acceptMail: true
+objectClass: top
+objectClass: virtualHost
+cn: example.com
+host: host2
+documentRoot: /home/users/investici.org/uno/html-example.com
+creationDate: 02-08-2013
+originalHost: host2
+statsId: 2192
+
 dn: dbname=unodb,alias=uno,uid=uno@investici.org,ou=People,dc=example,dc=com
 status: active
 clearPassword: password