LocalSpace
Backend Core

Authorization

How authorization and access control is handled using Bouncer.

Authorization (determining what a user is allowed to do) is managed by AdonisJS Bouncer. This allows for a clean separation of authorization logic from your controllers and business logic.

1. Policies

Authorization rules are defined within Policy classes in the app/policies directory. Each policy typically corresponds to a Lucid model and contains methods that define the permissions for actions on that model.

For example, BlogPolicy.ts defines who can create, update, delete, or publish a blog post.

// app/policies/blog_policy.ts
import User from "#models/user";
import Workspace from "#models/workspace";
import Blog from "#models/blog";

export default class BlogPolicy {
  // A user can create a blog if they are an owner, manager, or editor of the workspace.
  async create(user: User, workspace: Workspace): Promise<boolean> {
    return await workspace.memberHasRole({
      user,
      roles: ["owner", "manager", "editor"],
    });
  }

  // A user can only update a blog if they have the right role AND the blog is a draft.
  async update(user: User, workspace: Workspace, blog: Blog): Promise<boolean> {
    const can = await workspace.memberHasRole({
      user,
      roles: ["owner", "manager", "editor"],
    });
    if (!can) return false;

    if (!blog.isDraft) {
      return AuthorizationResponse.deny("Blog is not in draft status");
    }

    return true;
  }
}

2. Using Policies in Controllers

Controllers use ctx.bouncer to authorize actions before executing them. This ensures that no unauthorized operations are performed.

The authorize method will automatically throw a ForbiddenException if the policy check fails, which is then handled by the global exception handler.

// app/controllers/customer/workspace/blog/create_controller.ts

// ...
const workspace = await Workspace.findOrFail(payload.params.workspaceId)

// Authorize the 'create' action on the 'BlogPolicy'
await ctx.bouncer.with('BlogPolicy').authorize('create', workspace)

// If authorization passes, the code continues...
const blog = await workspace.related('blogs').create({ ... })
// ...

3. Role-Based Logic

A key pattern in this project is that authorization is often dependent on a user's role within a specific context, such as a Workspace. The policies don't contain complex role-checking logic themselves. Instead, they delegate this to helper methods on the models.

In the BlogPolicy example above, the check workspace.memberHasRole(...) is a method on the Workspace model that encapsulates the logic for checking a user's membership and role within that specific workspace. This keeps the policies clean and readable.