rails_claude_skills 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.yml +134 -0
- data/.github/ISSUE_TEMPLATE/config.yml +11 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +129 -0
- data/.github/ISSUE_TEMPLATE/question.yml +90 -0
- data/.github/dependabot.yml +19 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/release.yml +66 -0
- data/.rubocop.yml +52 -0
- data/CHANGELOG.md +94 -0
- data/CLAUDE.md +332 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +580 -0
- data/LICENSE.txt +21 -0
- data/README.md +544 -0
- data/Rakefile +8 -0
- data/lib/generators/claude/agent/agent_generator.rb +71 -0
- data/lib/generators/claude/agent/templates/agent.md.tt +62 -0
- data/lib/generators/claude/command/command_generator.rb +50 -0
- data/lib/generators/claude/command/templates/command.md.tt +28 -0
- data/lib/generators/claude/commands_library/create-pr.md +27 -0
- data/lib/generators/claude/commands_library/dbchange.md +19 -0
- data/lib/generators/claude/commands_library/quality.md +20 -0
- data/lib/generators/claude/commands_library/stimulus.md +19 -0
- data/lib/generators/claude/commands_library/turbo-feature.md +17 -0
- data/lib/generators/claude/install/install_generator.rb +211 -0
- data/lib/generators/claude/install/templates/README.md.tt +59 -0
- data/lib/generators/claude/install/templates/USAGE +28 -0
- data/lib/generators/claude/install/templates/agents/api-dev.md.tt +46 -0
- data/lib/generators/claude/install/templates/agents/fullstack-dev.md.tt +48 -0
- data/lib/generators/claude/install/templates/agents/rails-developer.md.tt +40 -0
- data/lib/generators/claude/install/templates/settings.local.json.tt +13 -0
- data/lib/generators/claude/rule/rule_generator.rb +175 -0
- data/lib/generators/claude/rule/templates/rule.md.tt +7 -0
- data/lib/generators/claude/rules_library/code-style.md +37 -0
- data/lib/generators/claude/rules_library/database.md +47 -0
- data/lib/generators/claude/rules_library/hotwire.md +56 -0
- data/lib/generators/claude/rules_library/security.md +54 -0
- data/lib/generators/claude/rules_library/testing.md +47 -0
- data/lib/generators/claude/skill/skill_generator.rb +196 -0
- data/lib/generators/claude/skill/templates/SKILL.md.tt +27 -0
- data/lib/generators/claude/skills_library/create-task-files/SKILL.md +311 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/bug.md +60 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/epic.md +47 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/issue.md +45 -0
- data/lib/generators/claude/skills_library/create-task-files/templates/user-story.md +57 -0
- data/lib/generators/claude/skills_library/minitest-testing/SKILL.md +398 -0
- data/lib/generators/claude/skills_library/minitest-testing/references/examples.md +889 -0
- data/lib/generators/claude/skills_library/plan-feature/SKILL.md +253 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/SKILL.md +1041 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/api-documentation.md +422 -0
- data/lib/generators/claude/skills_library/rails-api-controllers/references/serialization.md +456 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/SKILL.md +191 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/advanced.md +331 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/api-auth.md +266 -0
- data/lib/generators/claude/skills_library/rails-auth-with-devise/references/omniauth.md +194 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/SKILL.md +603 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/api-authorization.md +543 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/complex-permissions.md +572 -0
- data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md +373 -0
- data/lib/generators/claude/skills_library/rails-controllers/SKILL.md +514 -0
- data/lib/generators/claude/skills_library/rails-debugging/SKILL.md +260 -0
- data/lib/generators/claude/skills_library/rails-deployment/SKILL.md +437 -0
- data/lib/generators/claude/skills_library/rails-deployment/references/examples.md +901 -0
- data/lib/generators/claude/skills_library/rails-hotwire/SKILL.md +367 -0
- data/lib/generators/claude/skills_library/rails-jobs/MISSION_CONTROL_SETUP.md +639 -0
- data/lib/generators/claude/skills_library/rails-jobs/SKILL.md +704 -0
- data/lib/generators/claude/skills_library/rails-mailers/SKILL.md +549 -0
- data/lib/generators/claude/skills_library/rails-models/SKILL.md +379 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/SKILL.md +622 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/api-pagination.md +523 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/custom-themes.md +498 -0
- data/lib/generators/claude/skills_library/rails-pagination-kaminari/references/performance.md +478 -0
- data/lib/generators/claude/skills_library/rails-views/SKILL.md +508 -0
- data/lib/generators/claude/skills_library/refine-requirements/SKILL.md +226 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/examples.md +344 -0
- data/lib/generators/claude/skills_library/refine-requirements/references/reference.md +298 -0
- data/lib/generators/claude/skills_library/rspec-testing/SKILL.md +572 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/better_specs_guide.md +273 -0
- data/lib/generators/claude/skills_library/rspec-testing/references/thoughtbot_patterns.md +407 -0
- data/lib/generators/claude/skills_library/tailwindcss/SKILL.md +371 -0
- data/lib/generators/claude/views/views_generator.rb +113 -0
- data/lib/rails_claude_skills/railtie.rb +16 -0
- data/lib/rails_claude_skills/version.rb +5 -0
- data/lib/rails_claude_skills.rb +27 -0
- data/sig/rails_claude_skills.rbs +4 -0
- metadata +199 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-authorization-cancancan
|
|
3
|
+
description: "Authorization and permissions management for Ruby on Rails applications using CanCanCan. Use when: (1) Implementing role-based access control (RBAC), (2) Defining user permissions and abilities, (3) Restricting resource access in controllers, (4) Filtering queries based on user permissions, (5) Hiding/showing UI elements based on authorization, (6) Testing authorization logic, (7) Managing admin vs user vs guest permissions, (8) Implementing attribute-based access control"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rails Authorization with CanCanCan
|
|
7
|
+
|
|
8
|
+
CanCanCan is a popular authorization library for Rails that restricts what resources a given user is allowed to access. It centralizes all permission logic in a single Ability class, keeping authorization rules DRY and maintainable.
|
|
9
|
+
|
|
10
|
+
## Quick Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Add to Gemfile
|
|
14
|
+
bundle add cancancan
|
|
15
|
+
|
|
16
|
+
# Generate Ability class
|
|
17
|
+
rails generate cancan:ability
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This creates `app/models/ability.rb` where all authorization rules are defined.
|
|
21
|
+
|
|
22
|
+
## Core Concepts
|
|
23
|
+
|
|
24
|
+
### Defining Abilities
|
|
25
|
+
|
|
26
|
+
The `Ability` class centralizes all permission logic:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# app/models/ability.rb
|
|
30
|
+
class Ability
|
|
31
|
+
include CanCan::Ability
|
|
32
|
+
|
|
33
|
+
def initialize(user)
|
|
34
|
+
# Guest users (not signed in)
|
|
35
|
+
can :read, Post, published: true
|
|
36
|
+
can :read, Comment
|
|
37
|
+
|
|
38
|
+
# Signed-in users
|
|
39
|
+
return unless user.present?
|
|
40
|
+
|
|
41
|
+
can :read, Post
|
|
42
|
+
can :create, Post
|
|
43
|
+
can :update, Post, user_id: user.id
|
|
44
|
+
can :destroy, Post, user_id: user.id
|
|
45
|
+
|
|
46
|
+
can :create, Comment
|
|
47
|
+
can :update, Comment, user_id: user.id
|
|
48
|
+
can :destroy, Comment, user_id: user.id
|
|
49
|
+
|
|
50
|
+
# Admin users
|
|
51
|
+
return unless user.admin?
|
|
52
|
+
|
|
53
|
+
can :manage, :all # Can do anything
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Best Practice**: Structure rules hierarchically (guest → user → admin) for clarity.
|
|
59
|
+
|
|
60
|
+
## Actions and Resources
|
|
61
|
+
|
|
62
|
+
### Standard CRUD Actions
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
:read # :index and :show
|
|
66
|
+
:create # :new and :create
|
|
67
|
+
:update # :edit and :update
|
|
68
|
+
:destroy # :destroy
|
|
69
|
+
|
|
70
|
+
:manage # All actions (use carefully!)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Custom Actions
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
can :publish, Post
|
|
77
|
+
can :archive, Post
|
|
78
|
+
can :approve, Comment
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Multiple Resources
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
can :read, [Post, Comment, Category]
|
|
85
|
+
can :manage, [User, Post], user_id: user.id
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Ability Conditions
|
|
89
|
+
|
|
90
|
+
### Hash Conditions
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# Simple equality
|
|
94
|
+
can :update, Post, user_id: user.id
|
|
95
|
+
|
|
96
|
+
# Multiple conditions (AND logic)
|
|
97
|
+
can :read, Post, published: true, category_id: user.accessible_category_ids
|
|
98
|
+
|
|
99
|
+
# SQL fragment (use sparingly)
|
|
100
|
+
can :read, Post, ["published_at <= ?", Time.zone.now]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Block Conditions
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# Complex logic
|
|
107
|
+
can :update, Post do |post|
|
|
108
|
+
post.user_id == user.id || user.admin?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# With associations
|
|
112
|
+
can :read, Post do |post|
|
|
113
|
+
post.published? || post.user_id == user.id
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Accessing current user
|
|
117
|
+
can :destroy, Comment do |comment|
|
|
118
|
+
comment.user_id == user.id && comment.created_at > 15.minutes.ago
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Important**: Block conditions cannot be used with `accessible_by` for database queries. Use hash conditions when you need to filter collections.
|
|
123
|
+
|
|
124
|
+
### Combining Conditions
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# Multiple can statements are OR'd together
|
|
128
|
+
can :read, Post, published: true # Public posts
|
|
129
|
+
can :read, Post, user_id: user.id # Own posts
|
|
130
|
+
# User can read posts that are EITHER published OR owned by them
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Controller Integration
|
|
134
|
+
|
|
135
|
+
### Manual Authorization
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class PostsController < ApplicationController
|
|
139
|
+
def show
|
|
140
|
+
@post = Post.find(params[:id])
|
|
141
|
+
authorize! :read, @post # Raises CanCan::AccessDenied if not authorized
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def update
|
|
145
|
+
@post = Post.find(params[:id])
|
|
146
|
+
authorize! :update, @post
|
|
147
|
+
|
|
148
|
+
if @post.update(post_params)
|
|
149
|
+
redirect_to @post
|
|
150
|
+
else
|
|
151
|
+
render :edit
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Automatic Loading and Authorization
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
class PostsController < ApplicationController
|
|
161
|
+
load_and_authorize_resource
|
|
162
|
+
|
|
163
|
+
def index
|
|
164
|
+
# @posts automatically loaded with accessible_by
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def show
|
|
168
|
+
# @post automatically loaded and authorized
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def create
|
|
172
|
+
# @post initialized and authorized
|
|
173
|
+
if @post.save
|
|
174
|
+
redirect_to @post
|
|
175
|
+
else
|
|
176
|
+
render :new
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def update
|
|
181
|
+
# @post loaded and authorized
|
|
182
|
+
if @post.update(post_params)
|
|
183
|
+
redirect_to @post
|
|
184
|
+
else
|
|
185
|
+
render :edit
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Benefits**: Eliminates repetitive authorization code across RESTful actions.
|
|
192
|
+
|
|
193
|
+
### Load and Authorize Options
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Specific actions only
|
|
197
|
+
load_and_authorize_resource only: [:show, :edit, :update, :destroy]
|
|
198
|
+
load_and_authorize_resource except: [:index]
|
|
199
|
+
|
|
200
|
+
# Different resource name
|
|
201
|
+
load_and_authorize_resource :article
|
|
202
|
+
|
|
203
|
+
# Custom find method
|
|
204
|
+
load_and_authorize_resource find_by: :slug
|
|
205
|
+
|
|
206
|
+
# Nested resources
|
|
207
|
+
class CommentsController < ApplicationController
|
|
208
|
+
load_and_authorize_resource :post
|
|
209
|
+
load_and_authorize_resource :comment, through: :post
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Skip loading (only authorize)
|
|
213
|
+
authorize_resource
|
|
214
|
+
|
|
215
|
+
# Skip authorization for specific actions
|
|
216
|
+
skip_authorize_resource only: [:index]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Fetching Authorized Records
|
|
220
|
+
|
|
221
|
+
### accessible_by
|
|
222
|
+
|
|
223
|
+
Retrieve only records the user can access:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
# In controller
|
|
227
|
+
def index
|
|
228
|
+
@posts = Post.accessible_by(current_ability)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# With specific action
|
|
232
|
+
@posts = Post.accessible_by(current_ability, :read)
|
|
233
|
+
@editable_posts = Post.accessible_by(current_ability, :update)
|
|
234
|
+
|
|
235
|
+
# Chainable with ActiveRecord
|
|
236
|
+
@published_posts = Post.published.accessible_by(current_ability)
|
|
237
|
+
@posts = Post.accessible_by(current_ability).where(category_id: params[:category_id])
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Performance**: Uses SQL conditions from ability rules for efficient database queries.
|
|
241
|
+
|
|
242
|
+
## View Helpers
|
|
243
|
+
|
|
244
|
+
### Conditional UI Elements
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# Check single permission
|
|
248
|
+
<% if can? :update, @post %>
|
|
249
|
+
<%= link_to 'Edit', edit_post_path(@post) %>
|
|
250
|
+
<% end %>
|
|
251
|
+
|
|
252
|
+
<% if can? :destroy, @post %>
|
|
253
|
+
<%= link_to 'Delete', @post, method: :delete, data: { confirm: 'Are you sure?' } %>
|
|
254
|
+
<% end %>
|
|
255
|
+
|
|
256
|
+
# Negative check
|
|
257
|
+
<% if cannot? :update, @post %>
|
|
258
|
+
<p>You cannot edit this post</p>
|
|
259
|
+
<% end %>
|
|
260
|
+
|
|
261
|
+
# Multiple permissions
|
|
262
|
+
<% if can?(:update, @post) || can?(:destroy, @post) %>
|
|
263
|
+
<div class="post-actions">
|
|
264
|
+
<%= link_to 'Edit', edit_post_path(@post) if can? :update, @post %>
|
|
265
|
+
<%= link_to 'Delete', @post, method: :delete if can? :destroy, @post %>
|
|
266
|
+
</div>
|
|
267
|
+
<% end %>
|
|
268
|
+
|
|
269
|
+
# Check on class (useful in index views)
|
|
270
|
+
<% if can? :create, Post %>
|
|
271
|
+
<%= link_to 'New Post', new_post_path %>
|
|
272
|
+
<% end %>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Navigation Menus
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
<nav>
|
|
279
|
+
<%= link_to 'Posts', posts_path if can? :read, Post %>
|
|
280
|
+
<%= link_to 'New Post', new_post_path if can? :create, Post %>
|
|
281
|
+
<%= link_to 'Admin', admin_path if can? :manage, :all %>
|
|
282
|
+
</nav>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Handling Unauthorized Access
|
|
286
|
+
|
|
287
|
+
### Exception Rescue
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
# app/controllers/application_controller.rb
|
|
291
|
+
class ApplicationController < ActionController::Base
|
|
292
|
+
rescue_from CanCan::AccessDenied do |exception|
|
|
293
|
+
respond_to do |format|
|
|
294
|
+
format.html { redirect_to root_path, alert: exception.message }
|
|
295
|
+
format.json { render json: { error: exception.message }, status: :forbidden }
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Custom Error Messages
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
# In Ability class
|
|
305
|
+
can :update, Post, user_id: user.id do |post|
|
|
306
|
+
post.user_id == user.id
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# In controller with custom message
|
|
310
|
+
authorize! :update, @post, message: "You can only edit your own posts"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Flash Messages
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
rescue_from CanCan::AccessDenied do |exception|
|
|
317
|
+
redirect_to root_path, alert: "Access denied: #{exception.message}"
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Common Patterns
|
|
322
|
+
|
|
323
|
+
### Role-Based Authorization
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
# app/models/user.rb
|
|
327
|
+
class User < ApplicationRecord
|
|
328
|
+
ROLES = %w[guest user moderator admin].freeze
|
|
329
|
+
|
|
330
|
+
enum role: { guest: 0, user: 1, moderator: 2, admin: 3 }
|
|
331
|
+
|
|
332
|
+
def role?(check_role)
|
|
333
|
+
role.to_sym == check_role.to_sym
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# app/models/ability.rb
|
|
338
|
+
class Ability
|
|
339
|
+
include CanCan::Ability
|
|
340
|
+
|
|
341
|
+
def initialize(user)
|
|
342
|
+
user ||= User.new # Guest user
|
|
343
|
+
|
|
344
|
+
if user.admin?
|
|
345
|
+
can :manage, :all
|
|
346
|
+
elsif user.moderator?
|
|
347
|
+
can :manage, Post
|
|
348
|
+
can :manage, Comment
|
|
349
|
+
can :read, User
|
|
350
|
+
elsif user.user?
|
|
351
|
+
can :read, :all
|
|
352
|
+
can :create, Post
|
|
353
|
+
can :manage, Post, user_id: user.id
|
|
354
|
+
can :manage, Comment, user_id: user.id
|
|
355
|
+
else
|
|
356
|
+
can :read, Post, published: true
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Organization/Tenant-Based Authorization
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
class Ability
|
|
366
|
+
include CanCan::Ability
|
|
367
|
+
|
|
368
|
+
def initialize(user)
|
|
369
|
+
return unless user.present?
|
|
370
|
+
|
|
371
|
+
# User can manage resources in their organization
|
|
372
|
+
can :manage, Post, organization_id: user.organization_id
|
|
373
|
+
can :manage, Comment, post: { organization_id: user.organization_id }
|
|
374
|
+
|
|
375
|
+
# Admin can manage organization settings
|
|
376
|
+
can :manage, Organization, id: user.organization_id if user.admin?
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Time-Based Authorization
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
class Ability
|
|
385
|
+
include CanCan::Ability
|
|
386
|
+
|
|
387
|
+
def initialize(user)
|
|
388
|
+
return unless user.present?
|
|
389
|
+
|
|
390
|
+
# Can edit posts within 1 hour of creation
|
|
391
|
+
can :update, Post do |post|
|
|
392
|
+
post.user_id == user.id && post.created_at > 1.hour.ago
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Can read posts after publication date
|
|
396
|
+
can :read, Post, ["published_at <= ?", Time.current]
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Attribute-Based Authorization
|
|
402
|
+
|
|
403
|
+
```ruby
|
|
404
|
+
class Ability
|
|
405
|
+
include CanCan::Ability
|
|
406
|
+
|
|
407
|
+
def initialize(user)
|
|
408
|
+
return unless user.present?
|
|
409
|
+
|
|
410
|
+
# Users can update specific attributes of their own posts
|
|
411
|
+
can [:update], Post, user_id: user.id
|
|
412
|
+
|
|
413
|
+
# Only admins can change published status
|
|
414
|
+
cannot :update, Post, :published unless user.admin?
|
|
415
|
+
|
|
416
|
+
# Users can update their profile but not role
|
|
417
|
+
can :update, User, id: user.id
|
|
418
|
+
cannot :update, User, :role
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Strong Parameters with CanCanCan
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
# app/controllers/posts_controller.rb
|
|
427
|
+
def post_params
|
|
428
|
+
params.require(:post).permit(:title, :body, :published)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Restrict based on abilities
|
|
432
|
+
def post_params
|
|
433
|
+
params.require(:post).permit(
|
|
434
|
+
current_user.admin? ? [:title, :body, :published] : [:title, :body]
|
|
435
|
+
)
|
|
436
|
+
end
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Testing
|
|
440
|
+
|
|
441
|
+
### RSpec Setup
|
|
442
|
+
|
|
443
|
+
```ruby
|
|
444
|
+
# spec/support/cancan.rb
|
|
445
|
+
RSpec.configure do |config|
|
|
446
|
+
config.include CanCan::Ability
|
|
447
|
+
end
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### Testing Abilities
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
# spec/models/ability_spec.rb
|
|
454
|
+
require 'rails_helper'
|
|
455
|
+
require 'cancan/matchers'
|
|
456
|
+
|
|
457
|
+
RSpec.describe Ability, type: :model do
|
|
458
|
+
subject(:ability) { Ability.new(user) }
|
|
459
|
+
|
|
460
|
+
describe 'Guest user' do
|
|
461
|
+
let(:user) { nil }
|
|
462
|
+
|
|
463
|
+
it { is_expected.to be_able_to(:read, Post.new(published: true)) }
|
|
464
|
+
it { is_expected.not_to be_able_to(:create, Post) }
|
|
465
|
+
it { is_expected.not_to be_able_to(:update, Post) }
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
describe 'Regular user' do
|
|
469
|
+
let(:user) { create(:user) }
|
|
470
|
+
let(:own_post) { create(:post, user: user) }
|
|
471
|
+
let(:other_post) { create(:post) }
|
|
472
|
+
|
|
473
|
+
it { is_expected.to be_able_to(:read, Post) }
|
|
474
|
+
it { is_expected.to be_able_to(:create, Post) }
|
|
475
|
+
it { is_expected.to be_able_to(:update, own_post) }
|
|
476
|
+
it { is_expected.not_to be_able_to(:update, other_post) }
|
|
477
|
+
it { is_expected.to be_able_to(:destroy, own_post) }
|
|
478
|
+
it { is_expected.not_to be_able_to(:destroy, other_post) }
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
describe 'Admin user' do
|
|
482
|
+
let(:user) { create(:user, admin: true) }
|
|
483
|
+
|
|
484
|
+
it { is_expected.to be_able_to(:manage, :all) }
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Testing Controllers
|
|
490
|
+
|
|
491
|
+
```ruby
|
|
492
|
+
# spec/controllers/posts_controller_spec.rb
|
|
493
|
+
RSpec.describe PostsController, type: :controller do
|
|
494
|
+
let(:user) { create(:user) }
|
|
495
|
+
let(:other_user) { create(:user) }
|
|
496
|
+
let(:post) { create(:post, user: user) }
|
|
497
|
+
|
|
498
|
+
before { sign_in user }
|
|
499
|
+
|
|
500
|
+
describe 'GET #edit' do
|
|
501
|
+
context 'when editing own post' do
|
|
502
|
+
it 'allows access' do
|
|
503
|
+
get :edit, params: { id: post.id }
|
|
504
|
+
expect(response).to have_http_status(:ok)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
context 'when editing other user post' do
|
|
509
|
+
let(:other_post) { create(:post, user: other_user) }
|
|
510
|
+
|
|
511
|
+
it 'denies access' do
|
|
512
|
+
expect {
|
|
513
|
+
get :edit, params: { id: other_post.id }
|
|
514
|
+
}.to raise_error(CanCan::AccessDenied)
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Testing accessible_by
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
RSpec.describe 'Post access', type: :model do
|
|
525
|
+
let(:user) { create(:user) }
|
|
526
|
+
let(:admin) { create(:user, admin: true) }
|
|
527
|
+
let!(:published_post) { create(:post, published: true) }
|
|
528
|
+
let!(:draft_post) { create(:post, published: false, user: user) }
|
|
529
|
+
let!(:other_draft) { create(:post, published: false) }
|
|
530
|
+
|
|
531
|
+
it 'returns correct posts for user' do
|
|
532
|
+
ability = Ability.new(user)
|
|
533
|
+
accessible = Post.accessible_by(ability)
|
|
534
|
+
|
|
535
|
+
expect(accessible).to include(published_post, draft_post)
|
|
536
|
+
expect(accessible).not_to include(other_draft)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
it 'returns all posts for admin' do
|
|
540
|
+
ability = Ability.new(admin)
|
|
541
|
+
accessible = Post.accessible_by(ability)
|
|
542
|
+
|
|
543
|
+
expect(accessible).to include(published_post, draft_post, other_draft)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## Performance Considerations
|
|
549
|
+
|
|
550
|
+
### Use Hash Conditions for Collections
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
# Good - generates SQL query
|
|
554
|
+
can :read, Post, user_id: user.id
|
|
555
|
+
@posts = Post.accessible_by(current_ability)
|
|
556
|
+
|
|
557
|
+
# Bad - cannot generate SQL, will raise error
|
|
558
|
+
can :read, Post do |post|
|
|
559
|
+
post.user_id == user.id
|
|
560
|
+
end
|
|
561
|
+
@posts = Post.accessible_by(current_ability) # Error!
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Eager Loading
|
|
565
|
+
|
|
566
|
+
```ruby
|
|
567
|
+
# Prevent N+1 queries
|
|
568
|
+
@posts = Post.accessible_by(current_ability).includes(:user, :comments)
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Caching Abilities
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
# Cache ability checks in instance variable
|
|
575
|
+
def current_ability
|
|
576
|
+
@current_ability ||= Ability.new(current_user)
|
|
577
|
+
end
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Integration with Pundit
|
|
581
|
+
|
|
582
|
+
If migrating from Pundit or using both:
|
|
583
|
+
|
|
584
|
+
```ruby
|
|
585
|
+
# CanCanCan uses a single Ability class
|
|
586
|
+
# Pundit uses policy classes per model
|
|
587
|
+
|
|
588
|
+
# They can coexist, but choose one primary approach
|
|
589
|
+
# CanCanCan: Centralized, better for simple RBAC
|
|
590
|
+
# Pundit: Decentralized, better for complex domain logic
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
## Advanced Patterns
|
|
594
|
+
|
|
595
|
+
For more complex scenarios, see:
|
|
596
|
+
- **Multi-tenancy**: [references/multi-tenancy.md](references/multi-tenancy.md)
|
|
597
|
+
- **API authorization**: [references/api-authorization.md](references/api-authorization.md)
|
|
598
|
+
- **Complex permissions**: [references/complex-permissions.md](references/complex-permissions.md)
|
|
599
|
+
|
|
600
|
+
## Resources
|
|
601
|
+
|
|
602
|
+
- [CanCanCan GitHub](https://github.com/CanCanCommunity/cancancan)
|
|
603
|
+
- [CanCanCan Wiki](https://github.com/CanCanCommunity/cancancan/wiki)
|