diff --git a/agent/config.example.json b/agent/config.example.json index f0ebbc8..3b25335 100644 --- a/agent/config.example.json +++ b/agent/config.example.json @@ -1,6 +1,6 @@ { - "server_url": "https://keywarden.example.com", - "server_id": "", + "server_url": "https://keywarden.dev.ntbx.io/api/v1", + "server_id": "4", "sync_interval_seconds": 30, "log_batch_size": 500, "state_dir": "/var/lib/keywarden-agent", @@ -11,4 +11,4 @@ "create_home": true, "lock_on_revoke": true } -} +} \ No newline at end of file diff --git a/agent/keywarden-agent b/agent/keywarden-agent index db4bcb7..65d6dc5 100755 Binary files a/agent/keywarden-agent and b/agent/keywarden-agent differ diff --git a/app/apps/accounts/templates/accounts/login.html b/app/apps/accounts/templates/accounts/login.html index cb4e755..3310642 100644 --- a/app/apps/accounts/templates/accounts/login.html +++ b/app/apps/accounts/templates/accounts/login.html @@ -8,7 +8,7 @@

Sign in

{% csrf_token %} - +
@@ -35,4 +35,3 @@
{% endblock %} - diff --git a/app/apps/servers/admin.py b/app/apps/servers/admin.py index 839c387..2fb9b51 100644 --- a/app/apps/servers/admin.py +++ b/app/apps/servers/admin.py @@ -59,12 +59,11 @@ class AgentCertificateAuthorityAdmin(admin.ModelAdmin): list_display = ("name", "is_active", "created_at", "revoked_at") list_filter = ("is_active", "created_at", "revoked_at") search_fields = ("name", "fingerprint") - readonly_fields = ("fingerprint", "serial", "created_at", "revoked_at", "created_by") + readonly_fields = ("cert_pem", "fingerprint", "serial", "created_at", "revoked_at", "created_by") fields = ( "name", "is_active", "cert_pem", - "key_pem", "fingerprint", "serial", "created_by", diff --git a/app/apps/servers/templates/servers/dashboard.html b/app/apps/servers/templates/servers/dashboard.html new file mode 100644 index 0000000..28b7bb7 --- /dev/null +++ b/app/apps/servers/templates/servers/dashboard.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %}Servers • Keywarden{% endblock %} + +{% block content %} +
+
+

Servers

+

Your active server access and recent activity at a glance.

+
+ + {% if servers %} +
+ {% for item in servers %} +
+
+
+
+ {{ item.server.initial }} +
+
+

{{ item.server.display_name }}

+

+ {{ item.server.hostname|default:item.server.ipv4|default:item.server.ipv6|default:"Unassigned" }} +

+
+
+ Active +
+ +
+
+
Access until
+
+ {% if item.expires_at %} + {{ item.expires_at|date:"M j, Y H:i" }} + {% else %} + No expiry + {% endif %} +
+
+
+
Last accessed
+
+ {% if item.last_accessed %} + {{ item.last_accessed|date:"M j, Y H:i" }} + {% else %} + — + {% endif %} +
+
+
+ + +
+ {% endfor %} +
+ {% else %} +
+

No server access yet

+

Request access to a server to see it here.

+
+ {% endif %} +
+{% endblock %} diff --git a/app/apps/servers/templates/servers/detail.html b/app/apps/servers/templates/servers/detail.html new file mode 100644 index 0000000..efa2184 --- /dev/null +++ b/app/apps/servers/templates/servers/detail.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block title %}{{ server.display_name }} • Keywarden{% endblock %} + +{% block content %} +
+
+
+
+ {{ server.initial }} +
+
+

{{ server.display_name }}

+

+ {{ server.hostname|default:server.ipv4|default:server.ipv6|default:"Unassigned" }} +

+
+
+ Back to servers +
+ +
+
+

Access

+
+
+
Access until
+
+ {% if expires_at %} + {{ expires_at|date:"M j, Y H:i" }} + {% else %} + No expiry + {% endif %} +
+
+
+
Last accessed
+
+ {% if last_accessed %} + {{ last_accessed|date:"M j, Y H:i" }} + {% else %} + — + {% endif %} +
+
+
+
+ +
+

Server details

+
+
+
Hostname
+
{{ server.hostname|default:"—" }}
+
+
+
IPv4
+
{{ server.ipv4|default:"—" }}
+
+
+
IPv6
+
{{ server.ipv6|default:"—" }}
+
+
+
+
+ +
+
+

Logs

+ Placeholder +
+
+ Logs will appear here once collection is enabled for this server. +
+
+
+{% endblock %} diff --git a/app/apps/servers/urls.py b/app/apps/servers/urls.py new file mode 100644 index 0000000..4e1ce0d --- /dev/null +++ b/app/apps/servers/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +app_name = "servers" + +urlpatterns = [ + path("", views.dashboard, name="dashboard"), + path("/", views.detail, name="detail"), +] diff --git a/app/apps/servers/views.py b/app/apps/servers/views.py new file mode 100644 index 0000000..f532e61 --- /dev/null +++ b/app/apps/servers/views.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from django.contrib.auth.decorators import login_required +from django.db.models import Q +from django.http import Http404 +from django.shortcuts import render +from django.utils import timezone + +from apps.access.models import AccessRequest + + +@login_required(login_url="/accounts/login/") +def dashboard(request): + now = timezone.now() + access_qs = ( + AccessRequest.objects.select_related("server") + .filter( + requester=request.user, + status=AccessRequest.Status.APPROVED, + ) + .filter(Q(expires_at__isnull=True) | Q(expires_at__gt=now)) + .order_by("-requested_at") + ) + + seen = set() + servers = [] + for access in access_qs: + if access.server_id in seen: + continue + seen.add(access.server_id) + servers.append( + { + "server": access.server, + "expires_at": access.expires_at, + "last_accessed": None, + } + ) + + context = { + "servers": servers, + } + return render(request, "servers/dashboard.html", context) + + +@login_required(login_url="/accounts/login/") +def detail(request, server_id: int): + now = timezone.now() + access = ( + AccessRequest.objects.select_related("server") + .filter( + requester=request.user, + server_id=server_id, + status=AccessRequest.Status.APPROVED, + ) + .filter(Q(expires_at__isnull=True) | Q(expires_at__gt=now)) + .order_by("-requested_at") + .first() + ) + if not access: + raise Http404("Server not found") + + context = { + "server": access.server, + "expires_at": access.expires_at, + "last_accessed": None, + } + return render(request, "servers/detail.html", context) diff --git a/app/keywarden/api/routers/agent.py b/app/keywarden/api/routers/agent.py index 7ba0115..74dc6bd 100644 --- a/app/keywarden/api/routers/agent.py +++ b/app/keywarden/api/routers/agent.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import datetime, timedelta from typing import List, Optional @@ -8,10 +6,11 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID from django.conf import settings from django.core.exceptions import ValidationError -from django.db import models +from django.db import IntegrityError, models, transaction from django.http import HttpRequest from django.utils import timezone -from ninja import Router, Schema +from django.views.decorators.csrf import csrf_exempt +from ninja import Body, Router, Schema from ninja.errors import HttpError from pydantic import Field @@ -78,7 +77,8 @@ def build_router() -> Router: router = Router() @router.post("/enroll", response=AgentEnrollOut, auth=None) - def enroll_agent(request: HttpRequest, payload: AgentEnrollIn): + @csrf_exempt + def enroll_agent(request: HttpRequest, payload: AgentEnrollIn = Body(...)): """Enroll a server agent using a one-time token.""" token_value = (payload.token or "").strip() if not token_value: @@ -100,16 +100,19 @@ def build_router() -> Router: except ValidationError: hostname = None - server = Server.objects.create(display_name=display_name, hostname=hostname) - token.mark_used(server) - token.save(update_fields=["used_at", "server"]) - csr = _load_csr((payload.csr_pem or "").strip()) - cert_pem, ca_pem, fingerprint, serial = _issue_client_cert(csr, host, server.id) - server.agent_enrolled_at = timezone.now() - server.agent_cert_fingerprint = fingerprint - server.agent_cert_serial = serial - server.save(update_fields=["agent_enrolled_at", "agent_cert_fingerprint", "agent_cert_serial"]) + try: + with transaction.atomic(): + server = Server.objects.create(display_name=display_name, hostname=hostname) + token.mark_used(server) + token.save(update_fields=["used_at", "server"]) + cert_pem, ca_pem, fingerprint, serial = _issue_client_cert(csr, host, server.id) + server.agent_enrolled_at = timezone.now() + server.agent_cert_fingerprint = fingerprint + server.agent_cert_serial = serial + server.save(update_fields=["agent_enrolled_at", "agent_cert_fingerprint", "agent_cert_serial"]) + except IntegrityError: + raise HttpError(409, "Server already enrolled") return AgentEnrollOut( server_id=str(server.id), @@ -153,10 +156,10 @@ def build_router() -> Router: for key in keys ] - @router.post("/servers/{server_id}/sync-report", response=SyncReportOut) - def sync_report(request: HttpRequest, server_id: int, payload: SyncReportIn): + @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).""" - require_perms(request, "servers.view_server", "telemetry.add_telemetryevent") try: server = Server.objects.get(id=server_id) except Server.DoesNotExist: @@ -176,7 +179,8 @@ def build_router() -> Router: return SyncReportOut(status="ok") @router.post("/servers/{server_id}/logs", response=LogIngestOut, auth=None) - def ingest_logs(request: HttpRequest, server_id: int, payload: List[LogEventIn]): + @csrf_exempt + def ingest_logs(request: HttpRequest, server_id: int, payload: List[LogEventIn] = Body(...)): """Accept log batches from agents (mTLS required at the edge).""" try: Server.objects.get(id=server_id) diff --git a/app/keywarden/settings/base.py b/app/keywarden/settings/base.py index c0524c2..40f254c 100644 --- a/app/keywarden/settings/base.py +++ b/app/keywarden/settings/base.py @@ -226,7 +226,7 @@ else: ] LOGIN_URL = "/accounts/login/" LOGOUT_URL = "/oidc/logout/" -LOGIN_REDIRECT_URL = "/" +LOGIN_REDIRECT_URL = "/servers/" LOGOUT_REDIRECT_URL = "/" ANONYMOUS_USER_NAME = None diff --git a/app/keywarden/urls.py b/app/keywarden/urls.py index 1fcc001..dd71801 100644 --- a/app/keywarden/urls.py +++ b/app/keywarden/urls.py @@ -8,10 +8,11 @@ urlpatterns = [ path("admin/", admin.site.urls), path("oidc/", include("mozilla_django_oidc.urls")), path("accounts/", include("apps.accounts.urls")), + path("servers/", include("apps.servers.urls")), # API path("api/", ninja_api.urls), path("api/v1/", ninja_api_v1.urls), path("api/auth/jwt/create/", TokenObtainPairView.as_view(), name="jwt-create"), path("api/auth/jwt/refresh/", TokenRefreshView.as_view(), name="jwt-refresh"), - path("", RedirectView.as_view(pattern_name="accounts:login", permanent=False)), + path("", RedirectView.as_view(pattern_name="servers:dashboard", permanent=False)), ] diff --git a/app/templates/base.html b/app/templates/base.html index ab55513..4fefd9a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -19,6 +19,7 @@