Published on: October 5, 2026

5 min read

Two front doors: Module-level access in a Django GRC app

A technical deep-dive on restructuring a Django internal tool to support two distinct GitLab teams: Security Compliance and Internal Audit.

GitLab's engineering team builds a lot of our own internal tooling, but one platform in particular forced us to rethink how we handle authorization: our internal GRC tool that serves two very different customer bases under one roof — our Security Compliance team and our Internal Audit team.

If you're building or maintaining an internal Django tool that's starting to serve more than one audience — different teams, different data classification levels, different data that shouldn't cross streams — this post walks through the problem we hit, how we solved it, and how you can apply the same pattern to your own app.

The challenge

Our original tool was a single Django app built for the Security Compliance team with login_required on every view. This was the correct approach until Internal Audit found a new use case for our tool that would include a completely separate workflow, with its own data, its own models, and its own sensitive information that Security Compliance should not see.

login_required answers exactly one question: Are you logged in? It doesn't answer the question that actually matters in a multi-tenant internal tool: Are you allowed to be here? An authenticated Security Compliance user hitting an Internal Audit URL waltzed right past the login check and into data they were never supposed to see.

If your app has ever grown a second audience after the fact, this probably sounds familiar. The question worth asking yourself right now is simple: Does every view in your app check who the user is, or just whether they're logged in? If it's the latter, you likely have the same gap we did.

We needed clean separation — not "pay no attention to this button over here," but an enforced boundary that holds regardless of what URL someone types in.

The architectural decision

When Internal Audit's use case landed on our roadmap, we had the classic fork in the road: Bolt it onto the existing app or restructure? We restructured.

The result is a core Django project — shared auth, user management, audit logging, base UI components — with two independent module apps sitting on top of it. One is for Security Compliance and one is for Internal Audit. Each module owns its own models, views, and API surface.

The constraint we imposed on ourselves, and the one that made this whole thing worth writing about was that core has zero knowledge of compliance or internal audit data. Either module can depend on core. Neither module should depend on the other. That constraint keeps this from becoming two apps stitched together with duct tape and super glue.

Access control

We're a compliance engineering subdivision building a tool that stores sensitive compliance and risk data. Sloppy access control here isn't just a bug, it's a serious credibility problem.

Our design decision was to use something already native to Django: the built-in auth system. Each module maps to exactly one group. A user can belong to one group, both, or neither, and the system enforces the right separation of duty at every layer regardless of which combination applies. The navigation UI, the Django views, and the REST API each independently enforce group membership.

The implementation

There is one reusable class in core that both modules inherit from: GroupRequiredMixin. It checks group membership and raises PermissionDenied if the user isn't in the required group.

Module-level subclasses

Each module gets a three-line subclass that sets required_group_name — a GasGroupRequiredMixin and an IsGasGroupMember for the Security Compliance side, with the same pattern repeating for Internal Audit. That's the whole cost of adding a module's enforcement layer: one new file, one new attribute. The actual checking logic lives in core and never gets touched again.

The UI layer

Finally, there is a template filter, user_in_group, so navigation only renders what a user is actually allowed to view. It wraps a check like user|user_in_group:'erm' around any nav block that should only show up for ERM members.

This is a UX guard, not a security boundary. Hiding a navigation link doesn't stop anyone from typing the URL directly. That's what the mixins and the Django REST Framework permission class are for. The template filter just keeps the UI honest about what a given user can see and do.

What's next for us - and where you could start

Step back out to the platform level and the payoff is that this is a repeatable pattern that is module- and team-agnostic. A hypothetical third module — a vendor risk workflow, an audit management module, whatever comes next — inherits the same access control structure, the same audit trail, and the same shared authentication without a single new line of enforcement logic. The cost of a new module becomes almost entirely a business requirement, not infrastructure scaffolding.

That's the bet we made: Build the foundation once and let every future module ride on top of it instead of reinventing access control from scratch.

If you want to try this in your own app, here's roughly the order we'd suggest:

  1. Check your views for the login-versus-authorization gap. Grep for login_required and LoginRequiredMixin and ask, for each one, whether it's actually guarding sensitive data from other authenticated users — not just anonymous ones.
  2. Draw the module boundary before you write code. Decide what's genuinely shared (auth, base UI, logging) versus what belongs to one audience only.
  3. Write one mixin, not one per view. A single GroupRequiredMixin in your core app, subclassed per module, means your enforcement logic has exactly one place to be reviewed and one place to be wrong.
  4. Enforce at every layer independently — views, API, and navigation — so hiding a link is never your only line of defense.

References

We want to hear from you

Enjoyed reading this blog post or have questions or feedback? Share your thoughts by creating a new topic in the GitLab community forum.

Share your feedback

Start building faster today

See what your team can do with the intelligent orchestration platform for DevSecOps.