167 lines
5.9 KiB
Go
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)
|
|
}
|
|
}
|