Compare commits

...

2 Commits

Author SHA1 Message Date
548681face Improved API docs, removed DELETE endpoint from user 2026-01-26 13:42:08 +00:00
c115f41dac Switched to Redoc 2026-01-26 13:31:08 +00:00
13 changed files with 210 additions and 60 deletions

1
agent/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
keywarden-agent

View File

@@ -1,7 +1,7 @@
import inspect import inspect
from typing import List, Optional from typing import List, Optional
from ninja import NinjaAPI, Router, Schema from ninja import NinjaAPI, Router, Schema, Redoc
from ninja.security import django_auth from ninja.security import django_auth
from .security import JWTAuth from .security import JWTAuth
@@ -15,17 +15,18 @@ from .routers.access import build_router as build_access_router
from .routers.telemetry import build_router as build_telemetry_router from .routers.telemetry import build_router as build_telemetry_router
from .routers.agent import build_router as build_agent_router from .routers.agent import build_router as build_agent_router
from django.contrib.admin.views.decorators import staff_member_required
def register_routers(target_api: NinjaAPI) -> None: def register_routers(target_api: NinjaAPI) -> None:
target_api.add_router("/system", build_system_router(), tags=["system"]) target_api.add_router("/system", build_system_router(), tags=["System"])
target_api.add_router("/user", build_accounts_router(), tags=["user"]) target_api.add_router("/user", build_accounts_router(), tags=["Account Context"])
target_api.add_router("/audit", build_audit_router(), tags=["audit"]) target_api.add_router("/audit", build_audit_router(), tags=["Audit Logging"])
target_api.add_router("/servers", build_servers_router(), tags=["servers"]) target_api.add_router("/servers", build_servers_router(), tags=["Servers"])
target_api.add_router("/users", build_users_router(), tags=["users"]) target_api.add_router("/users", build_users_router(), tags=["User Directory"])
target_api.add_router("/keys", build_keys_router(), tags=["keys"]) target_api.add_router("/keys", build_keys_router(), tags=["SSH Keys"])
target_api.add_router("/access-requests", build_access_router(), tags=["access"]) target_api.add_router("/access-requests", build_access_router(), tags=["Access Requests"])
target_api.add_router("/telemetry", build_telemetry_router(), tags=["telemetry"]) target_api.add_router("/telemetry", build_telemetry_router(), tags=["Telemetry"])
target_api.add_router("/agent", build_agent_router(), tags=["agent"]) target_api.add_router("/agent", build_agent_router(), tags=["Agent"])
def build_api(**kwargs) -> NinjaAPI: def build_api(**kwargs) -> NinjaAPI:
@@ -39,6 +40,8 @@ api = build_api(
version="1.0.0", version="1.0.0",
description="Authenticated API for internal app use and external clients.", description="Authenticated API for internal app use and external clients.",
auth=[django_auth, JWTAuth()], auth=[django_auth, JWTAuth()],
docs=Redoc(),
docs_decorator=staff_member_required,
) )
register_routers(api) register_routers(api)
@@ -48,5 +51,7 @@ api_v1 = build_api(
description="Authenticated API for internal app use and external clients.", description="Authenticated API for internal app use and external clients.",
auth=[django_auth, JWTAuth()], auth=[django_auth, JWTAuth()],
urls_namespace="api-v1", urls_namespace="api-v1",
docs=Redoc(),
docs_decorator=staff_member_required,
) )
register_routers(api_v1) register_routers(api_v1)

View File

@@ -78,6 +78,7 @@ def build_router() -> Router:
- If user has global `access.view_accessrequest`, returns all requests. - If user has global `access.view_accessrequest`, returns all requests.
- Otherwise, returns only objects with `access.view_accessrequest` object permission. - Otherwise, returns only objects with `access.view_accessrequest` object permission.
Filters: status, server_id, requester_id (requester_id is honored only with global view). Filters: status, server_id, requester_id (requester_id is honored only with global view).
Rationale: powers the access request queue and auditing views.
""" """
require_authenticated(request) require_authenticated(request)
user = request.user user = request.user
@@ -107,6 +108,9 @@ def build_router() -> Router:
Auth: required. Auth: required.
Permissions: requires global `access.add_accessrequest`. Permissions: requires global `access.add_accessrequest`.
Side effects: grants owner object perms on the new request. Side effects: grants owner object perms on the new request.
Behavior: creates a pending access request; it does not grant access
until approved. Optional expires_at defines the requested access window.
Rationale: this is the entry point for delegating server access.
""" """
require_authenticated(request) require_authenticated(request)
if not request.user.has_perm("access.add_accessrequest"): if not request.user.has_perm("access.add_accessrequest"):
@@ -133,6 +137,7 @@ def build_router() -> Router:
Auth: required. Auth: required.
Permissions: requires `access.view_accessrequest` on the object. Permissions: requires `access.view_accessrequest` on the object.
Rationale: used for request detail views and approval workflows.
""" """
require_authenticated(request) require_authenticated(request)
try: try:
@@ -153,6 +158,9 @@ def build_router() -> Router:
- Admin/operator (global change) can set status to approved/denied/revoked/cancelled and - Admin/operator (global change) can set status to approved/denied/revoked/cancelled and
update expires_at. update expires_at.
- Non-admin can only set status to cancelled, and only while pending. - Non-admin can only set status to cancelled, and only while pending.
Side effects: updates object permissions for server visibility when
approvals or revocations occur.
Rationale: this is the core approval/denial path for access control.
""" """
require_authenticated(request) require_authenticated(request)
try: try:
@@ -195,24 +203,6 @@ def build_router() -> Router:
sync_server_view_perm(access_request) sync_server_view_perm(access_request)
return _request_to_out(access_request) return _request_to_out(access_request)
@router.delete("/{request_id}", response={204: None})
def delete_request(request: HttpRequest, request_id: int):
"""Delete an access request.
Auth: required.
Permissions: requires `access.delete_accessrequest` on the object.
"""
require_authenticated(request)
try:
access_request = AccessRequest.objects.get(id=request_id)
except AccessRequest.DoesNotExist:
raise HttpError(404, "Not Found")
if not request.user.has_perm("access.delete_accessrequest", access_request):
raise HttpError(403, "Forbidden")
access_request.delete()
sync_server_view_perm(access_request)
return 204, None
return router return router

View File

@@ -20,7 +20,14 @@ def build_router() -> Router:
@router.get("/me", response=UserSchema) @router.get("/me", response=UserSchema)
def me(request: HttpRequest): def me(request: HttpRequest):
"""Return the current authenticated user's profile.""" """Return the authenticated user's profile and role context.
Auth: required (session or JWT). Used by the UI to build navigation,
display the user identity, and decide which actions are enabled.
Fields: returns only the minimal identity and privilege flags needed
by the client; no secrets or permissions lists are exposed here.
Rationale: keeps the client-side state aligned with the session user.
"""
require_authenticated(request) require_authenticated(request)
user = request.user user = request.user
return { return {

View File

@@ -88,7 +88,18 @@ def build_router() -> Router:
@router.post("/enroll", response=AgentEnrollOut, auth=None) @router.post("/enroll", response=AgentEnrollOut, auth=None)
@csrf_exempt @csrf_exempt
def enroll_agent(request: HttpRequest, payload: AgentEnrollIn = Body(...)): def enroll_agent(request: HttpRequest, payload: AgentEnrollIn = Body(...)):
"""Enroll a server agent using a one-time token.""" """Enroll a server agent using a one-time enrollment token.
Auth: token only (no session/JWT); mTLS is not yet available until
enrollment completes.
Inputs: enrollment token + CSR from the agent, optional host/IP hints.
Behavior:
- Creates a Server record (agent is the source of truth for host/IP).
- Marks the token as used (single-use).
- Signs the CSR with the active Agent CA and returns client cert + CA.
Rationale: this is the only supported server onboarding flow. If this
endpoint is removed, agents cannot bootstrap mTLS credentials.
"""
token_value = (payload.token or "").strip() token_value = (payload.token or "").strip()
if not token_value: if not token_value:
raise HttpError(422, "Token required") raise HttpError(422, "Token required")
@@ -138,7 +149,14 @@ def build_router() -> Router:
@router.get("/servers/{server_id}/authorized-keys", response=List[AuthorizedKeyOut]) @router.get("/servers/{server_id}/authorized-keys", response=List[AuthorizedKeyOut])
def authorized_keys(request: HttpRequest, server_id: int): def authorized_keys(request: HttpRequest, server_id: int):
"""Return authorized public keys for a server (admin or operator).""" """Resolve the effective authorized_keys list for a server.
Auth: required (admin/operator via API).
Permissions: requires view access to servers, keys, and access requests.
Behavior: combines approved access requests with active SSH keys to
produce the exact key list the agent should deploy to the server.
Rationale: this is the policy enforcement point for per-user access.
"""
require_perms( require_perms(
request, request,
"servers.view_server", "servers.view_server",
@@ -175,7 +193,13 @@ def build_router() -> Router:
@router.post("/servers/{server_id}/sync-report", response=SyncReportOut, auth=None) @router.post("/servers/{server_id}/sync-report", response=SyncReportOut, auth=None)
@csrf_exempt @csrf_exempt
def sync_report(request: HttpRequest, server_id: int, payload: SyncReportIn = Body(...)): def sync_report(request: HttpRequest, server_id: int, payload: SyncReportIn = Body(...)):
"""Record an agent sync report for a server (admin or operator).""" """Record an agent sync report for a server.
Auth: mTLS expected at the edge (no session/JWT).
Behavior: stores a telemetry event with counts of applied/revoked keys.
Rationale: provides an audit trail of enforcement actions without
requiring full log ingestion for every sync cycle.
"""
try: try:
server = Server.objects.get(id=server_id) server = Server.objects.get(id=server_id)
except Server.DoesNotExist: except Server.DoesNotExist:
@@ -197,7 +221,14 @@ def build_router() -> Router:
@router.post("/servers/{server_id}/logs", response=LogIngestOut, auth=None) @router.post("/servers/{server_id}/logs", response=LogIngestOut, auth=None)
@csrf_exempt @csrf_exempt
def ingest_logs(request: HttpRequest, server_id: int, payload: List[LogEventIn] = Body(...)): def ingest_logs(request: HttpRequest, server_id: int, payload: List[LogEventIn] = Body(...)):
"""Accept log batches from agents (mTLS required at the edge).""" """Accept log batches from agents for audit collection.
Auth: mTLS expected at the edge (no session/JWT).
Behavior: accepts structured log events for later storage and indexing.
Storage: raw logs are persisted separately per-server (SQLite shards),
not in the primary Postgres database.
Rationale: this is the ingestion pipe for security audit logging.
"""
try: try:
Server.objects.get(id=server_id) Server.objects.get(id=server_id)
except Server.DoesNotExist: except Server.DoesNotExist:
@@ -208,7 +239,13 @@ def build_router() -> Router:
@router.post("/servers/{server_id}/heartbeat", response=SyncReportOut, auth=None) @router.post("/servers/{server_id}/heartbeat", response=SyncReportOut, auth=None)
@csrf_exempt @csrf_exempt
def heartbeat(request: HttpRequest, server_id: int, payload: AgentHeartbeatIn = Body(...)): def heartbeat(request: HttpRequest, server_id: int, payload: AgentHeartbeatIn = Body(...)):
"""Update server host metadata (mTLS required at the edge).""" """Update server host metadata (hostname/IPs) reported by the agent.
Auth: mTLS expected at the edge (no session/JWT).
Behavior: updates hostname/IPv4/IPv6 when they change (e.g., DHCP).
Conflict: unique constraints are enforced; conflicts return 409.
Rationale: keeps the server inventory accurate without manual edits.
"""
try: try:
server = Server.objects.get(id=server_id) server = Server.objects.get(id=server_id)
except Server.DoesNotExist: except Server.DoesNotExist:

View File

@@ -47,7 +47,14 @@ def build_router() -> Router:
@router.get("/event-types", response=List[AuditEventTypeSchema]) @router.get("/event-types", response=List[AuditEventTypeSchema])
def list_event_types(request: HttpRequest): def list_event_types(request: HttpRequest):
"""List audit event types and their default severity.""" """List audit event types used by the platform audit log.
Auth: required.
Permissions: requires global `audit.view_auditeventtype`.
Behavior: returns the canonical event taxonomy (key, title, severity).
Rationale: the admin UI and audit filters use this to map log entries
to human-readable categories and severity defaults.
"""
require_perms(request, "audit.view_auditeventtype") require_perms(request, "audit.view_auditeventtype")
qs: QuerySet[AuditEventType] = AuditEventType.objects.all() qs: QuerySet[AuditEventType] = AuditEventType.objects.all()
return [ return [
@@ -63,7 +70,16 @@ def build_router() -> Router:
@router.get("/logs", response=List[AuditLogSchema]) @router.get("/logs", response=List[AuditLogSchema])
def list_logs(request: HttpRequest, filters: LogsQuery = Query(...)): def list_logs(request: HttpRequest, filters: LogsQuery = Query(...)):
"""List audit logs with optional filters and pagination.""" """List application audit log entries with filters and pagination.
Auth: required.
Permissions: requires global `audit.view_auditlog`.
Filters: severity, actor_id, event_type_key, source.
Pagination: limit + offset.
Scope: this is the Keywarden app audit trail (who changed what), not
the server OS log ingestion stream stored by the agent.
Rationale: used by the audit UI and for administrative forensics.
"""
require_perms(request, "audit.view_auditlog") require_perms(request, "audit.view_auditlog")
qs: QuerySet[AuditLog] = AuditLog.objects.select_related("event_type", "actor").all() qs: QuerySet[AuditLog] = AuditLog.objects.select_related("event_type", "actor").all()
if filters.severity: if filters.severity:

View File

@@ -76,6 +76,7 @@ def build_router() -> Router:
- If user has global `keys.view_sshkey`, returns all keys. - If user has global `keys.view_sshkey`, returns all keys.
- Otherwise, returns only objects with `keys.view_sshkey` object permission. - Otherwise, returns only objects with `keys.view_sshkey` object permission.
Filter: user_id (honored only with global view). Filter: user_id (honored only with global view).
Rationale: powers the key inventory UI and lets admins audit key usage.
""" """
require_authenticated(request) require_authenticated(request)
user = request.user user = request.user
@@ -104,6 +105,7 @@ def build_router() -> Router:
- Default owner is the current user. - Default owner is the current user.
- If caller has global `keys.add_sshkey` and `keys.view_sshkey`, they may specify user_id. - If caller has global `keys.add_sshkey` and `keys.view_sshkey`, they may specify user_id.
Side effects: grants owner object perms on the new key. Side effects: grants owner object perms on the new key.
Rationale: keys are the core authorization material synced to servers.
""" """
require_authenticated(request) require_authenticated(request)
if not request.user.has_perm("keys.add_sshkey"): if not request.user.has_perm("keys.add_sshkey"):
@@ -140,6 +142,7 @@ def build_router() -> Router:
Auth: required. Auth: required.
Permissions: requires `keys.view_sshkey` on the object. Permissions: requires `keys.view_sshkey` on the object.
Rationale: used by key detail views and server access debugging.
""" """
require_authenticated(request) require_authenticated(request)
try: try:
@@ -156,6 +159,7 @@ def build_router() -> Router:
Auth: required. Auth: required.
Permissions: requires `keys.change_sshkey` on the object. Permissions: requires `keys.change_sshkey` on the object.
Rationale: allows key rotation and revocation without deletion.
""" """
require_authenticated(request) require_authenticated(request)
try: try:
@@ -187,6 +191,7 @@ def build_router() -> Router:
Auth: required. Auth: required.
Permissions: requires `keys.delete_sshkey` on the object. Permissions: requires `keys.delete_sshkey` on the object.
Behavior: sets is_active false and revoked_at if key is active. Behavior: sets is_active false and revoked_at if key is active.
Rationale: removes key access while preserving auditability.
""" """
require_authenticated(request) require_authenticated(request)
try: try:

View File

@@ -29,7 +29,13 @@ def build_router() -> Router:
@router.get("/", response=List[ServerOut]) @router.get("/", response=List[ServerOut])
def list_servers(request: HttpRequest): def list_servers(request: HttpRequest):
"""List servers visible to authenticated users.""" """List servers the caller can view.
Auth: required.
Permissions: requires `servers.view_server` globally or per-object.
Behavior: returns only servers the user can see via object perms.
Rationale: drives the server dashboard and access-aware navigation.
"""
require_perms(request, "servers.view_server") require_perms(request, "servers.view_server")
if request.user.has_perm("servers.view_server"): if request.user.has_perm("servers.view_server"):
servers = Server.objects.all() servers = Server.objects.all()
@@ -55,7 +61,13 @@ def build_router() -> Router:
@router.get("/{server_id}", response=ServerOut) @router.get("/{server_id}", response=ServerOut)
def get_server(request: HttpRequest, server_id: int): def get_server(request: HttpRequest, server_id: int):
"""Get server details by id.""" """Get a server record by id.
Auth: required.
Permissions: requires `servers.view_server` globally or per-object.
Rationale: used by server detail views and API clients inspecting
server metadata (hostname/IPs populated by the agent).
"""
require_perms(request, "servers.view_server") require_perms(request, "servers.view_server")
try: try:
server = Server.objects.get(id=server_id) server = Server.objects.get(id=server_id)
@@ -77,7 +89,14 @@ def build_router() -> Router:
@router.patch("/{server_id}", response=ServerOut) @router.patch("/{server_id}", response=ServerOut)
def update_server(request: HttpRequest, server_id: int, payload: ServerUpdate): def update_server(request: HttpRequest, server_id: int, payload: ServerUpdate):
"""Update server display name (admin only).""" """Update the server display name (admin only).
Auth: required.
Permissions: requires `servers.change_server`.
Behavior: only display_name is editable via API; host/IP data is owned
by the agent heartbeat to avoid conflicting sources of truth.
Rationale: allows human-friendly naming without bypassing enrollment.
"""
require_perms(request, "servers.change_server") require_perms(request, "servers.change_server")
if payload.display_name is None: if payload.display_name is None:
raise HttpError(422, {"detail": "No fields provided."}) raise HttpError(422, {"detail": "No fields provided."})

View File

@@ -14,7 +14,14 @@ def build_router() -> Router:
@router.get("/health", response=HealthResponse) @router.get("/health", response=HealthResponse)
def health(request) -> HealthResponse: def health(request) -> HealthResponse:
"""Health check endpoint for service monitoring.""" """Return application liveness for internal monitoring.
Auth: required (session or JWT). This is intentionally protected to avoid
exposing internal status to unauthenticated callers.
Behavior: returns a static {"status": "ok"} if the app stack is reachable.
Rationale: used by uptime checks and deployments to confirm the API
process is running and can authenticate requests.
"""
require_authenticated(request) require_authenticated(request)
return {"status": "ok"} return {"status": "ok"}

View File

@@ -71,7 +71,13 @@ def build_router() -> Router:
@router.get("/", response=List[TelemetryOut]) @router.get("/", response=List[TelemetryOut])
def list_events(request: HttpRequest, filters: TelemetryQuery = Query(...)): def list_events(request: HttpRequest, filters: TelemetryQuery = Query(...)):
"""List telemetry events with filters (admin or operator).""" """List telemetry events emitted by the platform and agents.
Auth: required.
Permissions: requires `telemetry.view_telemetryevent`.
Filters: event_type, server_id, user_id, success.
Rationale: supports operational dashboards and audit-style timelines.
"""
require_perms(request, "telemetry.view_telemetryevent") require_perms(request, "telemetry.view_telemetryevent")
qs = TelemetryEvent.objects.order_by("-created_at") qs = TelemetryEvent.objects.order_by("-created_at")
if filters.event_type: if filters.event_type:
@@ -87,7 +93,14 @@ def build_router() -> Router:
@router.post("/", response=TelemetryOut) @router.post("/", response=TelemetryOut)
def create_event(request: HttpRequest, payload: TelemetryCreateIn): def create_event(request: HttpRequest, payload: TelemetryCreateIn):
"""Create a telemetry event entry (admin or operator).""" """Create a telemetry event entry.
Auth: required.
Permissions: requires `telemetry.add_telemetryevent`.
Behavior: validates server/user references and normalizes source.
Rationale: used by internal automation; if external clients are not
expected to emit telemetry, this endpoint can be restricted further.
"""
require_perms(request, "telemetry.add_telemetryevent") require_perms(request, "telemetry.add_telemetryevent")
server = None server = None
if payload.server_id: if payload.server_id:
@@ -115,7 +128,12 @@ def build_router() -> Router:
@router.get("/summary", response=TelemetrySummaryOut) @router.get("/summary", response=TelemetrySummaryOut)
def summary(request: HttpRequest): def summary(request: HttpRequest):
"""Return a high-level telemetry summary (admin or operator).""" """Return a high-level success/failure summary.
Auth: required.
Permissions: requires `telemetry.view_telemetryevent`.
Rationale: feeds dashboard widgets without pulling full event lists.
"""
require_perms(request, "telemetry.view_telemetryevent") require_perms(request, "telemetry.view_telemetryevent")
totals = TelemetryEvent.objects.aggregate( totals = TelemetryEvent.objects.aggregate(
total=Count("id"), total=Count("id"),

View File

@@ -53,7 +53,15 @@ def build_router() -> Router:
@router.post("/", response=UserDetailOut) @router.post("/", response=UserDetailOut)
def create_user(request: HttpRequest, payload: UserCreateIn): def create_user(request: HttpRequest, payload: UserCreateIn):
"""Create a user with role and password (admin or operator).""" """Create a platform user and assign a Keywarden role.
Auth: required.
Permissions: requires `auth.add_user` (admin/operator).
Behavior: uses email as username, hashes the password, and assigns a
role which maps to Keywarden group permissions.
Rationale: enables automation and external admin workflows; mirrors
the admin UI user creation flow.
"""
require_perms(request, "auth.add_user") require_perms(request, "auth.add_user")
User = get_user_model() User = get_user_model()
email = payload.email.strip().lower() email = payload.email.strip().lower()
@@ -79,7 +87,13 @@ def build_router() -> Router:
@router.get("/", response=List[UserListOut]) @router.get("/", response=List[UserListOut])
def list_users(request: HttpRequest, pagination: UsersQuery = Query(...)): def list_users(request: HttpRequest, pagination: UsersQuery = Query(...)):
"""List users with pagination (admin or operator).""" """List users for administrative visibility and access management.
Auth: required.
Permissions: requires `auth.view_user`.
Pagination: limit + offset.
Rationale: used by admin UI and automation to audit user access.
"""
require_perms(request, "auth.view_user") require_perms(request, "auth.view_user")
User = get_user_model() User = get_user_model()
qs = User.objects.order_by("id")[pagination.offset : pagination.offset + pagination.limit] qs = User.objects.order_by("id")[pagination.offset : pagination.offset + pagination.limit]
@@ -95,7 +109,12 @@ def build_router() -> Router:
@router.get("/{user_id}", response=UserDetailOut) @router.get("/{user_id}", response=UserDetailOut)
def get_user(request: HttpRequest, user_id: int): def get_user(request: HttpRequest, user_id: int):
"""Get user details by id (admin or operator).""" """Fetch a single user record for inspection.
Auth: required.
Permissions: requires `auth.view_user`.
Rationale: used by admin detail views and automation scripts.
"""
require_perms(request, "auth.view_user") require_perms(request, "auth.view_user")
User = get_user_model() User = get_user_model()
try: try:
@@ -111,7 +130,13 @@ def build_router() -> Router:
@router.patch("/{user_id}", response=UserDetailOut) @router.patch("/{user_id}", response=UserDetailOut)
def update_user(request: HttpRequest, user_id: int, payload: UserUpdateIn): def update_user(request: HttpRequest, user_id: int, payload: UserUpdateIn):
"""Update user fields such as role, email, or status (admin only).""" """Update user identity, role, password, or activation state.
Auth: required.
Permissions: requires `auth.change_user` (admin).
Side effects: role changes update Keywarden role/group mappings.
Rationale: required for role delegation and account lifecycle control.
"""
require_perms(request, "auth.change_user") require_perms(request, "auth.change_user")
if payload.email is None and payload.password is None and payload.role is None and payload.is_active is None: if payload.email is None and payload.password is None and payload.role is None and payload.is_active is None:
raise HttpError(422, {"detail": "No fields provided."}) raise HttpError(422, {"detail": "No fields provided."})
@@ -143,18 +168,6 @@ def build_router() -> Router:
"is_active": user.is_active, "is_active": user.is_active,
} }
@router.delete("/{user_id}", response={204: None})
def delete_user(request: HttpRequest, user_id: int):
"""Delete a user by id (admin only)."""
require_perms(request, "auth.delete_user")
User = get_user_model()
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
raise HttpError(404, "Not Found")
user.delete()
return 204, None
return router return router

View File

@@ -0,0 +1,32 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title|default:"Keywarden API" }}</title>
<link rel="stylesheet" href="{% static 'ninja/swagger-ui.css' %}">
<style>
.swagger-ui .opblock-summary {
flex-direction: row;
flex-wrap: nowrap;
}
.swagger-ui .opblock-summary-path {
max-width: none;
white-space: nowrap;
word-break: normal;
overflow-wrap: normal;
writing-mode: horizontal-tb;
}
.swagger-ui .opblock-summary-path a {
white-space: nowrap;
}
</style>
</head>
<body data-api-csrf="{{ add_csrf|yesno:'true,false' }}" data-csrf-token="{{ csrf_token }}">
<div id="swagger-ui"></div>
<script id="swagger-settings" type="application/json">{{ swagger_settings|safe }}</script>
<script src="{% static 'ninja/swagger-ui-bundle.js' %}"></script>
<script src="{% static 'ninja/swagger-ui-init.js' %}"></script>
</body>
</html>

View File

@@ -1,4 +1,4 @@
add_header Content-Security-Policy "default-src 'self'; font-src *;img-src * data:; script-src * 'unsafe-eval'; style-src * 'unsafe-inline'"; add_header Content-Security-Policy "default-src 'self'; font-src *;img-src * data:; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'";
add_header X-Frame-Options "SAMEORIGIN"; add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy "strict-origin"; add_header Referrer-Policy "strict-origin";