mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
124 lines
3.4 KiB
Go
124 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
)
|
|
|
|
func TestMultiTenantAPITokenRemainsScopedToIssuingOrg(t *testing.T) {
|
|
defer SetMultiTenantEnabled(false)
|
|
SetMultiTenantEnabled(true)
|
|
t.Setenv("PULSE_DEV", "true")
|
|
|
|
const rawToken = "org-bound-router-token-123.12345678"
|
|
|
|
dataDir := t.TempDir()
|
|
hashedPass, err := internalauth.HashPassword("super-secure-pass")
|
|
if err != nil {
|
|
t.Fatalf("hash password: %v", err)
|
|
}
|
|
|
|
record, err := config.NewAPITokenRecord(rawToken, "org-a-token", []string{config.ScopeMonitoringRead})
|
|
if err != nil {
|
|
t.Fatalf("create api token record: %v", err)
|
|
}
|
|
record.OrgID = "org-a"
|
|
|
|
cfg := &config.Config{
|
|
DataPath: dataDir,
|
|
ConfigPath: dataDir,
|
|
AuthUser: "admin",
|
|
AuthPass: hashedPass,
|
|
APITokens: []config.APITokenRecord{*record},
|
|
}
|
|
cfg.SortAPITokens()
|
|
|
|
mtp := config.NewMultiTenantPersistence(dataDir)
|
|
for _, orgID := range []string{"org-a", "org-b"} {
|
|
if err := mtp.SaveOrganization(&models.Organization{
|
|
ID: orgID,
|
|
DisplayName: strings.ToUpper(orgID),
|
|
OwnerUserID: "admin",
|
|
Members: []models.OrganizationMember{
|
|
{UserID: "admin", Role: models.OrgRoleOwner, AddedAt: time.Now().UTC()},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("save organization %s: %v", orgID, err)
|
|
}
|
|
}
|
|
|
|
router := newMultiTenantRouter(t, cfg)
|
|
server := httptest.NewServer(router.Handler())
|
|
t.Cleanup(server.Close)
|
|
|
|
jar, err := cookiejar.New(nil)
|
|
if err != nil {
|
|
t.Fatalf("create cookie jar: %v", err)
|
|
}
|
|
sessionClient := &http.Client{Jar: jar}
|
|
|
|
loginBody, err := json.Marshal(map[string]string{
|
|
"username": "admin",
|
|
"password": "super-secure-pass",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal login payload: %v", err)
|
|
}
|
|
|
|
loginReq, err := http.NewRequest(http.MethodPost, server.URL+"/api/login", bytes.NewReader(loginBody))
|
|
if err != nil {
|
|
t.Fatalf("create login request: %v", err)
|
|
}
|
|
loginReq.Header.Set("Content-Type", "application/json")
|
|
loginResp, err := sessionClient.Do(loginReq)
|
|
if err != nil {
|
|
t.Fatalf("login request failed: %v", err)
|
|
}
|
|
loginResp.Body.Close()
|
|
if loginResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 on login, got %d", loginResp.StatusCode)
|
|
}
|
|
|
|
assertConfigAccess := func(orgID string, wantStatus int) string {
|
|
t.Helper()
|
|
|
|
req, err := http.NewRequest(http.MethodGet, server.URL+"/api/config", nil)
|
|
if err != nil {
|
|
t.Fatalf("create config request: %v", err)
|
|
}
|
|
req.Header.Set("X-Pulse-Org-ID", orgID)
|
|
req.Header.Set("Authorization", "Bearer "+rawToken)
|
|
|
|
res, err := sessionClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("config request failed: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatalf("read config response: %v", err)
|
|
}
|
|
if res.StatusCode != wantStatus {
|
|
t.Fatalf("expected %d for org %s, got %d: %s", wantStatus, orgID, res.StatusCode, string(body))
|
|
}
|
|
return string(body)
|
|
}
|
|
|
|
assertConfigAccess("org-a", http.StatusOK)
|
|
body := assertConfigAccess("org-b", http.StatusForbidden)
|
|
if !strings.Contains(body, "access_denied") || !strings.Contains(body, "Token") {
|
|
t.Fatalf("expected cross-org denial payload to mention access_denied and token binding, got %q", body)
|
|
}
|
|
}
|