Of course! Integrating Oso Cloud with a Wagtail application is a fantastic way to handle sophisticated permissions beyond what's available out-of-the-box. Since Wagtail is built on Django, we'll be using the Python SDK and focusing on how to model Wagtail's concepts like Pages, Users, and Groups within Oso.
Let's walk through this step-by-step, following a similar path to the Oso Cloud Quickstart, but tailored specifically for your Wagtail project.
We'll implement a role-based access control (RBAC) system where users can be assigned "Editor" or "Viewer" roles on specific sections of your Wagtail site (i.e., on specific Page nodes and their descendants).
Objective: Get your Oso Cloud account ready and install the necessary tools in your Wagtail project.
-
Create Your Oso Cloud Account: If you haven't already, sign up for a free developer account.
- Go to: https://ui.osohq.com/ and complete the signup process.
-
Install the Oso Cloud Python SDK: In your Wagtail project's virtual environment, install the Python library.
pip install oso-cloud
-
Get Your API Key: You'll need an API key to connect your app to Oso Cloud.
- Navigate to: https://ui.osohq.com/settings/?api-keys
- Click "Create development API key".
- Give it a name like "Wagtail Dev". We'll start with a "Read-Only" key for now to be safe.
- Copy the key immediately and store it securely.
-
Configure Your Wagtail Settings: The best practice is to store the API key in an environment variable, not in your code. Add it to your
.envfile or your server's environment..env file:
OSO_API_KEY="paste_your_api_key_here"Then, ensure your Wagtail
settings.pycan read it:# myproject/settings/base.py import os OSO_API_KEY = os.environ.get('OSO_API_KEY')
Checkpoint: You should have an Oso Cloud account, the oso-cloud package installed, and your API key configured in your Wagtail project's settings.
Objective: Create a policy in Oso Cloud that understands Wagtail concepts like User, Group, and Page.
Our model will be:
- A
Usercan be a member of aGroup. - A
Groupcan have arole(like "editor") on aPage. - Permissions are inherited down the page tree (if you have permission on a parent page, you have it on its children).
-
Navigate to the Rules Editor:
-
Write the Policy: Click "Edit as Code" and paste the following Polar policy. This is a great starting point for a CMS.
# Define the resource types we'll be working with. # We use Django's User and Group, and Wagtail's Page. resource User { } resource Group { } resource Page { permissions = ["view", "edit", "publish"]; roles = ["viewer", "editor", "publisher"]; } # An "editor" can view and edit. has_permission(actor: Actor, "view", resource: Page) if has_role(actor, "editor", resource); has_permission(actor: Actor, "edit", resource: Page) if has_role(actor, "editor", resource); # A "viewer" can only view. has_permission(actor: Actor, "view", resource: Page) if has_role(actor, "viewer", resource); # Users inherit roles from the groups they are in. has_role(user: User, role: String, page: Page) if group in user.groups and has_role(group, role, page); # Roles on a parent page are inherited by its children. # This is the magic for Wagtail's tree structure! has_role(actor: Actor, role: String, page: Page) if parent in page.ancestors and has_role(actor, role, parent); # The main allow rule. This is what Oso checks. allow(actor, action, resource) if has_permission(actor, action, resource);
-
Deploy the Policy: Click the Deploy button in the top right corner.
Checkpoint: Your policy is deployed. It defines User, Group, and Page resources and sets up inheritance for both groups and the page hierarchy.
Objective: Tell Oso about your users, groups, and pages so the policy can make decisions.
Facts are the data part of the equation. We need to tell Oso:
- Which users are in which groups.
- Which groups have which roles on which pages.
A great way to do this in Django is with signals or a management command. Let's create a management command to sync a specific group's role.
-
Create a Management Command:
- In one of your Django apps (e.g.,
home), create the following file structure:home/ └── management/ └── commands/ └── __init__.py └── sync_oso_roles.py
- In one of your Django apps (e.g.,
-
Write the Command Logic:
# home/management/commands/sync_oso_roles.py import os from django.core.management.base import BaseCommand from django.contrib.auth.models import Group from wagtail.models import Page from oso_cloud import Oso class Command(BaseCommand): help = 'Syncs initial roles and relationships to Oso Cloud' def handle(self, *args, **options): # 1. Initialize the Oso client oso = Oso(api_key=os.environ['OSO_API_KEY']) self.stdout.write("Connecting to Oso Cloud...") # 2. Get our Django/Wagtail objects try: # Let's say we have a group called "Web Editors" editors_group = Group.objects.get(name="Web Editors") # And we want to give them editor access to the "Blog" section blog_index_page = Page.objects.get(slug="blog", depth=3) # Be specific to get the right page except (Group.DoesNotExist, Page.DoesNotExist) as e: self.stderr.write(self.style.ERROR(f"Error: Could not find required Group or Page. {e}")) self.stderr.write("Please create a 'Web Editors' group and a 'blog' page before running.") return # 3. Create the "fact" in Oso Cloud # Tell Oso that the "Web Editors" group has the "editor" role on the blog page. # We use a f-string to create unique IDs for Oso. group_fact = { "name": "has_role", "args": [ {"type": "Group", "id": str(editors_group.pk)}, "editor", {"type": "Page", "id": str(blog_index_page.pk)} ] } oso.tell(group_fact) self.stdout.write(self.style.SUCCESS( f"Successfully granted 'editor' role to Group:'{editors_group.name}' on Page:'{blog_index_page.title}'" )) # You would also sync user-group memberships here # For example, for every user in editors_group: for user in editors_group.user_set.all(): user_group_fact = { "name": "has_relationship", "args": [ {"type": "User", "id": str(user.pk)}, "groups", # The relation name from our policy {"type": "Group", "id": str(editors_group.pk)} ] } oso.tell(user_group_fact) self.stdout.write(f" - Synced user {user.username} to group {editors_group.name}")
-
Run the Command:
- First, make sure you've created a Django
Groupnamed "Web Editors" and have a user in it. Make sure you have aPagewith the slug "blog". - Then run the command:
python manage.py sync_oso_roles
- First, make sure you've created a Django
Checkpoint: The command should run successfully. You can verify the facts were created by going to the Facts page in the Oso console: https://ui.osohq.com/facts/.
Objective: Before writing any enforcement code, let's verify our policy and data work as expected in the Oso Cloud Explain console.
Let's assume:
- Your "Web Editors" group has ID
1. - Your "Blog" page has ID
5. - A user named "alice" (ID
2) is in the "Web Editors" group. - A blog post under the blog index has ID
6.
-
Navigate to the Explain Console:
-
Run Tests:
-
Can Alice edit the main blog page?
- Query:
allow User:2 "edit" Page:5 - Expected Result: True (because she's in the group with the role)
- Query:
-
Can Alice edit a child page of the blog?
- Query:
allow User:2 "edit" Page:6 - To make this work, we need to tell Oso that page 6 is a child of page 5. We'd add an "ancestors" relationship fact:
has_relationship(Page:6, "ancestors", Page:5). - Expected Result: True (due to the parent inheritance rule)
- Query:
-
Can Alice publish the blog page?
- Query:
allow User:2 "publish" Page:5 - Expected Result: False (because our "editor" role doesn't grant "publish")
- Query:
-
Checkpoint: Your tests in the Explain console match the expected outcomes. This proves your logic is sound before you integrate it into your code.
Objective: Use the oso.authorize() method in your Wagtail views or hooks to protect actions.
A clean way to do this is with a decorator.
-
Create an Authorization Decorator:
# In a utils.py file or similar import os from functools import wraps from django.core.exceptions import PermissionDenied from oso_cloud import Oso, OsoError def oso_authorize(action, resource_arg_name="page"): """ A decorator to authorize an action against a resource using Oso. """ def decorator(view_func): @wraps(view_func) def _wrapped_view(request, *args, **kwargs): # Get the resource from the view's kwargs (e.g., the 'page' object) resource = kwargs.get(resource_arg_name) if not resource: raise ValueError(f"Resource '{resource_arg_name}' not found in view kwargs.") try: oso = Oso(api_key=os.environ['OSO_API_KEY']) oso.authorize( actor={"type": "User", "id": str(request.user.pk)}, action=action, resource={"type": resource.__class__.__name__, "id": str(resource.pk)} ) except OsoError as e: # Log the error for debugging print(f"Authorization failed: {e}") raise PermissionDenied # If authorize succeeds, proceed with the view return view_func(request, *args, **kwargs) return _wrapped_view return decorator
-
Apply the Decorator to a Wagtail View: Wagtail's
serveview is the most common one. You can't easily decorate it directly, but you can use Wagtail'sbefore_serve_pagehook.wagtail_hooks.py:
# In any of your app's wagtail_hooks.py import os from django.core.exceptions import PermissionDenied from wagtail import hooks from oso_cloud import Oso, OsoError # We'll check for "view" permission on every page serve @hooks.register('before_serve_page') def check_page_view_permission(page, request, serve_args, serve_kwargs): # Allow anonymous users to view public pages if you want if not request.user.is_authenticated: # Here you might have a public role check return try: oso = Oso(api_key=os.environ['OSO_API_KEY']) oso.authorize( actor={"type": "User", "id": str(request.user.pk)}, action="view", resource={"type": "Page", "id": str(page.pk)} ) except OsoError: # If Oso says no, deny permission raise PermissionDenied("You do not have permission to view this page.") # If we get here, the user is allowed. The request continues. return
-
Protecting Wagtail Admin Views: To protect the "edit" view, you can use the
before_edit_pagehook.# In the same wagtail_hooks.py @hooks.register('before_edit_page') def check_page_edit_permission(request, page): try: oso = Oso(api_key=os.environ['OSO_API_KEY']) oso.authorize( actor={"type": "User", "id": str(request.user.pk)}, action="edit", resource={"type": "Page", "id": str(page.pk)} ) except OsoError: raise PermissionDenied("You do not have permission to edit this page.") return
You've now got a solid foundation! From here you can:
- Automate Fact Syncing: Use Django signals (
post_save,post_delete,m2m_changed) to automatically update Oso facts when you save a page, change a user's group, etc. - Expand Your Policy: Add more roles ("publisher", "admin") and permissions.
- List Authorized Resources: Use
oso.list()to build menus or lists of pages that a user is allowed to see or edit. - Explore Authorization Academy: Dive deeper into advanced modeling concepts at the Oso Authorization Academy.
Welcome to Oso! Let me know if you have any questions about a specific step.