mirror of
https://github.com/luxfi/broker.git
synced 2026-09-24 00:25:18 +00:00
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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user