As your Wagtail site grows from a solo project to a team-managed platform, controlling who creates, edits, approves, and publishes content becomes essential. Wagtail builds on Django's auth framework with page-level permissions, moderation roles, collection-based snippet access, and group-scoped admin visibility. In this lesson, you will design a complete content governance model that scales from a two-person team to an enterprise editorial department.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Understand Wagtail's permission hierarchy: users, groups, roles, and how they interact
  • Create custom permission groups with granular page-level and snippet-level access
  • Configure the three-tier editorial workflow: draft, submitted for moderation, published
  • Implement collection-based access control for reusable content assets
  • Restrict admin panel views so editors see only their relevant sections
  • Design a content governance model for multi-team editorial environments
  • Troubleshoot permission errors and audit user access with Django tools

2. Why This Matters (Real-World Scenario)

Picture this: your Wagtail site now serves content in three languages (English, French, Spanish) through a multi-lingual setup you built in the previous lesson. Your team has grown — an English content writer, a French translator, a Spanish editor, a graphic designer uploading hero images, a marketing lead who approves all content, and two developers who maintain the template code. Without permissions, every team member can edit anyone else's pages, publish without review, and accidentally delete critical assets.

This isn't just about security — it's about trust and workflow. The French translator should only edit French pages. The graphic designer should only access the image library, not page content. The marketing lead should see a clean approval queue without wading through all draft pages. Wagtail's permission system makes this possible with fine-grained controls that map to real editorial roles. By the end of this lesson, you'll have a governance model that gives each person exactly the access they need — and nothing more.

3. Core Concepts

3.1 Wagtail's Permission Architecture

Wagtail extends Django's built-in authentication and permission framework (django.contrib.auth) with Wagtail-specific permissions. The hierarchy has four layers:

  • Users — Individual accounts. A user can be a superuser (full access, bypasses all permissions) or a regular user with limited rights.
  • Groups — Collections of users that share the same permissions. A user can belong to multiple groups, and permissions are additive (union of all group permissions).
  • Permissions — Individual rights such as "Can edit page" or "Can publish page." These are grouped by model (Page, Image, Document, Snippet types).
  • Collections — Containers for assets (images, documents) that can have their own permission rules, giving you folder-level access control.

3.2 Page-Level Permissions vs. Model-Level Permissions

This distinction is crucial and often misunderstood:

  • Model-level permissions control whether a user can perform actions on any page at all: add page, edit page, publish page, or delete page. These are set per page model (e.g., BlogPage, EventPage).
  • Page-level permissions restrict those actions to specific subtrees of the page hierarchy. For example, an editor might have "edit" permission on BlogPage model, but only on pages under the "/news/" subtree — not under "/about/".

Wagtail combines both: you assign model-level permissions via groups, then restrict scope via the Page Explorer or the GroupPagePermission model.

3.3 The Three-Tier Workflow: Draft, Moderation, Published

By default (without custom workflows), Wagtail has three content states:

  1. Draft — Only the creator and superusers can see and edit it. No one else can view the live version (there isn't one yet).
  2. Submitted for moderation — The creator has finished editing and submitted the page for review. Users with "publish" or "approve" permission can review and either publish or send back for revision.
  3. Published — Visible to the public. Only users with explicit publish permission can move a page from moderation to published.

This built-in moderation system can be extended with custom Workflows (Wagtail 4.0+) for multi-step approval chains — but the three-tier model covers most small-to-medium teams.

3.4 Collections and Snippet Permissions

Collections control access to Wagtail's asset library — images, documents, and optionally other snippet types. Each collection can have its own set of permissions:

  • Can add — Upload assets into this collection
  • Can edit — Modify asset metadata (title, alt text, tags)
  • Can delete — Remove assets from this collection

If a user has no specific collection permission but has model-level "choose" or "access" permission, they can view assets across all collections but not modify them. This is perfect for writers who need to browse images but shouldn't delete them.

4. Hands-On Practice — Step by Step

4.1 Prerequisites: Verify Your Wagtail Setup

Before creating permissions, ensure you have a running Wagtail project with at least two page models (e.g., BlogPage and GenericPage) and a few users besides yourself. If you're following the series, your existing Wagtail project from lesson 7 works perfectly.

# Create test users for our permission groups
python manage.py shell -c "
from django.contrib.auth.models import User

# A content writer
User.objects.create_user('alice', 'alice@example.com', 'password123')
# A reviewer
User.objects.create_user('bob', 'bob@example.com', 'password123')
# An editor with publishing rights
User.objects.create_user('carol', 'carol@example.com', 'password123')

print('Users created: alice, bob, carol')
"
Create test users for permission groups

4.2 Create Permission Groups in the Admin Panel

Navigate to Settings → Groups in the Wagtail admin. You can also create groups programmatically. Let's build three groups that map to real-world roles:

python manage.py shell -c "
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from wagtail.permissions import collection_permission_policy
from wagtail.images.models import Image
from wagtail.documents.models import Document

# --- Group 1: Content Writer (can create and edit drafts only) ---
writer_group, _ = Group.objects.get_or_create(name='Content Writer')

# Model-level permissions: can add and edit pages (NOT publish or delete)
page_ct = ContentType.objects.get(app_label='wagtailcore', model='page')
for codename in ['add_page', 'change_page']:
    perm = Permission.objects.get(
        content_type=page_ct, codename=codename)
    writer_group.permissions.add(perm)

# Can choose images (browse the library) but not upload/edit/delete
choose_img = Permission.objects.get(
    content_type=ContentType.objects.get_for_model(Image),
    codename='choose_image')
writer_group.permissions.add(choose_img)

# Can choose documents
choose_doc = Permission.objects.get(
    content_type=ContentType.objects.get_for_model(Document),
    codename='choose_document')
writer_group.permissions.add(choose_doc)

# Wagtail Admin access
admin_access = Permission.objects.get(
    content_type=page_ct, codename='access_admin')
writer_group.permissions.add(admin_access)

print('Created: Content Writer group')

# --- Group 2: Moderator (can edit and submit for approval) ---
mod_group, _ = Group.objects.get_or_create(name='Moderator')
for codename in ['add_page', 'change_page']:
    perm = Permission.objects.get(
        content_type=page_ct, codename=codename)
    mod_group.permissions.add(perm)

# Can publish pages (moderators can approve submissions)
publish_perm = Permission.objects.get(
    content_type=page_ct, codename='publish_page')
mod_group.permissions.add(publish_perm)

# Asset management
for model in [Image, Document]:
    ct = ContentType.objects.get_for_model(model)
    for codename in ['add', 'change', 'choose']:
        perm = Permission.objects.get(
            content_type=ct, codename=f'{codename}_{model._meta.model_name}')
        mod_group.permissions.add(perm)

mod_group.permissions.add(admin_access)
print('Created: Moderator group')

# --- Group 3: Admin Editor (full content control) ---
editor_group, _ = Group.objects.get_or_create(name='Admin Editor')
for codename in ['add_page', 'change_page', 'publish_page', 'delete_page']:
    perm = Permission.objects.get(
        content_type=page_ct, codename=codename)
    editor_group.permissions.add(perm)

for model in [Image, Document]:
    ct = ContentType.objects.get_for_model(model)
    for codename in ['add', 'change', 'delete', 'choose']:
        perm = Permission.objects.get(
            content_type=ct, codename=f'{codename}_{model._meta.model_name}')
        editor_group.permissions.add(perm)

editor_group.permissions.add(admin_access)
print('Created: Admin Editor group')

print('\\nAll groups created successfully!')
"
Create permission groups programmatically

4.3 Assign Users to Groups

Now assign our test users to their respective groups:

python manage.py shell -c "
from django.contrib.auth.models import User, Group
from django.contrib.auth.hashers import make_password

# Reset passwords to something we know
for username, pw in [('alice', 'write123'), ('bob', 'mod123'), ('carol', 'admin123')]:
    user = User.objects.get(username=username)
    user.password = make_password(pw)
    user.is_staff = True  # Required for Wagtail admin access
    user.save()

# Assign groups
alice = User.objects.get(username='alice')
alice.groups.add(Group.objects.get(name='Content Writer'))

try:
    bob = User.objects.get(username='bob')
    bob.groups.add(Group.objects.get(name='Moderator'))
except User.DoesNotExist:
    print('Bob not found - create him first')

carol = User.objects.get(username='carol')
carol.groups.add(Group.objects.get(name='Admin Editor'))

print('Users assigned to groups!')
"
Assign users to permission groups

4.4 Restrict Page-Level Access to Subtrees

Model-level permissions give users the ability to work with all pages of a given type. To limit a user to a specific subtree (e.g., only French locale pages), use page-level permissions. In Wagtail's admin, go to Settings → Groups → [Group Name] → Page Permissions and select the root page for each group's accessible area.

Programmatically, you create GroupPagePermission records:

python manage.py shell -c "
from django.contrib.auth.models import Group
from wagtail.models import Page, GroupPagePermission

# Get the root of the French locale tree
# In a multi-lingual setup (from lesson 7), each locale has a root
french_homepage = Page.objects.filter(
    locale__language_code='fr',
    slug='home'
).first()

if french_homepage:
    writer_group = Group.objects.get(name='Content Writer')
    GroupPagePermission.objects.create(
        group=writer_group,
        page=french_homepage,
        permission_type='add'
    )
    GroupPagePermission.objects.create(
        group=writer_group,
        page=french_homepage,
        permission_type='edit'
    )
    print(f'Restricted Content Writer to subtree rooted at: {french_homepage.title}')
else:
    print('French homepage not found - skip subtree restriction')
    print('(This is fine if you have not set up multi-lingual content)')
"
Restrict access to specific page subtrees

4.5 Configure Collection-Based Asset Access

Collections let you organize images and documents into folders with their own permission rules. Create collections for each team and restrict access:

python manage.py shell -c "
from wagtail.models import Collection
from wagtail.permissions import collection_permission_policy

# Create team collections
marketing_collection, _ = Collection.objects.get_or_create(
    name='Marketing Assets',
    parent=Collection.objects.filter(depth=1).first()  # Root collection
)

technical_collection, _ = Collection.objects.get_or_create(
    name='Technical Documentation',
    parent=Collection.objects.filter(depth=1).first()
)

print(f'Created collections: {marketing_collection.name}, {technical_collection.name}')
print('\\nNow set permissions in Admin: Settings -> Groups -> [Group] -> Collections')
print('Give Content Writer: Can choose (Marketing Assets)')
print('Give Moderator: Can add, change, choose (Marketing Assets)')
print('Give Admin Editor: All permissions (both collections)')
"
Create collections for asset access control

4.6 Testing Permissions as Each User

The best way to verify your permission setup is to log in as each user and test. You can simulate this from the shell using Wagtail's permission utilities:

python manage.py shell -c "
from django.contrib.auth.models import User
from wagtail.models import Page

# Test Alice's permissions
alice = User.objects.get(username='alice')
root = Page.objects.get(slug='home')

print(f'Testing permissions for: {alice.username}')
print(f'  Can add child pages under root: {root.permissions_for_user(alice).can_add_subpage()}')
print(f'  Can publish any page: {root.permissions_for_user(alice).can_publish()}')
print(f'  Can delete any page: {root.permissions_for_user(alice).can_delete()}')
print()

carol = User.objects.get(username='carol')
print(f'Testing permissions for: {carol.username}')
print(f'  Can add child pages under root: {root.permissions_for_user(carol).can_add_subpage()}')
print(f'  Can publish any page: {root.permissions_for_user(carol).can_publish()}')
print(f'  Can delete any page: {root.permissions_for_user(carol).can_delete()}')
"
Verify permissions for each user

5. Common Errors & Solutions

  1. "You do not have permission to access this page" — The user lacks the access_admin permission. This is the most common Wagtail permission mistake. Every admin user needs this permission explicitly — it's not granted by being staff. Fix: Add the access_admin permission to the user's group.
  2. User can see pages but cannot publish — Missing the publish_page permission at the model level, or the page-level permission was set to "edit" without "publish." Check both model permissions and GroupPagePermission records.
  3. User cannot upload images but can see the image chooser — The user has choose_image permission but not add_image. Add the add_image permission, and ensure the user has "Can add" on the relevant collection.
  4. Group permissions don't seem to take effect — Wagtail caches permission checks. If you change a group's permissions, affected users may need to log out and back in. In development, restart the server to clear the cache.
  5. User sees all collections despite collection restrictions — Superusers always bypass collection permissions. Check if the user has is_superuser=True. Regular users' collection access is determined by both model-level permissions and per-collection group permissions.

6. Summary Checklist

  • Created user groups: Content Writer, Moderator, Admin Editor
  • Assigned model-level permissions (add, edit, publish, delete) per group
  • Added access_admin permission to each group
  • Set page-level permissions to restrict groups to specific locale or section subtrees
  • Created collections (Marketing Assets, Technical Documentation) with per-collection permissions
  • Assigned users to groups and verified they have is_staff=True
  • Tested each user's effective permissions using the permissions_for_user() API
  • Logged out and back in to clear permission cache after changes
  • Documented the permission model for your team's reference

7. Practice Exercise

Challenge: Build a Multi-Tier Editorial Workflow

You're the lead developer for a news website with four teams:

  • Reporters — Write drafts for news articles only. Cannot publish. Can only access the "News" subtree.
  • Section Editors — Edit and submit drafts for moderation. Can approve content from reporters. Can access all articles.
  • Editor-in-Chief — Can publish any content. Can delete pages. Access to all sections.
  • Photographers — Can upload and edit images in the "Photo Library" collection. Cannot create or edit pages.

Your tasks:

  1. Create four groups with the permissions described above
  2. Create four test users (one per group)
  3. Set up a restricted page subtree for the "News" section
  4. Create a "Photo Library" collection with photographer-only upload access
  5. Have the Reporter create a draft page, the Section Editor approve it, and the Editor-in-Chief publish it
  6. Verify that the Photographer cannot create pages but can upload images

Bonus: Use Wagtail 4.0+ Workflows to formalize this as an actual two-step approval workflow with tasks assigned to each role.

8. Next Steps

You've now implemented a complete content governance model! Your team members have exactly the access they need — writers write, moderators approve, and editors publish — all within their designated sections of the site. This foundation enables safe collaboration at any scale.

What's next? The Wagtail CMS series continues with Wagtail API & Headless Frontends: Building Decoupled Applications, where you'll learn how to expose your Wagtail content via the REST API and GraphQL, build a decoupled frontend in React or Next.js, handle authentication for headless setups, and optimize API performance with edge caching.

Further resources:

Common pitfalls to avoid:

  • Don't forget the access_admin permission — without it, users get the "no permission" page even with full page permissions
  • Avoid granting is_superuser to non-admin users — it bypasses every permission check
  • Always test permissions by logging in as the actual user, not by inspecting the admin — caching can make manual checks misleading
  • When restricting page subtrees, remember that permissions are inherited from ancestor pages — setting a restriction at a high level affects all descendants
  • Document your permission groups as you create them — after six months, no one remembers why "Group 3" was created

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen July 11, 2026

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment