Skip to content

Instantly share code, notes, and snippets.

@RobSpectre
Created June 24, 2025 17:52
Show Gist options
  • Save RobSpectre/8bca0c996d5b6ab72cfb99cb0e1e1ba2 to your computer and use it in GitHub Desktop.
Save RobSpectre/8bca0c996d5b6ab72cfb99cb0e1e1ba2 to your computer and use it in GitHub Desktop.
Gemini guide to integrate Oso with Wagtail

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.

The Goal:

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).


Phase 1: Account & Project Setup

Objective: Get your Oso Cloud account ready and install the necessary tools in your Wagtail project.

  1. Create Your Oso Cloud Account: If you haven't already, sign up for a free developer account.

  2. Install the Oso Cloud Python SDK: In your Wagtail project's virtual environment, install the Python library.

    pip install oso-cloud
  3. 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.
  4. 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 .env file or your server's environment.

    .env file:

    OSO_API_KEY="paste_your_api_key_here"
    

    Then, ensure your Wagtail settings.py can 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.


Phase 2: Modeling Wagtail Permissions in Polar

Objective: Create a policy in Oso Cloud that understands Wagtail concepts like User, Group, and Page.

Our model will be:

  • A User can be a member of a Group.
  • A Group can have a role (like "editor") on a Page.
  • Permissions are inherited down the page tree (if you have permission on a parent page, you have it on its children).
  1. Navigate to the Rules Editor:

  2. 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);
  3. 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.


Phase 3: Syncing Wagtail Data to Oso Cloud as "Facts"

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.

  1. 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
      
  2. 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}")
  3. Run the Command:

    • First, make sure you've created a Django Group named "Web Editors" and have a user in it. Make sure you have a Page with the slug "blog".
    • Then run the command:
    python manage.py sync_oso_roles

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/.


Phase 4: Testing in the Console

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.
  1. Navigate to the Explain Console:

  2. 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)
    • 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)
    • Can Alice publish the blog page?

      • Query: allow User:2 "publish" Page:5
      • Expected Result: False (because our "editor" role doesn't grant "publish")

Checkpoint: Your tests in the Explain console match the expected outcomes. This proves your logic is sound before you integrate it into your code.


Phase 5: Enforcing Permissions in Wagtail

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.

  1. 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
  2. Apply the Decorator to a Wagtail View: Wagtail's serve view is the most common one. You can't easily decorate it directly, but you can use Wagtail's before_serve_page hook.

    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
  3. Protecting Wagtail Admin Views: To protect the "edit" view, you can use the before_edit_page hook.

    # 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

Next Steps

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment