{"id":50736055,"url":"https://github.com/henrikac/gatekeeper.cr","last_synced_at":"2026-06-10T13:31:20.855Z","repository":{"id":324969343,"uuid":"1099289789","full_name":"henrikac/gatekeeper.cr","owner":"henrikac","description":"Lightweight authorization middleware with pluggable authenticators.","archived":false,"fork":false,"pushed_at":"2025-11-30T15:19:46.000Z","size":38,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-13T19:25:55.104Z","etag":null,"topics":["auth","authorization","crystal","crystal-lang"],"latest_commit_sha":null,"homepage":"","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/henrikac.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-11-18T20:15:03.000Z","updated_at":"2025-11-30T15:19:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/henrikac/gatekeeper.cr","commit_stats":null,"previous_names":["henrikac/kemal-guardian","henrikac/gatekeeper.cr"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/henrikac/gatekeeper.cr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrikac%2Fgatekeeper.cr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrikac%2Fgatekeeper.cr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrikac%2Fgatekeeper.cr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrikac%2Fgatekeeper.cr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/henrikac","download_url":"https://codeload.github.com/henrikac/gatekeeper.cr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrikac%2Fgatekeeper.cr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34155422,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["auth","authorization","crystal","crystal-lang"],"created_at":"2026-06-10T13:31:18.042Z","updated_at":"2026-06-10T13:31:20.848Z","avatar_url":"https://github.com/henrikac.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gatekeeper\n\n[![CI](https://github.com/henrikac/gatekeeper.cr/actions/workflows/ci.yml/badge.svg)](https://github.com/henrikac/gatekeeper.cr/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/henrikac/gatekeeper.cr)](https://github.com/henrikac/gatekeeper.cr/releases)\n[![License](https://img.shields.io/github/license/henrikac/gatekeeper.cr)](./LICENSE)\n\nA small authorization middleware with pluggable authenticators.\n- You define rules for your routes (using regex)\n- You define one or more authenticators that resolve a user identity\n- Gatekeeper checks whether the user is allowed to access the route\n\nIt does **not** implement login, sessions, JWT validation or password handling.\nYou bring the authentication mechanism — Gatekeeper enforces access rules.\n\n## Installation\n\n1. Add the dependency to your `shard.yml`:\n\n   ```yaml\n   dependencies:\n     gatekeeper:\n       github: henrikac/gatekeeper.cr\n   ```\n\n2. Run `shards install`\n\n## Usage\n\n### Configure Gatekeeper\n\n```crystal\nrequire \"gatekeeper\"\n\nGatekeeper.config do |config|\n  config.on_unauthenticated = Gatekeeper::ContextHandler.new do |ctx|\n    ctx.response.print \"You must log in first.\"\n  end\n\n  config.on_unauthorized = Gatekeeper::ContextHandler.new do |ctx|\n    ctx.response.print \"You do not have permission.\"\n  end\n\n  # Simple authenticator example\n  config.authenticators \u003c\u003c Gatekeeper.authenticator do |ctx|\n    Gatekeeper::IdentityUser(Int32).new(1, Set{\"admin\"})\n  end\n\n  # Optional: role hierarchy\n  # \"admin\" inherits permissions from \"user\" and \"reader\"\n  config.role_hierarchy = {\n    \"admin\"  =\u003e [\"user\", \"reader\"],\n    \"manager\" =\u003e [\"user\"],\n  }\nend\n\nGatekeeper.rules do |r|\n  r.allow_get \"/\"                     # allow GET / (exact match)\n  r.allow_post \"/login\"               # allow POST /login\n  r.allow \"/admin\", roles: [\"admin\"]  # exact match\n  r.allow /^\\/api/                    # prefix /api (regex as-is)\nend\n```\n\n### Basic example with HTTP::Server\n\n```crystal\nrequire \"http/server\"\nrequire \"gatekeeper\"\n\nclass AppHandler\n  include HTTP::Handler\n\n  def call(context)\n    case context.request.path\n    when \"/\"\n      context.response.print \"Hello World!\"\n    when \"/admin\"\n      context.response.print \"Hello admin!\"\n    else\n      context.response.status = HTTP::Status::NOT_FOUND\n      context.response.print \"Not found\"\n    end\n  end\nend\n\nGatekeeper.config do |config|\n  # …\nend\n\nhandlers = [\n  Gatekeeper::AuthHandler.new,\n  AppHandler.new,\n]\n\nserver = HTTP::Server.new(handlers)\n\naddress = server.bind_tcp \"0.0.0.0\", 3000\nputs \"Listening on http://#{address}\"\nserver.listen\n```\n\n### Basic example using Kemal\n\n```crystal\nrequire \"kemal\"\nrequire \"gatekeeper\"\n\nadd_handler Gatekeeper::AuthHandler.new\n\nGatekeeper.config do |config|\n  # …\nend\n\nget \"/\" do\n  \"Hello world\"\nend\n\nget \"/admin\" do\n  \"Hello admin\"\nend\n\nKemal.run\n```\n\n### How it works\n\nGatekeeper processes requests in this order:\n1. No rules defined: request is allowed\n2. Rule exists but does not match: request is allowed\n3. Rule matches but has no roles: request is allowed\n4. Rule matches + roles required → authenticators run\n  - If no authenticator returns a user → `401 Unauthorized`\n  - If a user exists but does not have a required role → `403 Forbidden`\n  - If the user has any of the allowed roles → request continues to the next handler\n\nRules are evaluated in the order they were added.\n\n### Authenticators\n\nAn authenticator is a small object (`Gatekeeper::Authenticator`) that knows how to extract an `Identity` from a request.  \nIf an authenticator returns `nil`, Gatekeeper moves on to the next one.  \nThe first authenticator that returns a user “wins”.  \nA rule may also define its own authenticator, which overrides the global ones.\n\n```crystal\nmy_special_auth = Gatekeeper.authenticator do |ctx|\n  # your logic here\nend\n\nGatekeeper::Rule.new(/^\\/private/, roles: [\"member\"], authenticator: my_special_auth)\n```\n\n#### Named authenticator\n\nAuthenticators may also be given a name, which is useful for debugging or when you have several different authentication strategies in the same application.\n\n```crystal\ntoken_auth = Gatekeeper.authenticator \"token auth\" do |ctx|\n  token = ctx.request.headers[\"Authorization\"]?\n  next nil unless token\n\n  user = UserRepository.find_by_token(token)\n  next nil unless user\n\n  Gatekeeper::IdentityUser(Int32).new(user.id, Set{\"member\"})\nend\n\nconfig.authenticators \u003c\u003c token_auth\n```\n\nYou can also attach a named authenticator directly to a rule.\n\n### Identity\n\nGatekeeper needs an identity type that represents the authenticated user.\nEvery identity must implement:\n\n```crystal\nabstract class Gatekeeper::Identity\n  abstract def roles : Set(String)\nend\n```\n\nGatekeeper ships with a simple identity type:\n\n```crystal\nclass Gatekeeper::IdentityUser(ID) \u003c Gatekeeper::Identity\n  getter id : ID\n  getter roles : Set(String)\nend\n```\n\nYou can use any ID type (`Int32`, `String`, `UUID`, etc.).\nTo define your own identity type, inherit from `Identity`:\n\n```crystal\nclass MyUser \u003c Gatekeeper::Identity\n  getter roles : Set(String)\n  getter email : String\nend\n```\n\n### Authenticator example\n\n```crystal\nconfig.authenticators \u003c\u003c Gatekeeper.authenticator do |ctx|\n  token = ctx.request.headers[\"Authorization\"]?\n  next nil unless token\n\n  user = MyUserRepository.find_by_token(token)\n  next nil unless user\n\n  Gatekeeper::IdentityUser(Int32).new(user.id, Set{\"admin\"})\nend\n```\n\n### Role hierarchy\n\nGatekeeper supports an optional *role hierarchy*, allowing one role to imply one or more additional roles.\n\nThis is useful when you have complex permission models or a natural “parent → child” role structure.\nWith a hierarchy, a user with a high-level role automatically inherits the permissions of lower-level roles.\n\nConfigure the hierarchy globally:\n\n```crystal\nGatekeeper.config do |config|\n  config.role_hierarchy = {\n    \"superadmin\"     =\u003e [\"admin\", \"auditor\"],\n    \"admin\"          =\u003e [\"manager\", \"support\", \"user\"],\n    \"manager\"        =\u003e [\"team_lead\", \"user\"],\n    \"support\"        =\u003e [\"user\"],\n    \"team_lead\"      =\u003e [\"user\"],\n    \"auditor\"        =\u003e [\"read_only\"],\n  }\nend\n```\n\nGatekeeper expands roles transitively at runtime.\n\nExample:\n\n- identity roles: `{\"superadmin\"}`\n- hierarchy expansion:\n  - superadmin → admin, auditor  \n  - admin → manager, support, user  \n  - manager → team_lead → user  \n  - auditor → read_only  \n- effective roles become:\n\n```\n{\"superadmin\", \"admin\", \"auditor\", \"manager\", \"support\", \"team_lead\", \"user\", \"read_only\"}\n```\n\n#### How hierarchy affects authorization\n\nGiven a rule:\n\n```crystal\nGatekeeper.allow \"/reports\", roles: [\"team_lead\"]\n```\n\nCase 1: Without hierarchy  \n- identity roles: `{\"admin\"}`\n- required role: `\"team_lead\"`\n- no hierarchy → `\"admin\"` does not imply `\"team_lead\"` → **403 Forbidden**\n\nCase 2: With hierarchy enabled  \n- identity roles: `{\"admin\"}`\n- hierarchy says: `admin → manager → team_lead`\n- expanded roles include `\"team_lead\"` → **allowed**\n\n#### Notes\n\n- Hierarchy is optional and defaults to `{}`.\n- Cycles (e.g., `\"A\" → \"B\" → \"A\"`) are handled safely.\n- Identity objects stay simple; hierarchy logic lives entirely in Gatekeeper’s authorization layer.\n- Hierarchies may be multi-level, branching, or diamond-shaped.\n\n### Rules\n\nA rule defines when and how Gatekeeper enforces authorization:\n\n```crystal\nGatekeeper::Rule.new(\n  path_regex : Regex,\n  roles : Array(String) = [],\n  methods : Array(String)? = nil,\n  authenticator : Gatekeeper::Authenticator? = nil\n)\n```\n\n- `path_regex`: matched against `ctx.request.path`\n- `roles`: user must have at least one of these roles\n- `methods`: optional HTTP method filter (GET/POST/PUT/DELETE/etc.)\n- `authenticator`: optional override for this rule only\n\nRules are evaluated in the order they were added.\nThe first matching rule is used.\n\n### Rule helpers\n\nGatekeeper includes a small convenience API to make rule definition more ergonomic.\n\n#### `Gatekeeper.allow`\n\nA helper that builds a `Rule` without needing to call `Rule.new` directly:\n\n```crystal\nrule = Gatekeeper.allow(\n  path : String | Regex,\n  roles : Array(String) = [],\n  methods : Array(String)? = nil,\n  authenticator : Gatekeeper::Authenticator? = nil\n)\n```\n\n**Path semantics:**\n\n- When `path` is a **String**, Gatekeeper converts it into an **exact match** regex.  \n  Example:  \n  `\"/admin\"` becomes `/^\\/admin$/`, matching **only** `/admin`.\n\n- When `path` is a **Regex**, it is used **as-is**.  \n  Useful for prefixes or patterns such as:  \n  `/^\\/api/` which matches `/api`, `/api/v1`, etc.\n\nExample:\n\n```crystal\nGatekeeper.allow(\"/admin\", roles: [\"admin\"])\nGatekeeper.allow(/^\\/api/, roles: [\"api_user\"])\n```\n\n### HTTP method helpers\n\nGatekeeper also provides convenience helpers for common HTTP verbs:\n\n```crystal\nGatekeeper.allow_get(path, roles = [], authenticator = nil)\nGatekeeper.allow_post(path, roles = [], authenticator = nil)\nGatekeeper.allow_put(path, roles = [], authenticator = nil)\nGatekeeper.allow_patch(path, roles = [], authenticator = nil)\nGatekeeper.allow_delete(path, roles = [], authenticator = nil)\nGatekeeper.allow_options(path, roles = [], authenticator = nil)\n```\n\nThese behave like `Gatekeeper.allow`, but automatically set the appropriate  \n`methods: [\"GET\"]`, `\"POST\"`, etc.\n\nExample:\n\n```crystal\nGatekeeper.allow_get \"/status\"\nGatekeeper.allow_post \"/login\"\nGatekeeper.allow_put \"/admin\", roles: [\"admin\"]\n```\n\n### Rule DSL (`Gatekeeper.rules`)\n\nGatekeeper also provides a small builder-style DSL for defining multiple rules in a clean and structured way.\nIt is simply syntactic sugar around `Gatekeeper.allow`.\n\n```crystal\nGatekeeper.rules do |r|\n  r.allow \"/admin\", roles: [\"admin\"]\n  r.allow /^\\/api/, roles: [\"api_user\"]\n\n  # HTTP verb helpers are also available inside the DSL:\n  r.allow_get \"/\"\n  r.allow_post \"/login\"\nend\n```\n\nThis is equivalent to calling `Gatekeeper.allow` manually and pushing the rules into the configuration, but keeps rule definitions grouped and readable.\n\nInternally, this DSL just appends rules to `Gatekeeper.config.auth_rules`.\n\n### ContextHandler\n\n`ContextHandler` is a small wrapper used for callbacks such as\n`on_unauthenticated` and `on_unauthorized`. It simply holds a block that\nreceives the current `HTTP::Server::Context` and writes a custom response.\n\n```crystal\nconfig.on_unauthenticated = Gatekeeper::ContextHandler.new do |ctx|\n  ctx.response.print \"You must log in first.\"\nend\n\nconfig.on_unauthorized = Gatekeeper::ContextHandler.new do |ctx|\n  ctx.response.print \"You do not have permission.\"\nend\n```\n\nGatekeeper calls these handlers internally when:\n\n- no authenticator returns a user → **401 Unauthorized**\n- a user exists but lacks the required role → **403 Forbidden**\n\nIf no handler is set, Gatekeeper simply returns the status code\nwithout a body.\n\n### ⚠️ Security Warning\n\nGatekeeper does not perform authentication.\nIt only consumes the identity returned by your authenticators and enforces authorization rules.\nMake sure your authentication mechanism (sessions, tokens, cookies, etc.) is secure.\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/henrikac/gatekeeper.cr/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- [Henrik Christensen](https://github.com/henrikac) - creator and maintainer\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhenrikac%2Fgatekeeper.cr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhenrikac%2Fgatekeeper.cr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhenrikac%2Fgatekeeper.cr/lists"}