Files
mailgoserver/internal/webui/mailbox_multitenant_test.go
T

167 lines
5.9 KiB
Go

package webui
import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"mailgoserver/internal/db"
"mailgoserver/internal/mailstore"
)
// createMailboxFor mirrors setupTwoTenants' sender helper, for a mailbox instead.
func createMailboxFor(t *testing.T, app *App, email string, domainID int64) *db.Mailbox {
t.Helper()
hash, err := db.HashPassword("password123")
if err != nil {
t.Fatal(err)
}
dek := mailstore.GenerateDEK()
wrapped, nonce, err := app.Mailstore.WrapDEK(dek)
if err != nil {
t.Fatal(err)
}
id, err := app.DB.CreateMailbox(email, hash, domainID, 5*1024*1024*1024, wrapped, nonce)
if err != nil {
t.Fatal(err)
}
mbox, err := app.DB.GetMailboxByID(id)
if err != nil || mbox == nil {
t.Fatalf("expected mailbox to exist: %v", err)
}
return mbox
}
func TestScopedAdminCannotAccessOtherTenantMailboxByID(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domainA, domainB, _, _ := setupTwoTenants(t, app)
mailboxB := createMailboxFor(t, app, "carol@"+domainB.DomainName, domainB.ID)
cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})
req := httptest.NewRequest(http.MethodGet, Prefix+"/mailboxes/"+strconv.FormatInt(mailboxB.ID, 10)+"/edit", nil)
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404 for a mailbox outside scope, got %d", rec.Code)
}
form := url.Values{}
req2 := httptest.NewRequest(http.MethodPost, Prefix+"/mailboxes/"+strconv.FormatInt(mailboxB.ID, 10)+"/delete", strings.NewReader(form.Encode()))
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req2.AddCookie(cookie)
rec2 := httptest.NewRecorder()
mux.ServeHTTP(rec2, req2)
if rec2.Code != http.StatusNotFound {
t.Fatalf("expected 404 disabling a mailbox outside scope, got %d", rec2.Code)
}
stillActive, err := app.DB.GetMailboxByID(mailboxB.ID)
if err != nil || stillActive == nil || !stillActive.IsActive {
t.Fatal("mailbox outside scope must not have been modified")
}
}
func TestScopedAdminCannotCreateMailboxOnUnownedDomain(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domainA, domainB, _, _ := setupTwoTenants(t, app)
cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})
form := url.Values{
"local_part": {"mallory"},
"domain_id": {strconv.FormatInt(domainB.ID, 10)}, // not theirs
"password": {"password123"},
}
req := httptest.NewRequest(http.MethodPost, Prefix+"/mailboxes/add", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404 creating a mailbox on an unowned domain, got %d", rec.Code)
}
if m, _ := app.DB.GetMailboxByEmail("mallory@" + domainB.DomainName); m != nil {
t.Fatal("mailbox must not have been created on a domain outside the admin's scope")
}
}
func TestScopedAdminCanManageOwnMailboxAppPasswords(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domainA, _, _, _ := setupTwoTenants(t, app)
mailboxA := createMailboxFor(t, app, "dave@"+domainA.DomainName, domainA.ID)
cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})
form := url.Values{"label": {"laptop"}}
req := httptest.NewRequest(http.MethodPost, Prefix+"/mailboxes/"+strconv.FormatInt(mailboxA.ID, 10)+"/apppasswords/add", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("expected redirect after creating an app password, got %d: %s", rec.Code, rec.Body.String())
}
passwords, err := app.DB.ListAppPasswordsForMailbox(mailboxA.ID)
if err != nil || len(passwords) != 1 {
t.Fatalf("expected exactly one app password, got %d (err=%v)", len(passwords), err)
}
if len(passwords[0].PasswordHash) == 0 {
t.Fatal("expected a password hash to be stored")
}
}
func TestScopedAdminCannotCreateAliasOnUnownedDomain(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domainA, domainB, _, _ := setupTwoTenants(t, app)
mailboxA := createMailboxFor(t, app, "dave@"+domainA.DomainName, domainA.ID)
cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID})
form := url.Values{
"local_part": {"evilalias"},
"domain_id": {strconv.FormatInt(domainB.ID, 10)}, // not theirs
}
req := httptest.NewRequest(http.MethodPost, Prefix+"/mailboxes/"+strconv.FormatInt(mailboxA.ID, 10)+"/aliases/add", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected 404 creating an alias on an unowned domain, got %d", rec.Code)
}
if a, _ := app.DB.GetAliasByEmail("evilalias@" + domainB.DomainName); a != nil {
t.Fatal("alias must not have been created on a domain outside the admin's scope")
}
}
func TestAppPasswordCannotBeRevokedFromAnotherMailbox(t *testing.T) {
app := newTestApp(t)
domainA, domainB, _, _ := setupTwoTenants(t, app)
mailboxA := createMailboxFor(t, app, "dave@"+domainA.DomainName, domainA.ID)
mailboxB := createMailboxFor(t, app, "carol@"+domainB.DomainName, domainB.ID)
pwID, err := app.DB.CreateAppPassword(mailboxB.ID, "carol's laptop", "irrelevant-hash", nil)
if err != nil {
t.Fatal(err)
}
// Attempting to remove mailboxB's app password while scoped to mailboxA must be a
// no-op — RemoveAppPassword is scoped by (id, mailboxID) precisely to prevent this.
if err := app.DB.RemoveAppPassword(pwID, mailboxA.ID); err != nil {
t.Fatal(err)
}
remaining, err := app.DB.ListAppPasswordsForMailbox(mailboxB.ID)
if err != nil || len(remaining) != 1 {
t.Fatalf("expected mailboxB's app password to survive a delete scoped to mailboxA, got %d remaining (err=%v)", len(remaining), err)
}
}