pundit v0.10.0
Pundit
A simple Crystal shard for managing authorization in Lucky applications. Intended to mimic the excellent Ruby Pundit gem.
This shard is very much still a work in progress. I'm using it in my own production apps, but the API is subject to major breaking changes and reworks until I tag v1.0.
Lucky Installation
-
Add the dependency to your
shard.yml:# shard.yml dependencies: pundit: github: stephendolan/pundit -
Run
shards install -
Require the shard in your Lucky application
# shards.cr require "pundit" -
Require the tasks in your Lucky application
# tasks.cr require "pundit/tasks/**" -
Require a new directory for policy definitions
# app.cr require "./policies/**" -
Include the
Pundit::ActionHelpersmodule inBrowserAction:# src/actions/browser_action.cr include Pundit::ActionHelpers(User) -
(Optional) Capture
Punditexceptions insrc/actions/errors/show.crwith a new#renderoverride:# Capture Pundit authorization exceptions to handle it elegantly def render(error : Pundit::NotAuthorizedError) if html? error_html "Sorry, you're not authorized to access that", status: 401 else error_json "Not authorized", status: 401 end end -
Run the initializer to create your
ApplicationPolicyif you don't want the default:lucky pundit.init
Usage
Creating policies
The easiest way to create new policies is to use the built-in Lucky task! After following the steps in the Installation section, simply run lucky gen.policy Book, for example, to create a new BookPolicy in your application.
Your policies must inherit from the provided ApplicationPolicy(T) abstract class, where T is the model you are authorizing against.
For example, the BookPolicy we created with lucky gen.policy Book might look like this:
class BookPolicy < ApplicationPolicy(Book)
def index?
# If you want to either allow or deny all visitors, simply return `true` or `false`
true
end
def show?
# You can reference other methods if you want to share authorization between them
update?
end
def create?
# Only signed-in users can create books
return false unless signed_in_user = user
end
def update?
# Only the owner of a book can update it
return false unless requested_book = record
requested_book.owner == user
end
def delete?
# You can reference other methods if you want to share authorization between them
update?
end
end
The following methods are provided in ApplicationPolicy:
| Method Name | Default Value |
|---|---|
index? |
false |
show? |
false |
create? |
false |
new? |
create? |
update? |
false |
edit? |
update? |
delete? |
false |
Authorizing actions
Let's say we have a Books::Index action that looks like this:
class Books::Index < BrowserAction
get "/books/index" do
html IndexPage, books: BookQuery.new
end
end
To use Pundit for authorization, simply add an authorize call:
class Books::Index < BrowserAction
get "/books/index" do
authorize
html IndexPage, books: BookQuery.new
end
end
Behind the scenes, this is using the action's class name to check whether the BookPolicy's index? method is permitted for current_user. If the call fails, a Pundit::NotAuthorizedError is raised.
The authorize call above is identical to writing this:
BookPolicy.new(current_user).index? || raise Pundit::NotAuthorizedError.new
You can also leverage specific records in your authorization. For example, say we have a Books::Update action that looks like this:
post "/books/:book_id/update" do
book = BookQuery.find(book_id)
SaveBook.update(book, params) do |operation, book|
redirect Home::Index
end
end
We can add an authorize call to check whether or not the user is permitted to update this specific book like this:
post "/books/:book_id/update" do
book = BookQuery.find(book_id)
authorize(book)
SaveBook.update(book, params) do |operation, book|
redirect Home::Index
end
end
Authorizing views
Say we have a button to create a new book:
def render
button "Create new book"
end
To ensure that the current_user is permitted to create a new book before showing the button, we can wrap the button in a policy check:
def render
if BookPolicy.new(current_user).create?
button "Create new book"
end
end
Overriding the User model
If your application doesn't return an instance of User from your current_user method, you'll need to make the following updates (we're using Account as an example):
-
Run
lucky pundit.init --user-model {Account}, or modify yourApplicationPolicy'sinitializecontent like this:abstract class ApplicationPolicy(T) getter account getter record def initialize(@account : Account?, @record : T? = nil) end end -
Update the
includeof thePundit::ActionHelpersmodule inBrowserAction:# src/actions/browser_action.cr include Pundit::ActionHelpers(Account)
Handling authorization errors
If a call to authorize fails, a Pundit::NotAuthorizedError will be raised.
You can handle this elegantly by adding an overloaded render method to your src/actions/errors/show.cr action:
# This class handles error responses and reporting.
#
# https://luckyframework.org/guides/http-and-routing/error-handling
class Errors::Show < Lucky::ErrorAction
DEFAULT_MESSAGE = "Something went wrong."
default_format :html
# Capture Pundit authorization exceptions to handle it elegantly
def render(error : Pundit::NotAuthorizedError)
if html?
# We might want to throw an appropriate status and message
error_html "Sorry, you're not authorized to access that", status: 401
# Or maybe we just redirect users back to the previous page
# redirect_back fallback: Home::Index
else
error_json "Not authorized", status: 401
end
end
end
Contributing
- Fork it (https://github.com/stephendolan/pundit/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Stephen Dolan - creator and maintainer
Inspiration
- The Pundit Ruby gem was what formed my need as a programmer for this kind of simple approach to authorization
- The Praetorian Crystal shard took an excellent first step towards proving out the Pundit model in Crystal
pundit
- 18
- 2
- 2
- 3
- 3
- about 2 years ago
- November 30, 2020
MIT License
Tue, 25 Nov 2025 12:39:44 GMT