Pundit
Authorization for Lucky Crystal Apps
Install / Use
/learn @stephendolan/PunditREADME
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
Related Skills
node-connect
353.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
111.7kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
353.3kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
353.3kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
