This commit is contained in:
seniorswe
2025-10-15 01:25:18 -04:00
parent 7f6166bf88
commit 798f1efd42
48 changed files with 153 additions and 407 deletions

View File

@@ -76,5 +76,5 @@ jobs:
- name: Build web client
working-directory: web-client
env:
NEXT_PUBLIC_SERVER_URL: http://localhost:5001
NEXT_PUBLIC_SERVER_URL: http://localhost:3001
run: npm run build

7
.gitignore vendored
View File

@@ -39,6 +39,8 @@ backend-services/proto/
**/*.pid
.DS_Store
**/.DS_Store
.idea/
.vscode/
# Cookie jars
cookies.txt
@@ -62,6 +64,7 @@ scripts/dedupe_docblocks.py
# Logs and runtime artifacts
*.log
**/*.log
pytest_backend_verbose.log
backend-services/platform-logs/
**/platform-logs/
backend-services/platform-logs/doorman.log
@@ -70,6 +73,10 @@ backend-services/platform-logs/doorman.log
backend-services/generated/
**/generated/
backend-services/proto/*.bin
backend-services/proto/*
backend-services/routes/proto/*
**/*memory_dump*.bin
**/*.bin
.production.env
.coverage
coverage_html/

View File

@@ -1,7 +1,7 @@
SHELL := /bin/bash
# Configurable envs (can override on CLI or set in backend-services/.env)
BASE_URL ?= http://localhost:5001
BASE_URL ?= http://localhost:3001
ADMIN_EMAIL ?= $(shell grep '^DOORMAN_ADMIN_EMAIL=' backend-services/.env 2>/dev/null | cut -d'=' -f2)
ADMIN_PASSWORD ?= $(shell grep '^DOORMAN_ADMIN_PASSWORD=' backend-services/.env 2>/dev/null | cut -d'=' -f2)
@@ -61,3 +61,23 @@ coverage-html:
coverage-all:
BASE_URL=$(BASE_URL) DOORMAN_ADMIN_EMAIL=$(ADMIN_EMAIL) DOORMAN_ADMIN_PASSWORD=$(ADMIN_PASSWORD) \
bash scripts/coverage_all.sh
.PHONY: clean clean-deep
# Remove common local build/test artifacts without touching dependencies
clean:
@echo "Cleaning caches and runtime artifacts..."
@find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
@find . -type d -name ".pytest_cache" -prune -exec rm -rf {} + || true
@find . -type f -name "*.py[co]" -delete || true
@find . -type f -name ".DS_Store" -delete || true
@rm -rf backend-services/platform-logs/*.log backend-services/doorman.pid doorman.pid uvicorn.pid || true
@rm -rf web-client/.next || true
@rm -f pytest_backend_verbose.log || true
@echo "Done."
# Deeper cleanup that also removes generated dev artifacts
clean-deep: clean
@echo "Removing generated dev artifacts..."
@rm -rf generated backend-services/generated || true
@echo "Done."

View File

@@ -20,8 +20,18 @@ A lightweight API gateway built for AI, REST, SOAP, GraphQL, and gRPC APIs. No s
## Features
Doorman supports user management, authentication, authorizaiton, dynamic routing, roles, groups, rate limiting, throttling, logging, redis caching, mongodb, and endpoint request payload validation. It allows you to manage REST, AI, SOAP, GraphQL, and gRPC APIs.
## Repository Structure
- `backend-services/`: Python gateway core, routes, services, tests, and runtime assets.
- `web-client/`: Next.js frontend (uses `next.config.mjs`).
- `docker/`: Entrypoint and container scripts.
- `user-docs/`: End-user documentation and guides.
- `scripts/`: Helper scripts (preflight, coverage, maintenance).
- `user-docs/examples/tests/`: Example tests previously at the repo root.
- `generated/`: Local dev artifacts (migrated into `backend-services/generated` at runtime).
- Dev shims moved to `scripts/test-shims/`: helper wrappers to run specific backend tests directly if desired.
## Launch With Docker
Ensure an env file exists at the repo root: `./.env` (use `./.env.example` as a reference). Keep this file outside the image and pass it at runtime. Note - this is set for development, update variables and hosts to reflect a production environment.
Ensure an env file exists at the repo root: `./.env` (use `./backend-services/.env.example` as a reference). Keep this file outside the image and pass it at runtime. Note - this is set for development, update variables and hosts to reflect a production environment.
### Quickstart
See commands below.

View File

@@ -175,8 +175,8 @@ LOG_FORMAT=plain
# LOGS_DIR=/path/to/logs
# App
PORT=5001
THREADS=4
PORT=3001
THREADS=1
DEV_RELOAD=false
# Production environment flag (enforces HTTPS checks)
ENV=development

View File

@@ -1,6 +1,6 @@
import os
BASE_URL = os.getenv('DOORMAN_BASE_URL', 'http://localhost:5001').rstrip('/')
BASE_URL = os.getenv('DOORMAN_BASE_URL', 'http://localhost:3001').rstrip('/')
ADMIN_EMAIL = os.getenv('DOORMAN_ADMIN_EMAIL', 'admin@doorman.dev')
ADMIN_PASSWORD = os.getenv('DOORMAN_ADMIN_PASSWORD')
if not ADMIN_PASSWORD:

View File

@@ -1,327 +0,0 @@
/**
* Doorman API Gateway - k6 Load Test
*
* Tests multiple scenarios to establish performance baseline:
* 1. Authentication (login)
* 2. REST Gateway (API proxying)
* 3. GraphQL Gateway
* 4. SOAP Gateway
* 5. Mixed workload
*
* Run with:
* k6 run load-tests/k6-load-test.js
*
* Generate HTML report:
* k6 run --out json=load-tests/results.json load-tests/k6-load-test.js
* k6 report load-tests/results.json --output load-tests/report.html
*/
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// Custom metrics
const authSuccessRate = new Rate('auth_success_rate');
const restGatewayLatency = new Trend('rest_gateway_latency');
const graphqlGatewayLatency = new Trend('graphql_gateway_latency');
const soapGatewayLatency = new Trend('soap_gateway_latency');
const errorCount = new Counter('error_count');
// Configuration
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8000';
const TEST_USERNAME = __ENV.TEST_USERNAME || 'admin';
const TEST_PASSWORD = __ENV.TEST_PASSWORD || 'change-me';
// Load test stages
export const options = {
scenarios: {
// Scenario 1: Smoke test (1 VU for 30s)
smoke: {
executor: 'constant-vus',
vus: 1,
duration: '30s',
tags: { scenario: 'smoke' },
exec: 'smokeTest',
},
// Scenario 2: Load test (ramp up to 50 VUs)
load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 10 }, // Ramp up to 10 VUs
{ duration: '3m', target: 10 }, // Stay at 10 VUs
{ duration: '1m', target: 50 }, // Ramp up to 50 VUs
{ duration: '3m', target: 50 }, // Stay at 50 VUs
{ duration: '1m', target: 0 }, // Ramp down to 0
],
tags: { scenario: 'load' },
exec: 'loadTest',
startTime: '30s', // Start after smoke test
},
// Scenario 3: Stress test (push to limits)
stress: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 100 }, // Ramp up to 100 VUs
{ duration: '5m', target: 100 }, // Stay at 100 VUs
{ duration: '2m', target: 200 }, // Push to 200 VUs
{ duration: '5m', target: 200 }, // Stay at 200 VUs
{ duration: '2m', target: 0 }, // Ramp down
],
tags: { scenario: 'stress' },
exec: 'stressTest',
startTime: '10m', // Start after load test
},
// Scenario 4: Spike test (sudden traffic spike)
spike: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '10s', target: 10 }, // Normal load
{ duration: '10s', target: 200 }, // Sudden spike
{ duration: '1m', target: 200 }, // Hold spike
{ duration: '10s', target: 10 }, // Back to normal
{ duration: '30s', target: 10 }, // Recovery period
],
tags: { scenario: 'spike' },
exec: 'spikeTest',
startTime: '26m', // Start after stress test
},
},
thresholds: {
// Overall thresholds
'http_req_duration': ['p(50)<100', 'p(95)<500', 'p(99)<1000'], // Latency targets
'http_req_failed': ['rate<0.05'], // Error rate < 5%
// Authentication thresholds
'auth_success_rate': ['rate>0.95'],
// Gateway-specific thresholds
'rest_gateway_latency': ['p(50)<150', 'p(95)<600', 'p(99)<1200'],
'graphql_gateway_latency': ['p(50)<200', 'p(95)<800', 'p(99)<1500'],
'soap_gateway_latency': ['p(50)<250', 'p(95)<1000', 'p(99)<2000'],
// Error threshold
'error_count': ['count<100'],
},
};
// Helper: Login and get auth token
function login() {
const loginPayload = JSON.stringify({
username: TEST_USERNAME,
password: TEST_PASSWORD,
});
const params = {
headers: {
'Content-Type': 'application/json',
},
};
const response = http.post(`${BASE_URL}/auth/authorization`, loginPayload, params);
const success = check(response, {
'login status is 200': (r) => r.status === 200,
'login has access_token_cookie': (r) => r.cookies.access_token_cookie !== undefined,
});
authSuccessRate.add(success);
if (success) {
return response.cookies.access_token_cookie[0].value;
}
errorCount.add(1);
return null;
}
// Helper: Make authenticated request
function makeAuthRequest(method, url, token, body = null) {
const params = {
headers: {
'Content-Type': 'application/json',
},
cookies: {
'access_token_cookie': token,
},
};
if (body) {
return http.request(method, url, JSON.stringify(body), params);
}
return http.request(method, url, null, params);
}
// Smoke Test: Basic functionality
export function smokeTest() {
group('Smoke Test - Basic Functionality', () => {
// Test 1: Health check
group('Health Check', () => {
const response = http.get(`${BASE_URL}/health`);
check(response, {
'health check status is 200': (r) => r.status === 200,
});
});
// Test 2: Login
group('Login', () => {
const token = login();
check(token, {
'login successful': (t) => t !== null,
});
});
sleep(1);
});
}
// Load Test: Realistic workload
export function loadTest() {
const token = login();
if (!token) {
errorCount.add(1);
return;
}
group('Load Test - Realistic Workload', () => {
// Test 1: List APIs
group('List APIs', () => {
const response = makeAuthRequest('GET', `${BASE_URL}/api/apis`, token);
check(response, {
'list apis status is 200': (r) => r.status === 200,
'list apis returns array': (r) => {
try {
const body = JSON.parse(r.body);
return Array.isArray(body);
} catch {
return false;
}
},
});
});
// Test 2: REST Gateway (simulate API call)
group('REST Gateway', () => {
const start = Date.now();
const response = http.get(`${BASE_URL}/myapi/v1/users`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
const duration = Date.now() - start;
restGatewayLatency.add(duration);
check(response, {
'rest gateway responds': (r) => r.status !== 0,
}) || errorCount.add(1);
});
// Test 3: List users
group('List Users', () => {
const response = makeAuthRequest('GET', `${BASE_URL}/user/users`, token);
check(response, {
'list users status is 200': (r) => r.status === 200,
});
});
sleep(1);
});
}
// Stress Test: Push system to limits
export function stressTest() {
const token = login();
if (!token) {
errorCount.add(1);
return;
}
group('Stress Test - High Load', () => {
// Rapid-fire requests
for (let i = 0; i < 5; i++) {
const response = makeAuthRequest('GET', `${BASE_URL}/api/apis`, token);
check(response, {
'stress test request succeeds': (r) => r.status === 200,
}) || errorCount.add(1);
}
sleep(0.5);
});
}
// Spike Test: Sudden traffic increase
export function spikeTest() {
const token = login();
if (!token) {
errorCount.add(1);
return;
}
group('Spike Test - Traffic Burst', () => {
// Burst of concurrent requests
const responses = http.batch([
['GET', `${BASE_URL}/api/apis`, null, { cookies: { 'access_token_cookie': token } }],
['GET', `${BASE_URL}/user/users`, null, { cookies: { 'access_token_cookie': token } }],
['GET', `${BASE_URL}/role/roles`, null, { cookies: { 'access_token_cookie': token } }],
['GET', `${BASE_URL}/group/groups`, null, { cookies: { 'access_token_cookie': token } }],
]);
responses.forEach((response) => {
check(response, {
'spike test request succeeds': (r) => r.status === 200,
}) || errorCount.add(1);
});
sleep(0.1);
});
}
// Summary handler
export function handleSummary(data) {
return {
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
'load-tests/k6-summary.json': JSON.stringify(data, null, 2),
};
}
// Text summary helper
function textSummary(data, options) {
const indent = options?.indent || '';
const enableColors = options?.enableColors || false;
const lines = [
'',
`${indent}===============================================`,
`${indent} Doorman API Gateway - Load Test Results`,
`${indent}===============================================`,
'',
`${indent}Scenarios:`,
`${indent} ✓ Smoke Test (1 VU, 30s)`,
`${indent} ✓ Load Test (0→10→50 VUs, 9m)`,
`${indent} ✓ Stress Test (0→100→200 VUs, 16m)`,
`${indent} ✓ Spike Test (10→200→10 VUs, 2m)`,
'',
`${indent}Key Metrics:`,
`${indent} HTTP Requests: ${data.metrics.http_reqs?.values?.count || 0}`,
`${indent} Failed Requests: ${data.metrics.http_req_failed?.values?.rate ? (data.metrics.http_req_failed.values.rate * 100).toFixed(2) : 0}%`,
`${indent} Auth Success Rate: ${data.metrics.auth_success_rate?.values?.rate ? (data.metrics.auth_success_rate.values.rate * 100).toFixed(2) : 0}%`,
'',
`${indent}Latency (p50/p95/p99):`,
`${indent} Overall: ${data.metrics.http_req_duration?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.http_req_duration?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.http_req_duration?.values?.['p(99)']?.toFixed(2) || 0}ms`,
`${indent} REST: ${data.metrics.rest_gateway_latency?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.rest_gateway_latency?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.rest_gateway_latency?.values?.['p(99)']?.toFixed(2) || 0}ms`,
`${indent} GraphQL: ${data.metrics.graphql_gateway_latency?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.graphql_gateway_latency?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.graphql_gateway_latency?.values?.['p(99)']?.toFixed(2) || 0}ms`,
`${indent} SOAP: ${data.metrics.soap_gateway_latency?.values?.['p(50)']?.toFixed(2) || 0}ms / ${data.metrics.soap_gateway_latency?.values?.['p(95)']?.toFixed(2) || 0}ms / ${data.metrics.soap_gateway_latency?.values?.['p(99)']?.toFixed(2) || 0}ms`,
'',
`${indent}===============================================`,
'',
];
return lines.join('\n');
}

View File

@@ -1,7 +1,7 @@
// k6 load test for /api/rest/* and /platform/* with thresholds and JUnit output
// Usage:
// k6 run k6/load.test.js \
// -e BASE_URL=http://localhost:5001 \
// k6 run load-tests/k6/load.test.js \
// -e BASE_URL=http://localhost:3001 \
// -e RPS=50 \
// -e DURATION=1m \
// -e REST_PATHS='["/api/rest/health"]' \
@@ -19,7 +19,7 @@ import http from 'k6/http'
import { check, sleep, group } from 'k6'
import { Trend, Rate, Counter } from 'k6/metrics'
const BASE_URL = __ENV.BASE_URL || 'http://localhost:5001'
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3001'
const DURATION = __ENV.DURATION || '1m'
const RPS = Number(__ENV.RPS || 20)
const REST_PATHS = (function () {
@@ -134,6 +134,7 @@ export function handleSummary (data) {
return {
'junit.xml': junitXml,
'summary.json': JSON.stringify(data, null, 2),
'load-tests/k6-summary.json': JSON.stringify(data, null, 2),
}
}
@@ -142,7 +143,6 @@ function escapeXml (s) {
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/\"/g, '&quot;')
.replace(/'/g, '&apos;')
}

View File

@@ -8,7 +8,7 @@ from urllib.parse import urljoin
import requests
def base_url() -> str:
return os.getenv('BASE_URL', 'http://localhost:5001').rstrip('/') + '/'
return os.getenv('BASE_URL', 'http://localhost:3001').rstrip('/') + '/'
def _csrf(sess: requests.Session) -> str | None:
for c in sess.cookies:
@@ -116,7 +116,7 @@ def do_rotate_admin(sess: requests.Session, args):
def main():
p = argparse.ArgumentParser(description='Doorman admin CLI')
p.add_argument('--base-url', default=os.getenv('BASE_URL'), help='Override base URL (default env BASE_URL or http://localhost:5001)')
p.add_argument('--base-url', default=os.getenv('BASE_URL'), help='Override base URL (default env BASE_URL or http://localhost:3001)')
p.add_argument('--email', default=os.getenv('DOORMAN_ADMIN_EMAIL', 'admin@doorman.dev'))
p.add_argument('--password', default=os.getenv('DOORMAN_ADMIN_PASSWORD'))
p.add_argument('-y', '--yes', action='store_true', help='Assume yes for safety prompts')

19
scripts/cleanup_repo.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[cleanup] Removing caches and runtime artifacts..."
find . -type d -name "__pycache__" -prune -exec rm -rf {} + || true
find . -type d -name ".pytest_cache" -prune -exec rm -rf {} + || true
find . -type f -name "*.py[co]" -delete || true
find . -type f -name ".DS_Store" -delete || true
rm -rf backend-services/platform-logs/*.log backend-services/doorman.pid doorman.pid uvicorn.pid || true
rm -rf web-client/.next || true
rm -f pytest_backend_verbose.log || true
if [[ "${1-}" == "--deep" ]]; then
echo "[cleanup] Removing generated dev artifacts..."
rm -rf generated backend-services/generated || true
fi
echo "[cleanup] Done."

View File

@@ -7,10 +7,10 @@ set -euo pipefail
# - Combines .coverage files and prints a report
#
# Env:
# BASE_URL (default http://localhost:5001)
# BASE_URL (default http://localhost:3001)
# DOORMAN_ADMIN_EMAIL, DOORMAN_ADMIN_PASSWORD (required for live tests)
BASE_URL="${BASE_URL:-http://localhost:5001}"
BASE_URL="${BASE_URL:-http://localhost:3001}"
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"

View File

@@ -3,11 +3,11 @@ set -euo pipefail
# One-time helper to force-clear stored allow_localhost_bypass=false via REST API.
# Env vars:
# - BASE_URL (default http://localhost:5001)
# - BASE_URL (default http://localhost:3001)
# - DOORMAN_ADMIN_EMAIL (required, no default)
# - DOORMAN_ADMIN_PASSWORD (required, no default)
BASE_URL="${BASE_URL:-http://localhost:5001}"
BASE_URL="${BASE_URL:-http://localhost:3001}"
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"

View File

@@ -7,13 +7,13 @@ set -euo pipefail
# - Optionally smokes REST/SOAP/GraphQL gateway if SMOKE_* upstream URLs are provided
#
# Env:
# BASE_URL (default http://localhost:5001)
# BASE_URL (default http://localhost:3001)
# DOORMAN_ADMIN_EMAIL, DOORMAN_ADMIN_PASSWORD (required)
# SMOKE_REST_UPSTREAM (optional, e.g., http://httpbin.org)
# SMOKE_SOAP_UPSTREAM (optional)
# SMOKE_GQL_UPSTREAM (optional)
BASE_URL="${BASE_URL:-http://localhost:5001}"
BASE_URL="${BASE_URL:-http://localhost:3001}"
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# Runs k6 load-tests/k6-load-test.js, captures CPU/loop-lag stats while running,
# Runs k6 load-tests/k6/load.test.js, captures CPU/loop-lag stats while running,
# and compares results against a baseline summary via scripts/compare_perf.py.
BASE_URL=${BASE_URL:-http://localhost:8000}
@@ -36,7 +36,7 @@ cleanup() {
trap cleanup EXIT INT TERM
echo "Running k6 load test..."
K6_CMD=(k6 run load-tests/k6-load-test.js --env BASE_URL="${BASE_URL}")
K6_CMD=(k6 run load-tests/k6/load.test.js --env BASE_URL="${BASE_URL}")
"${K6_CMD[@]}"
echo
@@ -52,4 +52,3 @@ fi
echo
echo "Comparing current vs baseline..."
python3 scripts/compare_perf.py "${CURRENT_JSON}" "${BASELINE_JSON}"

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:5001}"
BASE_URL="${BASE_URL:-http://localhost:3001}"
EMAIL="${DOORMAN_ADMIN_EMAIL:-}"
PASSWORD="${DOORMAN_ADMIN_PASSWORD:-}"

View File

@@ -0,0 +1,8 @@
These are optional developer shims that used to live at the repo root.
- `test_ip_filter_platform.py` and `test_platform_expanded.py` allow running select backend tests via their shim paths.
- `conftest.py` mirrors backend test fixtures for the shims.
- `sitecustomize.py` mirrors logging redaction behavior for ad-hoc runs; primary copies exist under `backend-services/` and `backend-services/tests/`.
They are not required for normal test execution. Use `make unit` or run `pytest` from `backend-services/`.

View File

@@ -1,27 +1,25 @@
"""
Root-level pytest configuration shim.
Pytest configuration shim for developer test shims.
Allows running tests from backend-services/tests using root-level nodeids
like `pytest -v test_ip_filter_platform.py::test_...` by dynamically
loading the backend-services test fixtures into this module's namespace.
Executes backend-services/tests/conftest.py and exposes its fixtures
in this module's namespace for the shim tests in this folder.
"""
import os
import sys
import runpy
def _path(*parts: str) -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), *parts))
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', *parts))
_BS_DIR = _path('backend-services')
if _BS_DIR not in sys.path:
sys.path.insert(0, _BS_DIR)
def _load_backend_services_conftest():
"""Execute backend-services/tests/conftest.py and copy its fixtures here.
This makes its fixtures visible to pytest when collecting root-level tests.
"""
def _load_backend_services_conftest():
bs_conftest = _path('backend-services', 'tests', 'conftest.py')
if not os.path.exists(bs_conftest):
return
@@ -32,5 +30,6 @@ def _load_backend_services_conftest():
continue
g.setdefault(k, v)
_load_backend_services_conftest()

View File

@@ -1,14 +1,18 @@
"""
Ensure the 'doorman.gateway' and 'doorman.logging' loggers have a redaction filter
attached, even when the application module isn't imported (e.g., unit tests that
only import logging). Python automatically imports sitecustomize at interpreter
startup when it's present on sys.path.
Optional dev sitecustomize to mirror logging redaction during ad-hoc runs.
Primary redaction hooks live under:
- backend-services/sitecustomize.py
- backend-services/tests/sitecustomize.py
Keeping this here avoids cluttering the repo root.
"""
import logging
import re
import sys
class _RedactFilter(logging.Filter):
PATTERNS = [
re.compile(r'(?i)(authorization\s*[:=]\s*)([^;\r\n]+)'),
@@ -31,6 +35,7 @@ class _RedactFilter(logging.Filter):
pass
return True
def _ensure_logger(name: str):
logger = logging.getLogger(name)
for h in logger.handlers:
@@ -41,6 +46,7 @@ def _ensure_logger(name: str):
h.addFilter(_RedactFilter())
logger.addHandler(h)
try:
_ensure_logger('doorman.gateway')
_ensure_logger('doorman.logging')

View File

@@ -1,22 +1,24 @@
"""
Root-level test shim for backend-services/tests/test_ip_filter_platform.py
Shim for backend-services/tests/test_ip_filter_platform.py
Enables running:
pytest -v test_ip_filter_platform.py::test_...
from the repository root.
Allows running from repo root:
pytest -v scripts/test-shims/test_ip_filter_platform.py::test_...
"""
import os
import sys
import runpy
def _path(*parts: str) -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), *parts))
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', *parts))
_BS_DIR = _path('backend-services')
if _BS_DIR not in sys.path:
sys.path.insert(0, _BS_DIR)
def _load_tests():
src = _path('backend-services', 'tests', 'test_ip_filter_platform.py')
if not os.path.exists(src):
@@ -28,5 +30,6 @@ def _load_tests():
continue
g.setdefault(k, v)
_load_tests()

View File

@@ -1,22 +1,24 @@
"""
Root-level test shim for backend-services/tests/test_platform_expanded.py
Shim for backend-services/tests/test_platform_expanded.py
Enables running:
pytest -v test_platform_expanded.py::test_security_and_memory_dump_restore
from the repository root.
Allows running from repo root:
pytest -v scripts/test-shims/test_platform_expanded.py::test_security_and_memory_dump_restore
"""
import os
import sys
import runpy
def _path(*parts: str) -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), *parts))
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', *parts))
_BS_DIR = _path('backend-services')
if _BS_DIR not in sys.path:
sys.path.insert(0, _BS_DIR)
def _load_tests():
src = _path('backend-services', 'tests', 'test_platform_expanded.py')
if not os.path.exists(src):
@@ -28,5 +30,6 @@ def _load_tests():
continue
g.setdefault(k, v)
_load_tests()

View File

@@ -25,7 +25,7 @@ docker compose up --build
```
**Services will be available at:**
- Backend API: `http://localhost:5001`
- Backend API: `http://localhost:3001`
- Web UI: `http://localhost:3000`
### 2. Configure Environment Variables
@@ -110,7 +110,7 @@ cd web-client
cp .env.local.example .env.local
# Edit .env.local and set:
# NEXT_PUBLIC_SERVER_URL=http://localhost:5001
# NEXT_PUBLIC_SERVER_URL=http://localhost:3001
npm ci
npm run dev # Development mode
@@ -131,11 +131,11 @@ export DOORMAN_ADMIN_PASSWORD="YourStrongPassword123!"
curl -s -c /tmp/doorman.cookies \
-H 'Content-Type: application/json' \
-d "{\"email\":\"$DOORMAN_ADMIN_EMAIL\",\"password\":\"$DOORMAN_ADMIN_PASSWORD\"}" \
http://localhost:5001/platform/authorization
http://localhost:3001/platform/authorization
# Check authentication status
curl -s -b /tmp/doorman.cookies \
http://localhost:5001/platform/authorization/status
http://localhost:3001/platform/authorization/status
```
### Via Web UI
@@ -153,7 +153,7 @@ Let's publish a simple REST API backed by httpbin for testing.
```bash
curl -s -b /tmp/doorman.cookies \
-H 'Content-Type: application/json' \
-X POST http://localhost:5001/platform/credit \
-X POST http://localhost:3001/platform/credit \
-d '{
"api_credit_group": "demo-api",
"api_key": "demo-secret-key-123",
@@ -175,7 +175,7 @@ curl -s -b /tmp/doorman.cookies \
```bash
curl -s -b /tmp/doorman.cookies \
-H 'Content-Type: application/json' \
-X POST http://localhost:5001/platform/api \
-X POST http://localhost:3001/platform/api \
-d '{
"api_name": "demo",
"api_version": "v1",
@@ -197,7 +197,7 @@ curl -s -b /tmp/doorman.cookies \
# GET endpoint
curl -s -b /tmp/doorman.cookies \
-H 'Content-Type: application/json' \
-X POST http://localhost:5001/platform/endpoint \
-X POST http://localhost:3001/platform/endpoint \
-d '{
"api_name": "demo",
"api_version": "v1",
@@ -209,7 +209,7 @@ curl -s -b /tmp/doorman.cookies \
# POST endpoint
curl -s -b /tmp/doorman.cookies \
-H 'Content-Type: application/json' \
-X POST http://localhost:5001/platform/endpoint \
-X POST http://localhost:3001/platform/endpoint \
-d '{
"api_name": "demo",
"api_version": "v1",
@@ -224,7 +224,7 @@ curl -s -b /tmp/doorman.cookies \
```bash
curl -s -b /tmp/doorman.cookies \
-H 'Content-Type: application/json' \
-X POST http://localhost:5001/platform/subscription/subscribe \
-X POST http://localhost:3001/platform/subscription/subscribe \
-d '{
"username": "admin",
"api_name": "demo",
@@ -237,13 +237,13 @@ curl -s -b /tmp/doorman.cookies \
```bash
# GET request
curl -s -b /tmp/doorman.cookies \
"http://localhost:5001/api/rest/demo/v1/get?test=123"
"http://localhost:3001/api/rest/demo/v1/get?test=123"
# POST request
curl -s -b /tmp/doorman.cookies \
-H 'Content-Type: application/json' \
-d '{"message": "Hello Doorman!"}' \
http://localhost:5001/api/rest/demo/v1/post
http://localhost:3001/api/rest/demo/v1/post
```
Doorman will automatically inject the `x-api-key` header to the upstream service!

View File

@@ -557,18 +557,18 @@ ALLOW_CREDENTIALS=True
Doorman includes comprehensive security test coverage:
**Test suites:**
- `tests/test_auth_csrf_https.py` - CSRF validation
- `tests/test_production_https_guard.py` - HTTPS enforcement
- `tests/test_ip_policy_allow_deny_cidr.py` - IP filtering
- `tests/test_security.py` - General security features
- `tests/test_request_id_and_logging_redaction.py` - Audit trail
- `backend-services/tests/test_auth_csrf_https.py` - CSRF validation
- `backend-services/tests/test_production_https_guard.py` - HTTPS enforcement
- `backend-services/tests/test_ip_policy_allow_deny_cidr.py` - IP filtering
- `backend-services/tests/test_security.py` - General security features
- `backend-services/tests/test_request_id_and_logging_redaction.py` - Audit trail
**Run security tests:**
```bash
cd backend-services
pytest tests/test_auth_csrf_https.py -v
pytest tests/test_production_https_guard.py -v
pytest tests/test_ip_policy_allow_deny_cidr.py -v
pytest backend-services/tests/test_auth_csrf_https.py -v
pytest backend-services/tests/test_production_https_guard.py -v
pytest backend-services/tests/test_ip_policy_allow_deny_cidr.py -v
```
---

View File

@@ -17,7 +17,7 @@ Throughout this guide, the **platform API** lives under `/platform/*` and the **
## Conventions
```bash
BASE=http://localhost:5001 # Backend URL
BASE=http://localhost:3001 # Backend URL
COOKIE=/tmp/doorman.cookies # Cookie jar path
```

View File

@@ -836,11 +836,11 @@ Import and tune thresholds to match your SLOs.
## Load Testing with k6
Use `k6/load.test.js` to validate performance and SLOs in CI:
Use `load-tests/k6/load.test.js` to validate performance and SLOs in CI:
```bash
k6 run k6/load.test.js \
-e BASE_URL=http://localhost:5001 \
k6 run load-tests/k6/load.test.js \
-e BASE_URL=http://localhost:3001 \
-e RPS=50 \
-e DURATION=2m \
-e REST_PATHS='["/api/rest/health"]' \

View File

@@ -429,7 +429,7 @@ Commandline helper for common maintenance actions with safety prompts.
```bash
# Environment (development defaults shown)
export BASE_URL=http://localhost:5001
export BASE_URL=http://localhost:3001
export DOORMAN_ADMIN_EMAIL=admin@doorman.dev
export DOORMAN_ADMIN_PASSWORD=your-admin-password

View File

@@ -53,7 +53,7 @@ Load Tests (k6 preferred, Locust optional)
Run against a running Doorman instance. k6 script writes a JSON summary; helper scripts compare against a baseline to detect regressions.
- k6 quick run (BASE_URL defaults to http://localhost:8000):
- k6 run load-tests/k6-load-test.js --env BASE_URL=http://localhost:8000
- k6 run load-tests/k6/load.test.js --env BASE_URL=http://localhost:8000
- End-to-end perf check with regression gating:
- BASE_URL=http://localhost:8000 bash scripts/run_perf_check.sh
@@ -82,4 +82,3 @@ Troubleshooting
- Auth: Admin password must match the one you started Doorman with.
- k6 missing: Install from https://k6.io/docs/get-started/installation/
- Baseline not found: Create one per the Load Tests section above.

View File

@@ -86,7 +86,7 @@ def test_jwt_algorithm_enforcement():
print()
print("4. Related files:")
print(" - utils/auth_util.py:94-99 (main auth)")
print(" - tests/test_redis_token_revocation_ha.py:48-52 (test code)")
print(" - backend-services/tests/test_redis_token_revocation_ha.py:48-52 (test code)")
print()
if __name__ == '__main__':

View File

@@ -25,7 +25,7 @@ def test_load_testing_implementation():
locations = [
{
'file': 'load-tests/k6-load-test.js',
'file': 'load-tests/k6/load.test.js',
'type': 'k6 Load Test',
'scenarios': 4,
'description': 'JavaScript-based load testing with k6'
@@ -253,16 +253,16 @@ def test_load_testing_implementation():
print("Running Tests:")
print()
print(" k6 - Basic Run:")
print(" k6 run load-tests/k6-load-test.js")
print(" k6 run load-tests/k6/load.test.js")
print()
print(" k6 - Custom Configuration:")
print(" k6 run --env BASE_URL=https://api.example.com \\")
print(" --env TEST_USERNAME=admin \\")
print(" --env TEST_PASSWORD=secret \\")
print(" load-tests/k6-load-test.js")
print(" load-tests/k6/load.test.js")
print()
print(" k6 - Generate JSON Results:")
print(" k6 run --out json=results.json load-tests/k6-load-test.js")
print(" k6 run --out json=results.json load-tests/k6/load.test.js")
print()
print(" Locust - Web UI Mode:")
print(" locust -f load-tests/locust-load-test.py \\")

View File

@@ -37,7 +37,7 @@
<pre>
# Example curl
curl -H "Authorization: Bearer ..." \
http://localhost:5001/api/rest/users/v1/list
http://localhost:3001/api/rest/users/v1/list
</pre>
<h3 id="servers">Servers</h3>
@@ -94,14 +94,14 @@ curl -H "Authorization: Bearer ..." \
# REST call through Doorman
curl -H "Authorization: Bearer &lt;token&gt;" \
-H "client-key: demo-client" \
http://localhost:5001/api/rest/users/v1/items/42
http://localhost:3001/api/rest/users/v1/items/42
# GraphQL
curl -H "Authorization: Bearer &lt;token&gt;" \
-H "X-API-Version: v1" \
-H "Content-Type: application/json" \
-d '{"query":"{ hello }"}' \
http://localhost:5001/api/graphql/example
http://localhost:3001/api/graphql/example
</pre>
</main>
</body>