Veröffentlicht am: 5. Oktober 2026

5 Minuten Lesezeit

Zwei Eingangstüren: Zugriffskontrolle auf Modulebene in Django

Angemeldet heißt nicht berechtigt – wie sich Zugriffsrechte in einer Django-App pro Modul durchsetzen lassen, statt nur den Login zu prüfen.

Das Engineering-Team von GitLab entwickelt viele interne Werkzeuge selbst. Bei einer Plattform mussten wir jedoch grundlegend neu überlegen, wie wir Autorisierung umsetzen: bei unserem internen GRC-Tool (Governance, Risk and Compliance). Es bedient unter einem Dach zwei sehr unterschiedliche Zielgruppen, das Security-Compliance-Team und das Internal-Audit-Team.

Dieser Beitrag richtet sich an alle, die ein internes Django-Tool bauen oder pflegen, das nach und nach mehr als eine Zielgruppe bedient: verschiedene Teams, verschiedene Datenklassifizierungen, Daten, die sich nicht vermischen dürfen. Er beschreibt das Problem, auf das wir gestoßen sind, wie wir es gelöst haben und wie sich das Muster auf die eigene App übertragen lässt.

Die Herausforderung

Unser ursprüngliches Tool war eine einzelne Django-App für das Security-Compliance-Team, mit login_required in jeder View. Damit lagen wir richtig, bis Internal Audit einen neuen Anwendungsfall für das Tool fand: einen komplett eigenen Workflow mit eigenen Daten, eigenen Models und eigenen sensiblen Informationen, die Security Compliance nicht sehen sollte.

login_required beantwortet genau eine Frage: Ist die Person angemeldet? Die Frage, auf die es in einem internen Tool für mehrere Teams eigentlich ankommt, beantwortet es nicht: Darf die Person das hier sehen? Eine angemeldete Person aus Security Compliance, die eine URL von Internal Audit aufrief, spazierte einfach an der Login-Prüfung vorbei und landete bei Daten, die sie nie hätte sehen dürfen.

Wenn eine App nachträglich eine zweite Zielgruppe bekommen hat, klingt das vermutlich vertraut. Eine einfache Gegenprobe: Prüft jede View der App, wer die Person ist, oder nur, ob sie angemeldet ist? Trifft Letzteres zu, klafft wahrscheinlich dieselbe Lücke wie bei uns.

Wir brauchten eine saubere Trennung. Nicht "diesen Button bitte einfach ignorieren", sondern eine verbindliche Grenze, die hält, egal welche URL jemand eintippt.

Die Architekturentscheidung

Als der Anwendungsfall von Internal Audit auf unserer Roadmap landete, standen wir vor der klassischen Wahl: an die bestehende App anbauen oder umstrukturieren? Wir haben umstrukturiert.

Das Ergebnis ist ein zentrales Django-Projekt (Core) mit gemeinsamer Authentifizierung, Nutzerverwaltung, Audit-Logging und grundlegenden UI-Komponenten. Darauf setzen zwei unabhängige Modul-Apps auf, eine für Security Compliance und eine für Internal Audit. Jedes Modul besitzt eigene Models, Views und eigene API-Endpunkte.

Den Kern bildet eine selbst auferlegte Regel: Core weiß nichts von den Daten aus Compliance oder Internal Audit. Beide Module dürfen von Core abhängen. Keines der Module darf vom anderen abhängen. Diese Regel verhindert, dass am Ende zwei Apps herauskommen, die nur mit Klebeband und Sekundenkleber zusammenhalten.

Die Zugriffskontrolle

Als Compliance-Engineering-Team bauen wir ein Tool, in dem sensible Compliance- und Risikodaten liegen. Nachlässige Zugriffskontrolle wäre hier nicht nur ein Bug, sondern ein ernstes Glaubwürdigkeitsproblem.

Unsere Entscheidung fiel auf etwas, das Django bereits mitbringt: das eingebaute Auth-System. Jedes Modul entspricht genau einer Gruppe. Eine Person kann einer Gruppe angehören, beiden oder keiner, und das System setzt die passende Funktionstrennung auf jeder Ebene durch, ganz gleich, welche Kombination zutrifft. Navigation, Django-Views und REST-API prüfen die Gruppenzugehörigkeit jeweils unabhängig voneinander.

Die Umsetzung

In Core gibt es eine einzige wiederverwendbare Klasse, von der beide Module erben: GroupRequiredMixin. Die Klasse prüft die Gruppenzugehörigkeit und wirft PermissionDenied, wenn die Person nicht zur geforderten Gruppe gehört.

Unterklassen auf Modulebene

Jedes Modul erhält eine dreizeilige Unterklasse, die required_group_name setzt: ein GasGroupRequiredMixin und ein IsGasGroupMember für Security Compliance (Gas ist ein interner Gruppenname aus unserem Code), nach demselben Muster auch für Internal Audit. Mehr kostet die Durchsetzungsschicht eines Moduls nicht: eine neue Datei, ein neues Attribut. Die eigentliche Prüflogik liegt in Core und muss nie wieder angefasst werden.

Die UI-Schicht

Schließlich gibt es den Template-Filter user_in_group, damit die Navigation nur anzeigt, was eine Person tatsächlich sehen darf. Jeder Navigationsblock, der nur für Mitglieder einer bestimmten Gruppe erscheinen soll, wird dafür in eine Prüfung wie user|user_in_group:'erm' eingebettet. Auch erm ist dabei ein interner Gruppenname.

Das dient der Benutzerführung und ist keine Sicherheitsgrenze. Ein ausgeblendeter Navigationslink hindert niemanden daran, die URL direkt einzugeben. Dafür sind die Mixins und die Permission-Klasse für Django REST Framework da. Der Template-Filter sorgt lediglich dafür, dass die Oberfläche nur anbietet, was eine Person auch sehen und tun darf.

Ausblick und erste Schritte

Auf Plattformebene zahlt sich aus, dass sich dieses Muster wiederholen lässt, unabhängig von Modul und Team. Ein hypothetisches drittes Modul, etwa ein Workflow für Lieferantenrisiken oder ein Modul für Audit-Management, übernimmt dieselbe Struktur der Zugriffskontrolle, denselben Audit-Trail und dieselbe gemeinsame Authentifizierung, ohne eine einzige neue Zeile Durchsetzungslogik. Der Aufwand für ein neues Modul ergibt sich damit fast vollständig aus den fachlichen Anforderungen und nicht aus dem technischen Gerüst.

Darauf haben wir gesetzt: das Fundament einmal bauen und jedes künftige Modul darauf aufbauen lassen, statt die Zugriffskontrolle jedes Mal neu zu erfinden.

Für die Umsetzung in der eigenen App empfehlen wir etwa diese Reihenfolge:

  1. Die Views auf die Lücke zwischen Login und Autorisierung prüfen. Nach login_required und LoginRequiredMixin suchen und bei jedem Treffer fragen, ob er sensible Daten tatsächlich vor anderen angemeldeten Personen schützt und nicht nur vor anonymen.
  2. Die Modulgrenze ziehen, bevor Code entsteht. Festlegen, was wirklich gemeinsam genutzt wird (Authentifizierung, grundlegende UI, Logging) und was nur einer Zielgruppe gehört.
  3. Ein einziges Mixin schreiben, nicht eines pro View. Ein GroupRequiredMixin in der Core-App, pro Modul abgeleitet, bedeutet: Die Durchsetzungslogik muss an genau einer Stelle geprüft werden und kann nur an genau einer Stelle fehlerhaft sein.
  4. Auf jeder Ebene unabhängig durchsetzen, also in Views, API und Navigation, damit ein ausgeblendeter Link nie die einzige Verteidigungslinie ist.

Referenzen

Feedback erwünscht

Hat dir dieser Blogbeitrag gefallen? Hast du Fragen oder Feedback? Erstelle ein neues Diskussionsthema im GitLab-Community-Forum und lass andere an deinen Eindrücken teilhaben.

Feedback teilen

Beginne noch heute, schneller zu entwickeln

Entdecke, was dein Team mit der intelligenten Orchestrierungsplattform für DevSecOps erreichen kann.