feat: compliance restricted to built-in org, RequireOrg middleware

- auth.RequireOrg() middleware rejects requests from wrong org
- Compliance routes require built-in org (COMPLIANCE_ADMIN_ORG env var)
- SuperAdmin users (built-in org) manage compliance for all customer orgs
- Customer data lives in liquidity org, admin access via built-in org
- Tests updated to use built-in org
This commit is contained in:
2026-04-03 14:22:51 -07:00
parent 1a99305992
commit 454b735593
3 changed files with 59 additions and 9 deletions
+16
View File
@@ -81,6 +81,22 @@ func Middleware(iamEndpoint string) func(http.Handler) http.Handler {
}
}
// RequireOrg returns middleware that rejects requests from users outside
// the specified org. Used to restrict compliance endpoints to built-in
// org (superadmin operators) and keep customer orgs separate.
func RequireOrg(allowedOrg string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
org := r.Header.Get("X-Org-Id")
if org != allowedOrg {
writeErr(w, http.StatusForbidden, "access restricted to "+allowedOrg+" org")
return
}
next.ServeHTTP(w, r)
})
}
}
// RequirePermission checks X-User-Roles or X-User-IsAdmin.
func RequirePermission(perm string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
+5 -5
View File
@@ -25,7 +25,7 @@ func doRequest(r chi.Router, method, path string, body interface{}) *httptest.Re
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-Id", "test-user-001")
req.Header.Set("X-Org-Id", "liquidity")
req.Header.Set("X-Org-Id", "built-in")
req.Header.Set("X-User-Email", "testadmin@")
req.Header.Set("X-User-Roles", "superadmin")
w := httptest.NewRecorder()
@@ -42,7 +42,7 @@ func doRequestAs(r chi.Router, method, path string, body interface{}, userID, ro
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-Id", userID)
req.Header.Set("X-Org-Id", "liquidity")
req.Header.Set("X-Org-Id", "built-in")
req.Header.Set("X-User-Roles", role)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
@@ -180,7 +180,7 @@ func TestKYCVerifyInvalidBody(t *testing.T) {
// Send a raw invalid JSON body.
req := httptest.NewRequest("POST", "/kyc/verify", bytes.NewBufferString("{invalid"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-Id", "test-user-001"); req.Header.Set("X-User-Roles", "superadmin")
req.Header.Set("X-User-Id", "test-user-001"); req.Header.Set("X-User-Roles", "superadmin"); req.Header.Set("X-Org-Id", "built-in")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
@@ -311,7 +311,7 @@ func TestPipelineCreateInvalidBody(t *testing.T) {
r, _ := newTestRouter()
req := httptest.NewRequest("POST", "/pipelines/", bytes.NewBufferString("not json"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-Id", "test-user-001"); req.Header.Set("X-User-Roles", "superadmin")
req.Header.Set("X-User-Id", "test-user-001"); req.Header.Set("X-User-Roles", "superadmin"); req.Header.Set("X-Org-Id", "built-in")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
@@ -3017,7 +3017,7 @@ func TestSecurityRBACUsesStoredPermissions(t *testing.T) {
req := httptest.NewRequest(method, path, &buf)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-User-Id", userID)
req.Header.Set("X-Org-Id", "liquidity")
req.Header.Set("X-Org-Id", "built-in")
req.Header.Set("X-User-Roles", role)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
+38 -4
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net"
"net/http"
"os"
"sort"
"strings"
"sync"
@@ -80,6 +81,14 @@ func NewRouter(store ComplianceStore, opts ...RouterOption) chi.Router {
})
})
// Compliance endpoints are restricted to the admin org (built-in).
// Customer orgs (liquidity, etc.) access the platform via the exchange app,
// not the compliance admin API. Configurable via COMPLIANCE_ADMIN_ORG env var.
adminOrg := os.Getenv("COMPLIANCE_ADMIN_ORG")
if adminOrg == "" {
adminOrg = "built-in"
}
// Health check — no auth required
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
@@ -88,11 +97,36 @@ func NewRouter(store ComplianceStore, opts ...RouterOption) chi.Router {
})
})
// All routes require IAM authentication via gateway headers.
// No internal admin JWT — the gateway is the only auth layer.
r.Group(func(r chi.Router) {
// Customer-facing self-service endpoints — any authenticated user, any org.
// Customers create their own onboarding sessions and applications.
r.Route("/sessions", func(r chi.Router) {
r.Get("/", guard("sessions", "read", onboard.handleListSessions))
r.Post("/", guard("sessions", "write", onboard.handleCreateSession))
r.Get("/{id}", guard("sessions", "read", onboard.handleGetSession))
r.Patch("/{id}", guard("sessions", "write", onboard.handleUpdateSession))
r.Get("/{id}/steps", guard("sessions", "read", onboard.handleGetSessionSteps))
r.Post("/{id}/steps", guard("sessions", "write", onboard.handleSaveSessionStep))
r.Post("/{id}/review", guard("sessions", "write", onboard.handleReviewSession))
r.Post("/{id}/activity", guard("sessions", "write", onboard.handleSessionActivity))
})
// KYC
r.Route("/applications", func(r chi.Router) {
r.Post("/", guard("applications", "write", apps.handleCreate))
r.Get("/lookup", guard("applications", "read", apps.handleGetByUser))
r.Get("/{id}", guard("applications", "read", apps.handleGet))
r.Post("/{id}/step/1", guard("applications", "write", apps.handleStep1))
r.Post("/{id}/step/2", guard("applications", "write", apps.handleStep2))
r.Post("/{id}/step/3", guard("applications", "write", apps.handleStep3))
r.Post("/{id}/step/4", guard("applications", "write", apps.handleStep4))
r.Post("/{id}/step/5", guard("applications", "write", apps.handleStep5))
r.Get("/{id}/documents", guard("applications", "read", apps.handleGetDocuments))
})
// Admin compliance routes — restricted to built-in org (superadmin).
r.Group(func(r chi.Router) {
r.Use(auth.RequireOrg(adminOrg))
// KYC (admin review)
r.Route("/kyc", func(r chi.Router) {
r.Post("/verify", rateLimitSensitive(guard("kyc", "write", kyc.handleVerify)))
r.Get("/", guard("kyc", "read", kyc.handleListByUser))