diff --git a/TODO.md b/TODO.md index 343b090..cfbaa80 100644 --- a/TODO.md +++ b/TODO.md @@ -16,3 +16,22 @@ Revocation: - Keywarden removes key / cert from target server, or invalidates on Keywarden's side - Keywarden removes object permissions - User cannot access server anymore + + +Permissions: + +Administrator: +- Everything + +Auditor: +- Can exclusively view audit logs of servers they have access to via request. + +User: + + + +Access Requests: + +- Can use Shell? +- Can view logs? +- Can have user account? diff --git a/app/apps/access/admin.py b/app/apps/access/admin.py index 1747bd0..890af4c 100644 --- a/app/apps/access/admin.py +++ b/app/apps/access/admin.py @@ -22,6 +22,9 @@ class AccessRequestAdmin(GuardedModelAdmin): "requester", "server", "status", + "request_shell", + "request_logs", + "request_users", "requested_at", "expires_at", "decided_by", @@ -50,6 +53,9 @@ class AccessRequestAdmin(GuardedModelAdmin): "server", "status", "reason", + "request_shell", + "request_logs", + "request_users", "expires_at", ) }, @@ -64,6 +70,9 @@ class AccessRequestAdmin(GuardedModelAdmin): "server", "status", "reason", + "request_shell", + "request_logs", + "request_users", "expires_at", ) }, diff --git a/app/apps/access/migrations/0002_remove_delete_permission.py b/app/apps/access/migrations/0002_remove_delete_permission.py new file mode 100644 index 0000000..d3c9d1a --- /dev/null +++ b/app/apps/access/migrations/0002_remove_delete_permission.py @@ -0,0 +1,37 @@ +from django.db import migrations, models + + +def remove_delete_accessrequest_perm(apps, schema_editor): + Permission = apps.get_model("auth", "Permission") + ContentType = apps.get_model("contenttypes", "ContentType") + try: + content_type = ContentType.objects.get(app_label="access", model="accessrequest") + except ContentType.DoesNotExist: + return + Permission.objects.filter(content_type=content_type, codename="delete_accessrequest").delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("access", "0001_initial"), + ("auth", "__latest__"), + ("contenttypes", "__latest__"), + ] + + operations = [ + migrations.RunPython(remove_delete_accessrequest_perm, migrations.RunPython.noop), + migrations.AlterModelOptions( + name="accessrequest", + options={ + "verbose_name": "Access request", + "verbose_name_plural": "Access requests", + "default_permissions": ("add", "view", "change"), + "indexes": [ + models.Index(fields=["status", "requested_at"], name="acc_req_status_req_idx"), + models.Index(fields=["server", "status"], name="acc_req_server_status_idx"), + ], + "ordering": ["-requested_at"], + }, + ), + ] diff --git a/app/apps/access/migrations/0003_access_request_options.py b/app/apps/access/migrations/0003_access_request_options.py new file mode 100644 index 0000000..2d1d17b --- /dev/null +++ b/app/apps/access/migrations/0003_access_request_options.py @@ -0,0 +1,26 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("access", "0002_remove_delete_permission"), + ] + + operations = [ + migrations.AddField( + model_name="accessrequest", + name="request_shell", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="accessrequest", + name="request_logs", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="accessrequest", + name="request_users", + field=models.BooleanField(default=False), + ), + ] diff --git a/app/apps/access/models.py b/app/apps/access/models.py index c280db8..9b13d75 100644 --- a/app/apps/access/models.py +++ b/app/apps/access/models.py @@ -28,6 +28,9 @@ class AccessRequest(models.Model): max_length=16, choices=Status.choices, default=Status.PENDING, db_index=True ) reason = models.TextField(blank=True) + request_shell = models.BooleanField(default=False) + request_logs = models.BooleanField(default=False) + request_users = models.BooleanField(default=False) requested_at = models.DateTimeField(default=timezone.now, editable=False) decided_at = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) @@ -42,6 +45,7 @@ class AccessRequest(models.Model): class Meta: verbose_name = "Access request" verbose_name_plural = "Access requests" + default_permissions = ("add", "view", "change") indexes = [ models.Index(fields=["status", "requested_at"], name="acc_req_status_req_idx"), models.Index(fields=["server", "status"], name="acc_req_server_status_idx"), diff --git a/app/apps/access/signals.py b/app/apps/access/signals.py index 531055b..c22b80f 100644 --- a/app/apps/access/signals.py +++ b/app/apps/access/signals.py @@ -16,11 +16,7 @@ def assign_access_request_perms(sender, instance: AccessRequest, created: bool, return if instance.requester_id: user = instance.requester - for perm in ( - "access.view_accessrequest", - "access.change_accessrequest", - "access.delete_accessrequest", - ): + for perm in ("access.view_accessrequest", "access.change_accessrequest"): assign_perm(perm, user, instance) assign_default_object_permissions(instance) sync_server_view_perm(instance) diff --git a/app/apps/core/management/commands/sync_object_perms.py b/app/apps/core/management/commands/sync_object_perms.py index 769de08..d509c36 100644 --- a/app/apps/core/management/commands/sync_object_perms.py +++ b/app/apps/core/management/commands/sync_object_perms.py @@ -20,7 +20,6 @@ class Command(BaseCommand): for perm in ( "access.view_accessrequest", "access.change_accessrequest", - "access.delete_accessrequest", ): assign_perm(perm, access_request.requester, access_request) assign_default_object_permissions(access_request) diff --git a/app/apps/core/rbac.py b/app/apps/core/rbac.py index fcd3f2e..793fa35 100644 --- a/app/apps/core/rbac.py +++ b/app/apps/core/rbac.py @@ -5,11 +5,9 @@ from guardian.shortcuts import assign_perm from ninja.errors import HttpError ROLE_ADMIN = "administrator" -ROLE_OPERATOR = "operator" -ROLE_AUDITOR = "auditor" ROLE_USER = "user" -ROLE_ORDER = (ROLE_ADMIN, ROLE_OPERATOR, ROLE_AUDITOR, ROLE_USER) +ROLE_ORDER = (ROLE_ADMIN, ROLE_USER) ROLE_ALL = ROLE_ORDER ROLE_ALIASES = {"admin": ROLE_ADMIN} ROLE_INPUTS = tuple(sorted(set(ROLE_ORDER) | set(ROLE_ALIASES.keys()))) @@ -20,21 +18,7 @@ def _model_perms(app_label: str, model: str, actions: list[str]) -> list[str]: ROLE_PERMISSIONS = { ROLE_ADMIN: [], - ROLE_OPERATOR: [ - *_model_perms("servers", "server", ["view"]), - *_model_perms("access", "accessrequest", ["add", "view", "change", "delete"]), - *_model_perms("keys", "sshkey", ["add", "view", "change", "delete"]), - *_model_perms("telemetry", "telemetryevent", ["add", "view"]), - *_model_perms("audit", "auditlog", ["view"]), - *_model_perms("audit", "auditeventtype", ["view"]), - *_model_perms("auth", "user", ["add", "view"]), - ], - ROLE_AUDITOR: [ - *_model_perms("audit", "auditlog", ["view"]), - *_model_perms("audit", "auditeventtype", ["view"]), - ], ROLE_USER: [ - *_model_perms("servers", "server", ["view"]), *_model_perms("access", "accessrequest", ["add"]), *_model_perms("keys", "sshkey", ["add"]), ], @@ -132,9 +116,6 @@ def set_user_role(user, role: str) -> str: if canonical == ROLE_ADMIN: user.is_staff = True user.is_superuser = True - elif canonical in {ROLE_OPERATOR, ROLE_AUDITOR}: - user.is_staff = True - user.is_superuser = False else: user.is_staff = False user.is_superuser = False diff --git a/app/keywarden/api/routers/access.py b/app/keywarden/api/routers/access.py index 87800d8..bc09e5d 100644 --- a/app/keywarden/api/routers/access.py +++ b/app/keywarden/api/routers/access.py @@ -19,6 +19,9 @@ from apps.access.permissions import sync_server_view_perm class AccessRequestCreateIn(Schema): server_id: int reason: Optional[str] = None + request_shell: bool = False + request_logs: bool = False + request_users: bool = False expires_at: Optional[datetime] = None @@ -33,6 +36,9 @@ class AccessRequestOut(Schema): server_id: int status: str reason: str + request_shell: bool + request_logs: bool + request_users: bool requested_at: str decided_at: Optional[str] = None expires_at: Optional[str] = None @@ -54,6 +60,9 @@ def _request_to_out(access_request: AccessRequest) -> AccessRequestOut: server_id=access_request.server_id, status=access_request.status, reason=access_request.reason or "", + request_shell=access_request.request_shell, + request_logs=access_request.request_logs, + request_users=access_request.request_users, requested_at=access_request.requested_at.isoformat(), decided_at=access_request.decided_at.isoformat() if access_request.decided_at else None, expires_at=access_request.expires_at.isoformat() if access_request.expires_at else None, @@ -123,6 +132,9 @@ def build_router() -> Router: requester=request.user, server=server, reason=(payload.reason or "").strip(), + request_shell=payload.request_shell, + request_logs=payload.request_logs, + request_users=payload.request_users, ) if payload.expires_at: access_request.expires_at = payload.expires_at