{"id":17682245,"url":"https://github.com/stephendolan/pundit","last_synced_at":"2025-11-07T11:05:07.049Z","repository":{"id":43063364,"uuid":"317103398","full_name":"stephendolan/pundit","owner":"stephendolan","description":"Authorization for Lucky Crystal Apps","archived":false,"fork":false,"pushed_at":"2023-09-05T09:45:40.000Z","size":151,"stargazers_count":18,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-04-20T18:39:32.363Z","etag":null,"topics":["authorization","crystal-shard","lucky","lucky-framework","shard"],"latest_commit_sha":null,"homepage":"https://stephendolan.github.io/pundit","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stephendolan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/funding.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"stephendolan"}},"created_at":"2020-11-30T03:44:04.000Z","updated_at":"2024-03-01T16:39:40.000Z","dependencies_parsed_at":"2024-10-24T13:33:52.550Z","dependency_job_id":null,"html_url":"https://github.com/stephendolan/pundit","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephendolan%2Fpundit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephendolan%2Fpundit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephendolan%2Fpundit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stephendolan%2Fpundit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stephendolan","download_url":"https://codeload.github.com/stephendolan/pundit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253830786,"owners_count":21970998,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authorization","crystal-shard","lucky","lucky-framework","shard"],"created_at":"2024-10-24T09:13:19.062Z","updated_at":"2025-11-07T11:05:07.001Z","avatar_url":"https://github.com/stephendolan.png","language":"Crystal","funding_links":["https://github.com/sponsors/stephendolan"],"categories":[],"sub_categories":[],"readme":"# Pundit\n\n![Shard CI](https://github.com/stephendolan/pundit/workflows/Shard%20CI/badge.svg)\n[![API Documentation Website](https://img.shields.io/website?down_color=red\u0026down_message=Offline\u0026label=API%20Documentation\u0026up_message=Online\u0026url=https%3A%2F%2Fstephendolan.github.io%2Fpundit%2F)](https://stephendolan.github.io/pundit)\n[![GitHub release](https://img.shields.io/github/release/stephendolan/pundit.svg?label=Release)](https://github.com/stephendolan/pundit/releases)\n\nA 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.\n\nThis 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.\n\n## Lucky Installation\n\n1. Add the dependency to your `shard.yml`:\n\n   ```yaml\n   # shard.yml\n   dependencies:\n     pundit:\n       github: stephendolan/pundit\n   ```\n\n1. Run `shards install`\n\n1. Require the shard in your Lucky application\n\n   ```crystal\n   # shards.cr\n   require \"pundit\"\n   ```\n\n1. Require the tasks in your Lucky application\n\n   ```crystal\n   # tasks.cr\n   require \"pundit/tasks/**\"\n   ```\n\n1. Require a new directory for policy definitions\n\n   ```crystal\n   # app.cr\n   require \"./policies/**\"\n   ```\n\n1. Include the `Pundit::ActionHelpers` module in `BrowserAction`:\n\n   ```crystal\n   # src/actions/browser_action.cr\n   include Pundit::ActionHelpers(User)\n   ```\n\n1. (Optional) Capture `Pundit` exceptions in `src/actions/errors/show.cr` with a new `#render` override:\n\n   ```crystal\n   # Capture Pundit authorization exceptions to handle it elegantly\n   def render(error : Pundit::NotAuthorizedError)\n     if html?\n       error_html \"Sorry, you're not authorized to access that\", status: 401\n     else\n       error_json \"Not authorized\", status: 401\n     end\n   end\n   ```\n\n1. Run the initializer to create your `ApplicationPolicy` if you don't want [the default](src/pundit/application_policy.cr):\n\n   ```sh\n   lucky pundit.init\n   ```\n\n## Usage\n\n### Creating policies\n\nThe 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.\n\nYour policies must inherit from the provided [`ApplicationPolicy(T)`](src/pundit/application_policy.cr) abstract class, where `T` is the model you are authorizing against.\n\nFor example, the `BookPolicy` we created with `lucky gen.policy Book` might look like this:\n\n```crystal\nclass BookPolicy \u003c ApplicationPolicy(Book)\n  def index?\n    # If you want to either allow or deny all visitors, simply return `true` or `false`\n    true\n  end\n\n  def show?\n    # You can reference other methods if you want to share authorization between them\n    update?\n  end\n\n  def create?\n    # Only signed-in users can create books\n    return false unless signed_in_user = user\n  end\n\n  def update?\n    # Only the owner of a book can update it\n    return false unless requested_book = record\n    \n    requested_book.owner == user\n  end\n\n  def delete?\n    # You can reference other methods if you want to share authorization between them\n    update?\n  end\nend\n```\n\nThe following methods are provided in [`ApplicationPolicy`](src/pundit/application_policy.cr):\n\n| Method Name | Default Value |\n| ----------- | ------------- |\n| `index?`    | `false`       |\n| `show?`     | `false`       |\n| `create?`   | `false`       |\n| `new?`      | `create?`     |\n| `update?`   | `false`       |\n| `edit?`     | `update?`     |\n| `delete?`   | `false`       |\n\n### Authorizing actions\n\nLet's say we have a `Books::Index` action that looks like this:\n\n```crystal\nclass Books::Index \u003c BrowserAction\n  get \"/books/index\" do\n    html IndexPage, books: BookQuery.new\n  end\nend\n```\n\nTo use Pundit for authorization, simply add an `authorize` call:\n\n```crystal\nclass Books::Index \u003c BrowserAction\n  get \"/books/index\" do\n    authorize\n\n    html IndexPage, books: BookQuery.new\n  end\nend\n```\n\nBehind 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.\n\nThe `authorize` call above is identical to writing this:\n\n```crystal\nBookPolicy.new(current_user).index? || raise Pundit::NotAuthorizedError.new\n```\n\nYou can also leverage specific records in your authorization. For example, say we have a `Books::Update` action that looks like this:\n\n```crystal\npost \"/books/:book_id/update\" do\n  book = BookQuery.find(book_id)\n\n  SaveBook.update(book, params) do |operation, book|\n    redirect Home::Index\n  end\nend\n```\n\nWe can add an `authorize` call to check whether or not the user is permitted to update this specific book like this:\n\n```crystal\npost \"/books/:book_id/update\" do\n  book = BookQuery.find(book_id)\n\n  authorize(book)\n\n  SaveBook.update(book, params) do |operation, book|\n    redirect Home::Index\n  end\nend\n```\n\n### Authorizing views\n\nSay we have a button to create a new book:\n\n```crystal\ndef render\n  button \"Create new book\"\nend\n```\n\nTo 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:\n\n```crystal\ndef render\n  if BookPolicy.new(current_user).create?\n    button \"Create new book\"\n  end\nend\n```\n\n### Overriding the User model\n\nIf 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):\n\n- Run `lucky pundit.init --user-model {Account}`, or modify your `ApplicationPolicy`'s `initialize` content like this:\n\n  ```crystal\n  abstract class ApplicationPolicy(T)\n    getter account\n    getter record\n\n    def initialize(@account : Account?, @record : T? = nil)\n    end\n  end\n  ```\n\n- Update the `include` of the `Pundit::ActionHelpers` module in `BrowserAction`:\n\n  ```crystal\n  # src/actions/browser_action.cr\n  include Pundit::ActionHelpers(Account)\n  ```\n\n### Handling authorization errors\n\nIf a call to `authorize` fails, a `Pundit::NotAuthorizedError` will be raised.\n\nYou can handle this elegantly by adding an overloaded `render` method to your `src/actions/errors/show.cr` action:\n\n```crystal\n# This class handles error responses and reporting.\n#\n# https://luckyframework.org/guides/http-and-routing/error-handling\nclass Errors::Show \u003c Lucky::ErrorAction\n  DEFAULT_MESSAGE = \"Something went wrong.\"\n  default_format :html\n\n  # Capture Pundit authorization exceptions to handle it elegantly\n  def render(error : Pundit::NotAuthorizedError)\n    if html?\n      # We might want to throw an appropriate status and message\n      error_html \"Sorry, you're not authorized to access that\", status: 401\n\n      # Or maybe we just redirect users back to the previous page\n      # redirect_back fallback: Home::Index\n    else\n      error_json \"Not authorized\", status: 401\n    end\n  end\nend\n```\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/stephendolan/pundit/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## Contributors\n\n- [Stephen Dolan](https://github.com/stephendolan) - creator and maintainer\n\n## Inspiration\n\n- 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\n- The [Praetorian](https://github.com/ilanusse/praetorian) Crystal shard took an excellent first step towards proving out the Pundit model in Crystal\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstephendolan%2Fpundit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstephendolan%2Fpundit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstephendolan%2Fpundit/lists"}