https://github.com/stephendolan/pundit
Authorization for Lucky Crystal Apps
https://github.com/stephendolan/pundit
authorization crystal-shard lucky lucky-framework shard
Last synced: 5 months ago
JSON representation
Authorization for Lucky Crystal Apps
- Host: GitHub
- URL: https://github.com/stephendolan/pundit
- Owner: stephendolan
- License: mit
- Created: 2020-11-30T03:44:04.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2023-09-05T09:45:40.000Z (about 2 years ago)
- Last Synced: 2025-04-20T18:39:32.363Z (6 months ago)
- Topics: authorization, crystal-shard, lucky, lucky-framework, shard
- Language: Crystal
- Homepage: https://stephendolan.github.io/pundit
- Size: 147 KB
- Stars: 18
- Watchers: 0
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Funding: .github/funding.yml
- License: LICENSE
Awesome Lists containing this project
README
# Pundit

[](https://stephendolan.github.io/pundit)
[](https://github.com/stephendolan/pundit/releases)A simple Crystal shard for managing authorization in [Lucky](https://luckyframework.org) applications. Intended to mimic the excellent Ruby [Pundit](https://github.com/varvet/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
1. Add the dependency to your `shard.yml`:
```yaml
# shard.yml
dependencies:
pundit:
github: stephendolan/pundit
```1. Run `shards install`
1. Require the shard in your Lucky application
```crystal
# shards.cr
require "pundit"
```1. Require the tasks in your Lucky application
```crystal
# tasks.cr
require "pundit/tasks/**"
```1. Require a new directory for policy definitions
```crystal
# app.cr
require "./policies/**"
```1. Include the `Pundit::ActionHelpers` module in `BrowserAction`:
```crystal
# src/actions/browser_action.cr
include Pundit::ActionHelpers(User)
```1. (Optional) Capture `Pundit` exceptions in `src/actions/errors/show.cr` with a new `#render` override:
```crystal
# 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
```1. Run the initializer to create your `ApplicationPolicy` if you don't want [the default](src/pundit/application_policy.cr):
```sh
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)`](src/pundit/application_policy.cr) 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:
```crystal
class BookPolicy < ApplicationPolicy(Book)
def index?
# If you want to either allow or deny all visitors, simply return `true` or `false`
true
enddef show?
# You can reference other methods if you want to share authorization between them
update?
enddef create?
# Only signed-in users can create books
return false unless signed_in_user = user
enddef update?
# Only the owner of a book can update it
return false unless requested_book = record
requested_book.owner == user
enddef delete?
# You can reference other methods if you want to share authorization between them
update?
end
end
```The following methods are provided in [`ApplicationPolicy`](src/pundit/application_policy.cr):
| 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:
```crystal
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:
```crystal
class Books::Index < BrowserAction
get "/books/index" do
authorizehtml 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:
```crystal
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:
```crystal
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:
```crystal
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:
```crystal
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:
```crystal
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 your `ApplicationPolicy`'s `initialize` content like this:
```crystal
abstract class ApplicationPolicy(T)
getter account
getter recorddef initialize(@account : Account?, @record : T? = nil)
end
end
```- Update the `include` of the `Pundit::ActionHelpers` module in `BrowserAction`:
```crystal
# 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:
```crystal
# 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
1. Fork it ()
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request## Contributors
- [Stephen Dolan](https://github.com/stephendolan) - creator and maintainer
## Inspiration
- The [Pundit](https://github.com/varvet/pundit) Ruby gem was what formed my need as a programmer for this kind of simple approach to authorization
- The [Praetorian](https://github.com/ilanusse/praetorian) Crystal shard took an excellent first step towards proving out the Pundit model in Crystal