Compare commits
2 Commits
69802f3ece
...
548681face
| Author | SHA1 | Date | |
|---|---|---|---|
| 548681face | |||
| c115f41dac |
1
agent/.gitignore
vendored
Normal file
1
agent/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
keywarden-agent
|
||||
@@ -1,7 +1,7 @@
|
||||
import inspect
|
||||
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 .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.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:
|
||||
target_api.add_router("/system", build_system_router(), tags=["system"])
|
||||
target_api.add_router("/user", build_accounts_router(), tags=["user"])
|
||||
target_api.add_router("/audit", build_audit_router(), tags=["audit"])
|
||||
target_api.add_router("/servers", build_servers_router(), tags=["servers"])
|
||||
target_api.add_router("/users", build_users_router(), tags=["users"])
|
||||
target_api.add_router("/keys", build_keys_router(), tags=["keys"])
|
||||
target_api.add_router("/access-requests", build_access_router(), tags=["access"])
|
||||
target_api.add_router("/telemetry", build_telemetry_router(), tags=["telemetry"])
|
||||
target_api.add_router("/agent", build_agent_router(), tags=["agent"])
|
||||
target_api.add_router("/system", build_system_router(), tags=["System"])
|
||||
target_api.add_router("/user", build_accounts_router(), tags=["Account Context"])
|
||||
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("/users", build_users_router(), tags=["User Directory"])
|
||||
target_api.add_router("/keys", build_keys_router(), tags=["SSH Keys"])
|
||||
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("/agent", build_agent_router(), tags=["Agent"])
|
||||
|
||||
|
||||
def build_api(**kwargs) -> NinjaAPI:
|
||||
@@ -39,6 +40,8 @@ api = build_api(
|
||||
version="1.0.0",
|
||||
description="Authenticated API for internal app use and external clients.",
|
||||
auth=[django_auth, JWTAuth()],
|
||||
docs=Redoc(),
|
||||
docs_decorator=staff_member_required,
|
||||
)
|
||||
register_routers(api)
|
||||
|
||||
@@ -48,5 +51,7 @@ api_v1 = build_api(
|
||||
description="Authenticated API for internal app use and external clients.",
|
||||
auth=[django_auth, JWTAuth()],
|
||||
urls_namespace="api-v1",
|
||||
docs=Redoc(),
|
||||
docs_decorator=staff_member_required,
|
||||
)
|
||||
register_routers(api_v1)
|
||||
|
||||
@@ -78,6 +78,7 @@ def build_router() -> Router:
|
||||
- If user has global `access.view_accessrequest`, returns all requests.
|
||||
- Otherwise, returns only objects with `access.view_accessrequest` object permission.
|
||||
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)
|
||||
user = request.user
|
||||
@@ -107,6 +108,9 @@ def build_router() -> Router:
|
||||
Auth: required.
|
||||
Permissions: requires global `access.add_accessrequest`.
|
||||
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)
|
||||
if not request.user.has_perm("access.add_accessrequest"):
|
||||
@@ -133,6 +137,7 @@ def build_router() -> Router:
|
||||
|
||||
Auth: required.
|
||||
Permissions: requires `access.view_accessrequest` on the object.
|
||||
Rationale: used for request detail views and approval workflows.
|
||||
"""
|
||||
require_authenticated(request)
|
||||
try:
|
||||
@@ -153,6 +158,9 @@ def build_router() -> Router:
|
||||
- Admin/operator (global change) can set status to approved/denied/revoked/cancelled and
|
||||
update expires_at.
|
||||
- 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)
|
||||
try:
|
||||
@@ -195,24 +203,6 @@ def build_router() -> Router:
|
||||
sync_server_view_perm(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
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,14 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/me", response=UserSchema)
|
||||
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)
|
||||
user = request.user
|
||||
return {
|
||||
|
||||
@@ -88,7 +88,18 @@ def build_router() -> Router:
|
||||
@router.post("/enroll", response=AgentEnrollOut, auth=None)
|
||||
@csrf_exempt
|
||||
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()
|
||||
if not token_value:
|
||||
raise HttpError(422, "Token required")
|
||||
@@ -138,7 +149,14 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/servers/{server_id}/authorized-keys", response=List[AuthorizedKeyOut])
|
||||
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(
|
||||
request,
|
||||
"servers.view_server",
|
||||
@@ -175,7 +193,13 @@ def build_router() -> Router:
|
||||
@router.post("/servers/{server_id}/sync-report", response=SyncReportOut, auth=None)
|
||||
@csrf_exempt
|
||||
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:
|
||||
server = Server.objects.get(id=server_id)
|
||||
except Server.DoesNotExist:
|
||||
@@ -197,7 +221,14 @@ def build_router() -> Router:
|
||||
@router.post("/servers/{server_id}/logs", response=LogIngestOut, auth=None)
|
||||
@csrf_exempt
|
||||
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:
|
||||
Server.objects.get(id=server_id)
|
||||
except Server.DoesNotExist:
|
||||
@@ -208,7 +239,13 @@ def build_router() -> Router:
|
||||
@router.post("/servers/{server_id}/heartbeat", response=SyncReportOut, auth=None)
|
||||
@csrf_exempt
|
||||
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:
|
||||
server = Server.objects.get(id=server_id)
|
||||
except Server.DoesNotExist:
|
||||
|
||||
@@ -47,7 +47,14 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/event-types", response=List[AuditEventTypeSchema])
|
||||
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")
|
||||
qs: QuerySet[AuditEventType] = AuditEventType.objects.all()
|
||||
return [
|
||||
@@ -63,7 +70,16 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/logs", response=List[AuditLogSchema])
|
||||
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")
|
||||
qs: QuerySet[AuditLog] = AuditLog.objects.select_related("event_type", "actor").all()
|
||||
if filters.severity:
|
||||
|
||||
@@ -76,6 +76,7 @@ def build_router() -> Router:
|
||||
- If user has global `keys.view_sshkey`, returns all keys.
|
||||
- Otherwise, returns only objects with `keys.view_sshkey` object permission.
|
||||
Filter: user_id (honored only with global view).
|
||||
Rationale: powers the key inventory UI and lets admins audit key usage.
|
||||
"""
|
||||
require_authenticated(request)
|
||||
user = request.user
|
||||
@@ -104,6 +105,7 @@ def build_router() -> Router:
|
||||
- Default owner is the current user.
|
||||
- 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.
|
||||
Rationale: keys are the core authorization material synced to servers.
|
||||
"""
|
||||
require_authenticated(request)
|
||||
if not request.user.has_perm("keys.add_sshkey"):
|
||||
@@ -140,6 +142,7 @@ def build_router() -> Router:
|
||||
|
||||
Auth: required.
|
||||
Permissions: requires `keys.view_sshkey` on the object.
|
||||
Rationale: used by key detail views and server access debugging.
|
||||
"""
|
||||
require_authenticated(request)
|
||||
try:
|
||||
@@ -156,6 +159,7 @@ def build_router() -> Router:
|
||||
|
||||
Auth: required.
|
||||
Permissions: requires `keys.change_sshkey` on the object.
|
||||
Rationale: allows key rotation and revocation without deletion.
|
||||
"""
|
||||
require_authenticated(request)
|
||||
try:
|
||||
@@ -187,6 +191,7 @@ def build_router() -> Router:
|
||||
Auth: required.
|
||||
Permissions: requires `keys.delete_sshkey` on the object.
|
||||
Behavior: sets is_active false and revoked_at if key is active.
|
||||
Rationale: removes key access while preserving auditability.
|
||||
"""
|
||||
require_authenticated(request)
|
||||
try:
|
||||
|
||||
@@ -29,7 +29,13 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/", response=List[ServerOut])
|
||||
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")
|
||||
if request.user.has_perm("servers.view_server"):
|
||||
servers = Server.objects.all()
|
||||
@@ -55,7 +61,13 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/{server_id}", response=ServerOut)
|
||||
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")
|
||||
try:
|
||||
server = Server.objects.get(id=server_id)
|
||||
@@ -77,7 +89,14 @@ def build_router() -> Router:
|
||||
|
||||
@router.patch("/{server_id}", response=ServerOut)
|
||||
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")
|
||||
if payload.display_name is None:
|
||||
raise HttpError(422, {"detail": "No fields provided."})
|
||||
|
||||
@@ -14,7 +14,14 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/health", response=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)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@@ -71,7 +71,13 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/", response=List[TelemetryOut])
|
||||
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")
|
||||
qs = TelemetryEvent.objects.order_by("-created_at")
|
||||
if filters.event_type:
|
||||
@@ -87,7 +93,14 @@ def build_router() -> Router:
|
||||
|
||||
@router.post("/", response=TelemetryOut)
|
||||
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")
|
||||
server = None
|
||||
if payload.server_id:
|
||||
@@ -115,7 +128,12 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/summary", response=TelemetrySummaryOut)
|
||||
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")
|
||||
totals = TelemetryEvent.objects.aggregate(
|
||||
total=Count("id"),
|
||||
|
||||
@@ -53,7 +53,15 @@ def build_router() -> Router:
|
||||
|
||||
@router.post("/", response=UserDetailOut)
|
||||
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")
|
||||
User = get_user_model()
|
||||
email = payload.email.strip().lower()
|
||||
@@ -79,7 +87,13 @@ def build_router() -> Router:
|
||||
|
||||
@router.get("/", response=List[UserListOut])
|
||||
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")
|
||||
User = get_user_model()
|
||||
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)
|
||||
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")
|
||||
User = get_user_model()
|
||||
try:
|
||||
@@ -111,7 +130,13 @@ def build_router() -> Router:
|
||||
|
||||
@router.patch("/{user_id}", response=UserDetailOut)
|
||||
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")
|
||||
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."})
|
||||
@@ -143,18 +168,6 @@ def build_router() -> Router:
|
||||
"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
|
||||
|
||||
|
||||
|
||||
32
app/templates/ninja/swagger.html
Normal file
32
app/templates/ninja/swagger.html
Normal 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>
|
||||
@@ -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-Content-Type-Options nosniff;
|
||||
add_header Referrer-Policy "strict-origin";
|
||||
|
||||
Reference in New Issue
Block a user