package webui import ( "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" "mailgoserver/internal/db" ) // scopedLogin creates a domain-scoped (non-global) admin with access to exactly // domainIDs and returns its session cookie. CreateScopedAdminUser always sets // must_change_password (matching the real "you can't keep an admin-picked initial // password" flow), so this clears it the same way completing /first-login would — // otherwise every protected route redirects to /first-login before the scoping logic // these tests exercise ever runs. func scopedLogin(t *testing.T, app *App, username string, domainIDs []int64) *http.Cookie { t.Helper() hash, err := db.HashPassword("scoped-password-123!") if err != nil { t.Fatal(err) } userID, err := app.DB.CreateScopedAdminUser(username, hash, 0, domainIDs) if err != nil { t.Fatal(err) } if err := app.DB.UpdateAdminCredentials(userID, username, hash); err != nil { t.Fatal(err) } token, err := app.DB.CreateSession(userID, true, sessionTTL) if err != nil { t.Fatal(err) } return &http.Cookie{Name: sessionCookieName, Value: token} } // setupTwoTenants seeds two separate domains, each with its own sender, whitelisted // IP, and DKIM key, and returns everything needed to test cross-tenant isolation. func setupTwoTenants(t *testing.T, app *App) (domainA, domainB db.Domain, senderA, senderB *db.Sender) { t.Helper() aID, err := app.DB.CreateDomain("tenant-a.example") if err != nil { t.Fatal(err) } bID, err := app.DB.CreateDomain("tenant-b.example") if err != nil { t.Fatal(err) } da, _ := app.DB.GetDomainByID(aID) db_, _ := app.DB.GetDomainByID(bID) hash, _ := db.HashPassword("password123") saID, err := app.DB.CreateSender("alice@tenant-a.example", hash, aID, false, false) if err != nil { t.Fatal(err) } sbID, err := app.DB.CreateSender("bob@tenant-b.example", hash, bID, false, false) if err != nil { t.Fatal(err) } sa, _ := app.DB.GetSenderByID(saID) sb, _ := app.DB.GetSenderByID(sbID) if _, err := app.DKIM.GenerateDKIMKeypair("tenant-a.example", "", false); err != nil { t.Fatal(err) } if _, err := app.DKIM.GenerateDKIMKeypair("tenant-b.example", "", false); err != nil { t.Fatal(err) } return *da, *db_, sa, sb } func TestScopedAdminOnlySeesOwnDomainInList(t *testing.T) { app := newTestApp(t) mux := app.Mux() domainA, domainB, _, _ := setupTwoTenants(t, app) cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID}) req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) body := rec.Body.String() if !strings.Contains(body, domainA.DomainName) { t.Error("scoped admin's own domain should appear in the domains list") } if strings.Contains(body, domainB.DomainName) { t.Error("scoped admin must NOT see a domain outside their assignment") } } func TestScopedAdminCannotAccessOtherTenantSenderByID(t *testing.T) { app := newTestApp(t) mux := app.Mux() domainA, _, _, senderB := setupTwoTenants(t, app) cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID}) // Direct URL access to a sender belonging to a domain they don't manage. req := httptest.NewRequest(http.MethodGet, Prefix+"/senders/"+strconv.FormatInt(senderB.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 sender outside scope, got %d", rec.Code) } // Same for the mutating route — must not be able to disable it either. form := url.Values{} req2 := httptest.NewRequest(http.MethodPost, Prefix+"/senders/"+strconv.FormatInt(senderB.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 sender outside scope, got %d", rec2.Code) } stillActive, err := app.DB.GetSenderByID(senderB.ID) if err != nil || stillActive == nil || !stillActive.IsActive { t.Fatal("sender outside scope must not have been modified") } } func TestScopedAdminCannotCreateSenderOnUnownedDomain(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+"/senders/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 sender on an unowned domain, got %d", rec.Code) } if s, _ := app.DB.GetSenderByEmail("mallory@" + domainB.DomainName); s != nil { t.Fatal("sender must not have been created on a domain outside the admin's scope") } } func TestNewDomainAutoGrantedToScopedCreator(t *testing.T) { app := newTestApp(t) mux := app.Mux() domainA, _, _, _ := setupTwoTenants(t, app) cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID}) userID, err := app.DB.GetAdminUserByUsername("tenant-a-admin") if err != nil || userID == nil { t.Fatal("expected the scoped admin to exist") } form := url.Values{"domain_name": {"brand-new.example"}} req := httptest.NewRequest(http.MethodPost, Prefix+"/domains/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 domain, got %d: %s", rec.Code, rec.Body.String()) } newDomain, err := app.DB.GetDomainByNameExact("brand-new.example") if err != nil || newDomain == nil { t.Fatalf("expected domain to be created: %v", err) } ids, err := app.DB.AccessibleDomainIDs(userID.ID) if err != nil { t.Fatal(err) } found := false for _, id := range ids { if id == newDomain.ID { found = true } } if !found { t.Fatal("a scoped admin who creates a domain must automatically get access to it") } } func TestScopedAdminCannotDelegateUnownedDomain(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{ "username": {"sub-admin"}, "password": {"sub-admin-password-1!"}, "domain_ids": {strconv.FormatInt(domainA.ID, 10), strconv.FormatInt(domainB.ID, 10)}, // B isn't theirs } req := httptest.NewRequest(http.MethodPost, Prefix+"/admins/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 (with an error flash), got %d", rec.Code) } if u, _ := app.DB.GetAdminUserByUsername("sub-admin"); u != nil { t.Fatal("admin creation must be rejected outright when it tries to delegate a domain outside the creator's own scope") } } func TestScopedAdminCanDelegateOwnedDomainAndManageResultingAdmin(t *testing.T) { app := newTestApp(t) mux := app.Mux() domainA, _, _, _ := setupTwoTenants(t, app) cookie := scopedLogin(t, app, "tenant-a-admin", []int64{domainA.ID}) form := url.Values{ "username": {"sub-admin"}, "password": {"sub-admin-password-1!"}, "domain_ids": {strconv.FormatInt(domainA.ID, 10)}, } req := httptest.NewRequest(http.MethodPost, Prefix+"/admins/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 delegating an owned domain, got %d: %s", rec.Code, rec.Body.String()) } subAdmin, err := app.DB.GetAdminUserByUsername("sub-admin") if err != nil || subAdmin == nil { t.Fatalf("expected sub-admin to be created: %v", err) } // The delegating admin must be able to see and manage the new sub-admin, per the // "any admin within scope, not just ones I personally created" rule. listReq := httptest.NewRequest(http.MethodGet, Prefix+"/admins", nil) listReq.AddCookie(cookie) listRec := httptest.NewRecorder() mux.ServeHTTP(listRec, listReq) if !strings.Contains(listRec.Body.String(), "sub-admin") { t.Fatal("delegating admin should see the newly created sub-admin in their admin list") } } func TestGlobalAdminSeesEverything(t *testing.T) { app := newTestApp(t) mux := app.Mux() domainA, domainB, _, _ := setupTwoTenants(t, app) cookie := loginSession(t, app) // global admin req := httptest.NewRequest(http.MethodGet, Prefix+"/domains", nil) req.AddCookie(cookie) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) body := rec.Body.String() if !strings.Contains(body, domainA.DomainName) || !strings.Contains(body, domainB.DomainName) { t.Fatal("global admin must see every domain regardless of scoped assignments") } }