cancancan_cr
Version, currently master branch1 version
- master branchlatestOct 15, 2026
github.com/OrelSokolov/cancancan.cr
No description declared in shard.yml.
Installation
# Add this to your shard.yml
dependencies:
cancancan_cr:
github: OrelSokolov/cancancan.cr
branch: mastermaster is a branch, not a release, so this tracks it rather than pinning a version.
Then run:
shards installshard.yml
- Crystal
>= 1.21.0
Dependencies
Runtime Dependencies
- action_pack_cr~> 0.1.0github: OrelSokolov/action_pack.cr
- active_record_cr~> 0.1.0github: OrelSokolov/active_record.cr
README
cancancan.cr
A 1:1 Crystal port of CanCanCan 3.6.1 authorization on top of
active_record.cr. Namespace CanCan mirrors the gem; abilities,
rules, SQL-scope extraction (accessible_by) and controller
load_and_authorize_resource behave like the original.
⚠️ EXPERIMENTAL — DO NOT USE IN PRODUCTION YET.
Installation
dependencies:
cancancan_cr:
github: OrelSokolov/cancancan.cr
shards install
Depends on active_record.cr and action_pack.cr (controller layer).
Usage
class Ability
include CanCan::Ability
def initialize(user)
can :read, Post
can :manage, Post, author_id: user.id
cannot :destroy, Post, featured: true
end
end
ability.can?(:read, post)
posts = Post.accessible_by(ability, :read)
class PostsController < ApplicationController
load_and_authorize_resource
end
Feature comparison vs cancancan 3.6.1
Ability DSL
| Feature | Original (cancancan) | Port (cancancan.cr) | Status |
|---|---|---|---|
can / cannot (action arrays, subject arrays, :all, :manage, conditions hash, raw-SQL string, Relation scopes, attribute arrays/single attributes, blocks, catch-all blocks, two-argument attribute blocks) | ✅ | Same | ✅ |
can? / cannot?, incl. {any: [...]} multi-subject and nested {parent => child} forms | ✅ | Crystal syntax: can?(:read, any: [Article] of CanCan::Subject) and can?(:read, parent: article, child: comment) — semantics 1:1 | 🔶 |
authorize! (CanCan::AccessDenied with action/subject/message) | ✅ | Same | ✅ |
attributes_for, has_block?/has_raw_sql?, merge, permissions, permitted_attributes, unauthorized_message | ✅ | permissions renders the gem's Hash#inspect string (no runtime symbol-keyed hashes); permitted_attributes returns Array(String) | 🔶 |
alias_action (+ default read/create/update aliases, real-action target validation), clear_aliased_actions, expand_actions | ✅ | Same | ✅ |
Rules & scope extraction
| Feature | Original | Port | Status |
|---|---|---|---|
CanCan::Rule relevance/conditions matching (hash / range / array / nested hash, empty-has-nil semantics, belongs_to single-record, class-subjects ignore conditions, last-rule-wins) | ✅ | Same, incl. gem-identical #inspect | ✅ |
Model.accessible_by(ability, action) — full AR adapter pipeline | ✅ | RulesCompressor (duplicate dedupe + catch-all collapse), scope override with merge error, single-can fast path, AND/OR/NOT fold; array → IN, ranges → BETWEEN, nested hashes → (NOT) EXISTS subqueries via association joins (same result sets as the gem's LEFT JOIN aliasing) | ✅ |
| SQL fragments with bind parameters | ✅ | Inline escaped literals (strings quote-doubled) — Relation#where(String, *args) cannot take a runtime args array; result sets and to_sql column references match the oracle | 🔶 |
Controller additions
| Feature | Original | Port | Status |
|---|---|---|---|
ControllerAdditions seam (params/ivar store/current_ability/cancan_action/skips/param sanitizer) | ✅ | Same, oracle-covered (33 ctrl_* cases) | ✅ |
ControllerResource (load/authorize, new/create vs member actions, find_by, through parents, collection via accessible_by, sanitization) | ✅ | Same | ✅ |
load_and_authorize_resource / load_resource / authorize_resource macros on ActionController::Base | ✅ | Before-action filter + typed resource getters; model classes resolve via the CanCan.register_model compile-time registry (auto-registered by the macros) | 🔶 |
skip_load_resource-style macro sugar | ✅ | Instance-level cancan_skip(...) calls (action_pack filters run in registration order) | 🔶 |
check_authorization / after_action verify hooks, RSpec matchers | ✅ | Out of scope | ❌ |
Deviations & Crystal-specific notes
- No runtime
instance_variable_set/send/constantize— resources live in a typed seam store with generated getters; noString#to_sym— action names resolve via generatedcancan_actionmaps. - Blocks receive subjects typed as a union (
ActiveRecord::Base | Symbol | String); spec blocks cast with.as(Model). - Conditions matching reads records through
cancan_attribute(name)(columns via[]; models override it to expose associations). - Relation-scope conditions are captured at
cantime intoCanCan::RawScope(Crystal cannot unionRelation(T)across models);accessible_byre-applies the captured WHERE fragments. - Merge cases use the Symbol subject
:userinstead of a plain Ruby class (all oracle subjects are AR models or symbols anyway).
Testing
crystal spec
Oracle: gem 3.6.1 over SQLite in-memory (oracle/cases.rb), cases
mirroring the gem's rspec suite. The Crystal spec replays them 1:1
against PostgreSQL and must reproduce oracle/expected.yml exactly —
133 cases (100 core + 33 ctrl_*). ruby oracle/cases.rb regenerates
the expected file verbatim.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This branch
- Branch
master- Seen
- Oct 15, 2026
- Crystal
>= 1.21.0- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/OrelSokolov/cancancan.cr
Metadata
- Created
- Oct 14, 2026
- Updated
- Oct 24, 2026
- Synced
- Oct 24, 2026
- Versions
- 1