object‑permission–driven server access; agent‑managed account provisioning with presence reporting

This commit is contained in:
2026-01-26 17:03:44 +00:00
parent ed2f921b0f
commit 43bff4513a
10 changed files with 699 additions and 67 deletions

View File

@@ -0,0 +1,59 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("servers", "0003_agent_ca"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="ServerAccount",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("system_username", models.CharField(max_length=128)),
("is_present", models.BooleanField(db_index=True, default=False)),
("last_synced_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
("created_at", models.DateTimeField(default=django.utils.timezone.now, editable=False)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"server",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="accounts",
to="servers.server",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="server_accounts",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Server account",
"verbose_name_plural": "Server accounts",
"ordering": ["server_id", "user_id"],
},
),
migrations.AddConstraint(
model_name="serveraccount",
constraint=models.UniqueConstraint(fields=("server", "user"), name="unique_server_account"),
),
migrations.AddIndex(
model_name="serveraccount",
index=models.Index(fields=["server", "user"], name="servers_account_user_idx"),
),
migrations.AddIndex(
model_name="serveraccount",
index=models.Index(fields=["server", "is_present"], name="servers_account_present_idx"),
),
]

View File

@@ -157,3 +157,30 @@ class AgentCertificateAuthority(models.Model):
self.key_pem = key_pem
self.fingerprint = cert.fingerprint(hashes.SHA256()).hex()
self.serial = format(cert.serial_number, "x")
class ServerAccount(models.Model):
server = models.ForeignKey(Server, on_delete=models.CASCADE, related_name="accounts")
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="server_accounts"
)
system_username = models.CharField(max_length=128)
is_present = models.BooleanField(default=False, db_index=True)
last_synced_at = models.DateTimeField(default=timezone.now, editable=False)
created_at = models.DateTimeField(default=timezone.now, editable=False)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "Server account"
verbose_name_plural = "Server accounts"
constraints = [
models.UniqueConstraint(fields=["server", "user"], name="unique_server_account")
]
indexes = [
models.Index(fields=["server", "user"], name="servers_account_user_idx"),
models.Index(fields=["server", "is_present"], name="servers_account_present_idx"),
]
ordering = ["server_id", "user_id"]
def __str__(self) -> str:
return f"{self.system_username} ({self.server_id})"

View File

@@ -33,6 +33,18 @@
{% endif %}
</dd>
</div>
<div class="flex items-center justify-between">
<dt>Account on server</dt>
<dd class="font-medium text-gray-900">
{% if account_present is None %}
Unknown
{% elif account_present %}
Present
{% else %}
Missing
{% endif %}
</dd>
</div>
<div class="flex items-center justify-between">
<dt>Last accessed</dt>
<dd class="font-medium text-gray-900">

View File

@@ -5,24 +5,21 @@ from django.db.models import Q
from django.http import Http404
from django.shortcuts import render
from django.utils import timezone
from guardian.shortcuts import get_objects_for_user
from guardian.shortcuts import get_objects_for_user, get_perms
from apps.access.models import AccessRequest
from apps.servers.models import Server
from apps.servers.models import Server, ServerAccount
@login_required(login_url="/accounts/login/")
def dashboard(request):
now = timezone.now()
if request.user.has_perm("servers.view_server"):
server_qs = Server.objects.all()
else:
server_qs = get_objects_for_user(
request.user,
"servers.view_server",
klass=Server,
accept_global_perms=False,
)
server_qs = get_objects_for_user(
request.user,
"servers.view_server",
klass=Server,
accept_global_perms=False,
)
access_qs = (
AccessRequest.objects.select_related("server")
@@ -66,9 +63,7 @@ def detail(request, server_id: int):
server = Server.objects.get(id=server_id)
except Server.DoesNotExist:
raise Http404("Server not found")
if not request.user.has_perm("servers.view_server", server) and not request.user.has_perm(
"servers.view_server"
):
if "view_server" not in get_perms(request.user, server):
raise Http404("Server not found")
access = (
@@ -82,9 +77,13 @@ def detail(request, server_id: int):
.first()
)
account = ServerAccount.objects.filter(server=server, user=request.user).first()
context = {
"server": server,
"expires_at": access.expires_at if access else None,
"last_accessed": None,
"account_present": account.is_present if account else None,
"account_synced_at": account.last_synced_at if account else None,
"system_username": account.system_username if account else None,
}
return render(request, "servers/detail.html", context)