Files
training-software/docs/TRA-235-authorization-matrix.md
Paperclip CTO 1f6a4183d4 feat(TRA-247): M5 security hardening — tests, markers, and header enforcement
- tests/test_security.py: 30 security regression tests covering secure
  headers, CSP directives, middleware ordering, DRF throttle configuration,
  and SecurityAuditMiddleware event-detection logic
- tests/test_upload.py: 19 upload defense tests covering extension allow-list,
  byte-length limits, and magic-byte signature validation (polyglot / disguised
  executable detection)
- pytest.ini: register 'security' and 'upload' markers (--strict-markers
  enforcement was already on)

Security settings already committed in feat(TRA-233) via harness include:
SECURE_REFERRER_POLICY, CSP_* directives, DEFAULT_THROTTLE_*, MAX_UPLOAD_SIZE,
SESSION/CSRF cookie hardening, AWS presigned URL policy, and
SecurityAuditMiddleware with dual-logger (access + security) pattern.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-07 09:11:23 +02:00

12 KiB

TRA-235 Object-Level Authorization Matrix

Date: 2026-05-07 Issue: TRA-235 Owner: Senior Django Backend Engineer Status: Enforced (see api/permissions.py)


1) Roles

Slug Description
learner End-user consuming assigned courses and training.
trainer Instructor managing attendance/signoff for assigned sessions.
author Content creator managing courses, quizzes, and CMS assets.
manager Org-scoped oversight: reporting, enrollment management, exports.
admin Full tenant administration, policy overrides.

Role bindings are org-scoped (UserRoleBinding.org_id). A user may hold different roles in different orgs simultaneously. Admins bypass org-scope restrictions for tenant administration actions.


2) Status Codes for Access Failures

Condition Code
Unauthenticated (no valid session/token) 401 Unauthorized
Authenticated but insufficient role 403 Forbidden
Object exists but must be concealed per policy 404 Not Found

Rule: use 404 only when revealing the existence of the object would itself be a security disclosure (e.g. a learner should not know whether an unpublished course exists in an org they don't belong to). In all other cases, return 403.


3) Object-Level Matrix

3.1 Courses (courses app)

Object Action learner trainer author manager admin Notes
Course list own enrolled & published - org-scoped org-scoped all Learners see only enrolled/published courses
Course retrieve own enrolled & published - org-scoped org-scoped all
Course create ✓ (org) ✓ (org)
Course update ✓ (org) ✓ (org)
Course delete/archive ✓ (org)
Module / Lesson / Page all same as parent Course above
Enrollment create ✓ (org) Enrollment assignment
Enrollment retrieve own only org-scoped all IsEnrollmentOwner
Enrollment revoke ✓ (org)

Permission classes: IsContentEditor + IsCourseOrgScoped, IsEnrollmentOwner

3.2 CMS (cms app)

Object Action learner trainer author manager admin Notes
Asset list ✓ (org) ✓ (org)
Asset upload / presign ✓ (org) ✓ (org)
Asset delete / takedown ✓ (org)
PageContent read (published) ✓ (via course delivery) ✓ (org) ✓ (org) Learners access through course player endpoint only
PageContent write ✓ (org) ✓ (org)
Theme read ✓ (resolved) ✓ (org) ✓ (org)
Theme write ✓ (org) ✓ (org)

Permission classes: IsContentEditor + IsAssetOrgScoped

3.3 Tracking (tracking app)

Object Action learner trainer author manager admin Notes
TrackingEvent create (heartbeat/focus) own enrollment only Must own the enrollment
PageProgress retrieve own only org-scoped org-scoped all IsOwnProgressOrPrivileged
EnrollmentProgress retrieve own only org-scoped org-scoped all
ProgressAuditEvent list ✓ (org) ✓ (org) Privileged audit stream

Permission classes: IsEnrollmentOwner (write), IsOwnProgressOrPrivileged (read), IsPrivileged (audit stream)

3.4 Quizzes (quizzes app)

Object Action learner trainer author manager admin Notes
Quiz list / retrieve published only org-scoped org-scoped org-scoped all
Quiz create / update ✓ (org) ✓ (org)
Quiz archive ✓ (org)
Question / Choice read via published quiz ✓ (org) ✓ (org) ✓ (org)
Question / Choice write ✓ (org) ✓ (org)
Attempt create (start) own enrollment Must be enrolled; respects attempt_limit
Attempt retrieve own only org-scoped cohort org-scoped all IsAttemptOwner
Attempt submit own in-progress Idempotent
Attempt regrade ✓ (org) Privileged flow
GradingDecisionAudit read ✓ (org)

Permission classes: IsAttemptOwner, IsContentEditor + IsQuizOrgScoped, IsPrivileged

3.5 Training (training app)

Object Action learner trainer author manager admin Notes
TrainingSession list own registered only own sessions + org org-scoped all
TrainingSession retrieve own registration ✓ (own/org) org-scoped all
TrainingSession create / update ✓ (assigned org) ✓ (org)
TrainingSession cancel ✓ (org)
AttendanceEvent create ✓ (own session) ✓ (org) IsSessionTrainerOrPrivileged
TrainerSignoff create ✓ (own session) ✓ (org) Immutable once written; requires audit trail
TrainingCompletionDecision read own only org-scoped org-scoped all

Permission classes: IsSessionTrainerOrPrivileged, IsAtLeastManager + IsOrgScopedObject

3.6 Certificates (certificates app)

Object Action learner trainer author manager admin Notes
Certificate issue (trigger) ✓ (org) Requires TrainingCompletionDecision == eligible
Certificate list own only org-scoped all IsCertificateOwner
Certificate retrieve own only org-scoped all
Certificate revoke ✓ (org)
Verify GET /verify/{hash}/ public No auth required; minimal public response
CertificateRenderJob read ✓ (org)

Permission classes: IsCertificateOwner, IsAtLeastManager + IsOrgScopedObject, IsCertificateVerifyPublic

3.7 Reports (reports app)

Object Action learner trainer author manager admin Notes
Progress / Attempt / Completion reports list ✓ (org) IsReportOrgScoped
CSV export trigger ✓ (org) Actor-scoped data only
AuditExportJob request limited scope Manager: own-org approved policy scopes only
AuditExportJob retrieve / download ✓ (own jobs) Access log mandatory
AuditExportJob revoke ✓ (own jobs)

Permission classes: IsAtLeastManager + IsReportOrgScoped, IsPrivileged

3.8 Notifications (notifications app)

Object Action learner trainer author manager admin Notes
Notification list own only own only own only own only own only All roles read own
Notification mark read own only own only own only own only own only IsOwnNotification
NotificationDelivery telemetry ✓ (org) ✓ (org) Admin/Manager privileged

Permission classes: IsOwnNotification, IsPrivileged


4) Permission Class Reference

Class Location Use case
IsLearner api/permissions.py Restrict to learner role at view level
IsTrainer api/permissions.py Restrict to trainer role
IsAuthor api/permissions.py Restrict to author role
IsManager api/permissions.py Restrict to manager role
IsAdmin api/permissions.py Restrict to admin role
IsAtLeastTrainer api/permissions.py Trainer, Author, Manager, Admin
IsAtLeastManager api/permissions.py Manager or Admin
IsPrivileged api/permissions.py Manager or Admin (privileged endpoints)
IsContentEditor api/permissions.py Author, Manager, Admin (content mutation)
IsEnrollmentOwner api/permissions.py Learner owns enrollment; managers bypass
IsAttemptOwner api/permissions.py Learner owns attempt; managers bypass
IsCertificateOwner api/permissions.py Learner owns certificate; managers bypass
IsOwnNotification api/permissions.py User owns notification (no bypass)
IsOwnProgressOrPrivileged api/permissions.py Own progress or trainer/manager/admin
IsOrgScopedObject api/permissions.py Object org_id in user's role-binding orgs
IsCourseOrgScoped api/permissions.py Course/Module/Lesson/Page org scope
IsAssetOrgScoped api/permissions.py CMS Asset org scope
IsQuizOrgScoped api/permissions.py Quiz org scope
IsReportOrgScoped api/permissions.py ReportSnapshot org scope
IsSessionTrainerOrPrivileged api/permissions.py Trainer owns session; manager/admin bypass
IsCertificateVerifyPublic api/permissions.py Public GET for cert verification hash
IsReadOnlyOrPrivileged api/permissions.py Safe methods for all; mutations for privileged
IsLearnerOwnerOrPrivileged api/permissions.py Composite owner-or-privileged check

5) Enforcement Rules

  1. Default closed: REST_FRAMEWORK["DEFAULT_PERMISSION_CLASSES"] = [IsAuthenticated] is already set in config/settings/base.py. Every view must override with a more restrictive class — never loosen to AllowAny without explicit @api_view decorator and justification comment.

  2. Object-level checks: GenericAPIView.get_object() calls check_object_permissions() automatically. Views using get_queryset() + manual filtering must call self.check_object_permissions(request, obj) explicitly when returning objects by PK.

  3. Queryset pre-filtering: Object-level permission classes are the last line of defense. Views must also filter querysets to the user's org scope so list endpoints don't expose row counts for unauthorized orgs (even when pagination conceals the data).

  4. Concealment policy: For learner-facing list endpoints, filter the queryset to exclude inaccessible objects (return 404 on direct PK lookup rather than 403) when the object's existence is org-private.

  5. Audit log: All privileged accesses (audit export download, delivery telemetry, regrade) must write an access log entry (see AuditExportAccessLog, CertificateVerificationAudit).


6) Test Coverage Requirement

Per TRA-253 §8.3, CI must fail when permission tests are missing for changed endpoints. Minimum required per endpoint:

  • One passing test per role class (learner / trainer / author / manager / admin).
  • One owner test (owner should pass, non-owner should fail).
  • One unauthenticated test (must return 401).

Test file: tests/test_permissions.py


7) Out of Scope

  • Enterprise ABAC policy engine (dynamic attribute-based rules).
  • Field-level read/write visibility beyond serializer include/exclude.
  • Row-level security at the database layer (Postgres RLS).