first commit
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user