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,572 @@
|
|
|
1
|
+
# Complex Permissions with CanCanCan
|
|
2
|
+
|
|
3
|
+
Advanced authorization patterns for sophisticated business logic and edge cases.
|
|
4
|
+
|
|
5
|
+
## Attribute-Level Permissions
|
|
6
|
+
|
|
7
|
+
Controlling which fields users can read or modify:
|
|
8
|
+
|
|
9
|
+
### Read-Only Fields
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# app/models/ability.rb
|
|
13
|
+
class Ability
|
|
14
|
+
include CanCan::Ability
|
|
15
|
+
|
|
16
|
+
def initialize(user)
|
|
17
|
+
return unless user.present?
|
|
18
|
+
|
|
19
|
+
# Users can update their posts
|
|
20
|
+
can :update, Post, user_id: user.id
|
|
21
|
+
|
|
22
|
+
# But cannot change published status
|
|
23
|
+
cannot :update, Post, :published unless user.admin?
|
|
24
|
+
|
|
25
|
+
# Cannot change featured status
|
|
26
|
+
cannot :update, Post, :featured unless user.editor?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Controller Implementation
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# app/controllers/posts_controller.rb
|
|
35
|
+
class PostsController < ApplicationController
|
|
36
|
+
def update
|
|
37
|
+
@post = Post.find(params[:id])
|
|
38
|
+
authorize! :update, @post
|
|
39
|
+
|
|
40
|
+
# Check specific attributes
|
|
41
|
+
if params[:post][:published] && cannot?(:update, @post, :published)
|
|
42
|
+
params[:post].delete(:published)
|
|
43
|
+
flash[:warning] = "You cannot change the published status"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if @post.update(post_params)
|
|
47
|
+
redirect_to @post
|
|
48
|
+
else
|
|
49
|
+
render :edit
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def post_params
|
|
56
|
+
permitted = [:title, :body]
|
|
57
|
+
permitted << :published if can?(:update, @post, :published)
|
|
58
|
+
permitted << :featured if can?(:update, @post, :featured)
|
|
59
|
+
|
|
60
|
+
params.require(:post).permit(*permitted)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Conditional Permissions Based on State
|
|
66
|
+
|
|
67
|
+
### Time-Based Editing Windows
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# app/models/ability.rb
|
|
71
|
+
class Ability
|
|
72
|
+
include CanCan::Ability
|
|
73
|
+
|
|
74
|
+
def initialize(user)
|
|
75
|
+
return unless user.present?
|
|
76
|
+
|
|
77
|
+
# Can edit posts within 1 hour of creation
|
|
78
|
+
can :update, Post do |post|
|
|
79
|
+
post.user_id == user.id && post.created_at > 1.hour.ago
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Can delete comments within 15 minutes
|
|
83
|
+
can :destroy, Comment do |comment|
|
|
84
|
+
comment.user_id == user.id && comment.created_at > 15.minutes.ago
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Admins bypass time restrictions
|
|
88
|
+
if user.admin?
|
|
89
|
+
can :manage, [Post, Comment]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Workflow State Permissions
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# app/models/post.rb
|
|
99
|
+
class Post < ApplicationRecord
|
|
100
|
+
include AASM
|
|
101
|
+
|
|
102
|
+
aasm column: :status do
|
|
103
|
+
state :draft, initial: true
|
|
104
|
+
state :pending_review
|
|
105
|
+
state :published
|
|
106
|
+
state :archived
|
|
107
|
+
|
|
108
|
+
event :submit do
|
|
109
|
+
transitions from: :draft, to: :pending_review
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
event :publish do
|
|
113
|
+
transitions from: :pending_review, to: :published
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
event :archive do
|
|
117
|
+
transitions from: :published, to: :archived
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# app/models/ability.rb
|
|
123
|
+
class Ability
|
|
124
|
+
include CanCan::Ability
|
|
125
|
+
|
|
126
|
+
def initialize(user)
|
|
127
|
+
return unless user.present?
|
|
128
|
+
|
|
129
|
+
# Authors can edit their own drafts
|
|
130
|
+
can :update, Post, user_id: user.id, status: 'draft'
|
|
131
|
+
|
|
132
|
+
# Can submit for review
|
|
133
|
+
can :submit, Post, user_id: user.id, status: 'draft'
|
|
134
|
+
|
|
135
|
+
# Editors can review and publish
|
|
136
|
+
if user.editor? || user.admin?
|
|
137
|
+
can :publish, Post, status: 'pending_review'
|
|
138
|
+
can :update, Post, status: 'pending_review'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Only admins can archive
|
|
142
|
+
if user.admin?
|
|
143
|
+
can :archive, Post, status: 'published'
|
|
144
|
+
can :manage, Post, status: 'archived'
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Nobody can edit published posts (except admins)
|
|
148
|
+
cannot :update, Post, status: 'published' unless user.admin?
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Hierarchical Permissions
|
|
154
|
+
|
|
155
|
+
### Role Hierarchy
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# app/models/user.rb
|
|
159
|
+
class User < ApplicationRecord
|
|
160
|
+
enum role: {
|
|
161
|
+
viewer: 0,
|
|
162
|
+
contributor: 1,
|
|
163
|
+
editor: 2,
|
|
164
|
+
manager: 3,
|
|
165
|
+
admin: 4
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def role_level
|
|
169
|
+
User.roles[role]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def has_role?(check_role)
|
|
173
|
+
role_level >= User.roles[check_role.to_s]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# app/models/ability.rb
|
|
178
|
+
class Ability
|
|
179
|
+
include CanCan::Ability
|
|
180
|
+
|
|
181
|
+
def initialize(user)
|
|
182
|
+
return unless user.present?
|
|
183
|
+
|
|
184
|
+
# Everyone can read published content
|
|
185
|
+
can :read, Post, published: true
|
|
186
|
+
|
|
187
|
+
# Viewers (level 0) - read only
|
|
188
|
+
can :read, Post if user.has_role?(:viewer)
|
|
189
|
+
|
|
190
|
+
# Contributors (level 1) - can create and edit own
|
|
191
|
+
if user.has_role?(:contributor)
|
|
192
|
+
can :create, Post
|
|
193
|
+
can [:update, :destroy], Post, user_id: user.id
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Editors (level 2) - can manage others' content
|
|
197
|
+
if user.has_role?(:editor)
|
|
198
|
+
can :manage, Post
|
|
199
|
+
can :publish, Post
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Managers (level 3) - can manage users
|
|
203
|
+
if user.has_role?(:manager)
|
|
204
|
+
can :manage, [Post, Comment, Category]
|
|
205
|
+
can :read, User
|
|
206
|
+
can :update, User do |target_user|
|
|
207
|
+
target_user.role_level < user.role_level
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Admins (level 4) - full access
|
|
212
|
+
can :manage, :all if user.has_role?(:admin)
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Group and Team Permissions
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
# app/models/team.rb
|
|
221
|
+
class Team < ApplicationRecord
|
|
222
|
+
has_many :team_memberships
|
|
223
|
+
has_many :users, through: :team_memberships
|
|
224
|
+
has_many :projects
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# app/models/team_membership.rb
|
|
228
|
+
class TeamMembership < ApplicationRecord
|
|
229
|
+
belongs_to :user
|
|
230
|
+
belongs_to :team
|
|
231
|
+
|
|
232
|
+
enum role: { member: 0, lead: 1, owner: 2 }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# app/models/ability.rb
|
|
236
|
+
class Ability
|
|
237
|
+
include CanCan::Ability
|
|
238
|
+
|
|
239
|
+
def initialize(user)
|
|
240
|
+
return unless user.present?
|
|
241
|
+
|
|
242
|
+
# Get all teams user belongs to
|
|
243
|
+
team_ids = user.teams.pluck(:id)
|
|
244
|
+
|
|
245
|
+
# Can read team projects
|
|
246
|
+
can :read, Project, team_id: team_ids
|
|
247
|
+
|
|
248
|
+
# Can contribute to team projects
|
|
249
|
+
can :create, Task, project: { team_id: team_ids }
|
|
250
|
+
can :update, Task, project: { team_id: team_ids }, user_id: user.id
|
|
251
|
+
|
|
252
|
+
# Team leads can manage project tasks
|
|
253
|
+
lead_team_ids = user.team_memberships.where(role: [:lead, :owner]).pluck(:team_id)
|
|
254
|
+
can :manage, Task, project: { team_id: lead_team_ids }
|
|
255
|
+
|
|
256
|
+
# Team owners can manage teams
|
|
257
|
+
owned_team_ids = user.team_memberships.where(role: :owner).pluck(:team_id)
|
|
258
|
+
can :manage, Team, id: owned_team_ids
|
|
259
|
+
can :manage, TeamMembership, team_id: owned_team_ids
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Delegated Permissions
|
|
265
|
+
|
|
266
|
+
### Sharing and Collaboration
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# app/models/document.rb
|
|
270
|
+
class Document < ApplicationRecord
|
|
271
|
+
belongs_to :owner, class_name: 'User'
|
|
272
|
+
has_many :document_shares
|
|
273
|
+
has_many :shared_with_users, through: :document_shares, source: :user
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# app/models/document_share.rb
|
|
277
|
+
class DocumentShare < ApplicationRecord
|
|
278
|
+
belongs_to :document
|
|
279
|
+
belongs_to :user
|
|
280
|
+
|
|
281
|
+
enum permission: { read: 0, write: 1, admin: 2 }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# app/models/ability.rb
|
|
285
|
+
class Ability
|
|
286
|
+
include CanCan::Ability
|
|
287
|
+
|
|
288
|
+
def initialize(user)
|
|
289
|
+
return unless user.present?
|
|
290
|
+
|
|
291
|
+
# Owner has full control
|
|
292
|
+
can :manage, Document, owner_id: user.id
|
|
293
|
+
|
|
294
|
+
# Shared documents - read permission
|
|
295
|
+
can :read, Document, document_shares: { user_id: user.id, permission: [:read, :write, :admin] }
|
|
296
|
+
|
|
297
|
+
# Shared documents - write permission
|
|
298
|
+
can :update, Document, document_shares: { user_id: user.id, permission: [:write, :admin] }
|
|
299
|
+
|
|
300
|
+
# Shared documents - admin permission (can share with others)
|
|
301
|
+
can :share, Document, document_shares: { user_id: user.id, permission: :admin }
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# app/controllers/documents_controller.rb
|
|
306
|
+
class DocumentsController < ApplicationController
|
|
307
|
+
def share
|
|
308
|
+
@document = Document.find(params[:id])
|
|
309
|
+
authorize! :share, @document
|
|
310
|
+
|
|
311
|
+
@share = @document.document_shares.create(
|
|
312
|
+
user_id: params[:user_id],
|
|
313
|
+
permission: params[:permission]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
redirect_to @document, notice: 'Document shared successfully'
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Conditional Abilities Based on External Services
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
# app/models/ability.rb
|
|
325
|
+
class Ability
|
|
326
|
+
include CanCan::Ability
|
|
327
|
+
|
|
328
|
+
def initialize(user, context = {})
|
|
329
|
+
return unless user.present?
|
|
330
|
+
|
|
331
|
+
# Check subscription status from external service
|
|
332
|
+
if context[:subscription_service]
|
|
333
|
+
subscription = context[:subscription_service].get_subscription(user.id)
|
|
334
|
+
|
|
335
|
+
case subscription.plan
|
|
336
|
+
when 'free'
|
|
337
|
+
can :create, Post
|
|
338
|
+
cannot :create, Post if user.posts.count >= 5 # Free tier limit
|
|
339
|
+
when 'pro'
|
|
340
|
+
can :create, Post
|
|
341
|
+
can :upload, :attachments
|
|
342
|
+
when 'enterprise'
|
|
343
|
+
can :manage, :all
|
|
344
|
+
can :access, :analytics
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Feature flags
|
|
349
|
+
if context[:feature_flags]
|
|
350
|
+
can :access, :beta_features if context[:feature_flags].enabled?(:beta_features, user)
|
|
351
|
+
can :use, :ai_assistant if context[:feature_flags].enabled?(:ai_assistant, user)
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# app/controllers/application_controller.rb
|
|
357
|
+
def current_ability
|
|
358
|
+
@current_ability ||= Ability.new(current_user, {
|
|
359
|
+
subscription_service: SubscriptionService.new,
|
|
360
|
+
feature_flags: FeatureFlags.new
|
|
361
|
+
})
|
|
362
|
+
end
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## IP-Based Restrictions
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# app/models/ability.rb
|
|
369
|
+
class Ability
|
|
370
|
+
include CanCan::Ability
|
|
371
|
+
|
|
372
|
+
def initialize(user, request = nil)
|
|
373
|
+
return unless user.present?
|
|
374
|
+
|
|
375
|
+
can :read, Post
|
|
376
|
+
|
|
377
|
+
# Admin access only from specific IPs
|
|
378
|
+
if user.admin?
|
|
379
|
+
if request && allowed_admin_ip?(request.remote_ip)
|
|
380
|
+
can :manage, :all
|
|
381
|
+
else
|
|
382
|
+
can :read, :all
|
|
383
|
+
cannot :destroy, :all
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private
|
|
389
|
+
|
|
390
|
+
def allowed_admin_ip?(ip)
|
|
391
|
+
allowed_ranges = [
|
|
392
|
+
IPAddr.new('10.0.0.0/8'),
|
|
393
|
+
IPAddr.new('172.16.0.0/12'),
|
|
394
|
+
IPAddr.new('192.168.0.0/16')
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
allowed_ranges.any? { |range| range.include?(ip) }
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# app/controllers/application_controller.rb
|
|
402
|
+
def current_ability
|
|
403
|
+
@current_ability ||= Ability.new(current_user, request)
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Performance Optimization for Complex Rules
|
|
408
|
+
|
|
409
|
+
### Caching Abilities
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
# app/models/ability.rb
|
|
413
|
+
class Ability
|
|
414
|
+
include CanCan::Ability
|
|
415
|
+
|
|
416
|
+
def initialize(user)
|
|
417
|
+
return unless user.present?
|
|
418
|
+
|
|
419
|
+
# Cache expensive queries
|
|
420
|
+
@user_team_ids = user.teams.pluck(:id)
|
|
421
|
+
@user_organization_id = user.organization_id
|
|
422
|
+
|
|
423
|
+
define_permissions
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
private
|
|
427
|
+
|
|
428
|
+
def define_permissions
|
|
429
|
+
can :read, Project, team_id: @user_team_ids
|
|
430
|
+
can :read, Project, organization_id: @user_organization_id
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Eager Loading for Better Performance
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
# app/controllers/posts_controller.rb
|
|
439
|
+
class PostsController < ApplicationController
|
|
440
|
+
def index
|
|
441
|
+
@posts = Post
|
|
442
|
+
.includes(:user, :category)
|
|
443
|
+
.accessible_by(current_ability)
|
|
444
|
+
.page(params[:page])
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Using Database Views for Complex Permissions
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
# db/migrate/20240101000000_create_accessible_posts_view.rb
|
|
453
|
+
class CreateAccessiblePostsView < ActiveRecord::Migration[7.0]
|
|
454
|
+
def up
|
|
455
|
+
execute <<-SQL
|
|
456
|
+
CREATE VIEW accessible_posts AS
|
|
457
|
+
SELECT p.*,
|
|
458
|
+
u.organization_id,
|
|
459
|
+
CASE
|
|
460
|
+
WHEN p.published = true THEN true
|
|
461
|
+
WHEN p.user_id = u.id THEN true
|
|
462
|
+
WHEN EXISTS (
|
|
463
|
+
SELECT 1 FROM team_memberships tm
|
|
464
|
+
JOIN teams t ON tm.team_id = t.id
|
|
465
|
+
WHERE tm.user_id = u.id AND t.id = p.team_id
|
|
466
|
+
) THEN true
|
|
467
|
+
ELSE false
|
|
468
|
+
END as user_can_access
|
|
469
|
+
FROM posts p
|
|
470
|
+
CROSS JOIN users u
|
|
471
|
+
SQL
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def down
|
|
475
|
+
execute "DROP VIEW accessible_posts"
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Testing Complex Permissions
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
# spec/models/ability_spec.rb
|
|
484
|
+
require 'rails_helper'
|
|
485
|
+
require 'cancan/matchers'
|
|
486
|
+
|
|
487
|
+
RSpec.describe Ability, type: :model do
|
|
488
|
+
describe 'complex workflow permissions' do
|
|
489
|
+
let(:author) { create(:user, role: :contributor) }
|
|
490
|
+
let(:editor) { create(:user, role: :editor) }
|
|
491
|
+
let(:admin) { create(:user, role: :admin) }
|
|
492
|
+
|
|
493
|
+
let(:draft_post) { create(:post, user: author, status: :draft) }
|
|
494
|
+
let(:pending_post) { create(:post, user: author, status: :pending_review) }
|
|
495
|
+
let(:published_post) { create(:post, user: author, status: :published) }
|
|
496
|
+
|
|
497
|
+
describe 'author abilities' do
|
|
498
|
+
subject(:ability) { Ability.new(author) }
|
|
499
|
+
|
|
500
|
+
it { is_expected.to be_able_to(:update, draft_post) }
|
|
501
|
+
it { is_expected.to be_able_to(:submit, draft_post) }
|
|
502
|
+
it { is_expected.not_to be_able_to(:update, pending_post) }
|
|
503
|
+
it { is_expected.not_to be_able_to(:update, published_post) }
|
|
504
|
+
it { is_expected.not_to be_able_to(:publish, pending_post) }
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
describe 'editor abilities' do
|
|
508
|
+
subject(:ability) { Ability.new(editor) }
|
|
509
|
+
|
|
510
|
+
it { is_expected.to be_able_to(:update, pending_post) }
|
|
511
|
+
it { is_expected.to be_able_to(:publish, pending_post) }
|
|
512
|
+
it { is_expected.not_to be_able_to(:update, published_post) }
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
describe 'admin abilities' do
|
|
516
|
+
subject(:ability) { Ability.new(admin) }
|
|
517
|
+
|
|
518
|
+
it { is_expected.to be_able_to(:update, draft_post) }
|
|
519
|
+
it { is_expected.to be_able_to(:update, pending_post) }
|
|
520
|
+
it { is_expected.to be_able_to(:update, published_post) }
|
|
521
|
+
it { is_expected.to be_able_to(:archive, published_post) }
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
describe 'time-based permissions' do
|
|
526
|
+
let(:user) { create(:user) }
|
|
527
|
+
let(:recent_post) { create(:post, user: user, created_at: 30.minutes.ago) }
|
|
528
|
+
let(:old_post) { create(:post, user: user, created_at: 2.hours.ago) }
|
|
529
|
+
|
|
530
|
+
subject(:ability) { Ability.new(user) }
|
|
531
|
+
|
|
532
|
+
it { is_expected.to be_able_to(:update, recent_post) }
|
|
533
|
+
it { is_expected.not_to be_able_to(:update, old_post) }
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
describe 'delegated permissions' do
|
|
537
|
+
let(:owner) { create(:user) }
|
|
538
|
+
let(:viewer) { create(:user) }
|
|
539
|
+
let(:editor) { create(:user) }
|
|
540
|
+
let(:document) { create(:document, owner: owner) }
|
|
541
|
+
|
|
542
|
+
before do
|
|
543
|
+
create(:document_share, document: document, user: viewer, permission: :read)
|
|
544
|
+
create(:document_share, document: document, user: editor, permission: :write)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
it 'grants correct permissions to shared users' do
|
|
548
|
+
viewer_ability = Ability.new(viewer)
|
|
549
|
+
editor_ability = Ability.new(editor)
|
|
550
|
+
|
|
551
|
+
expect(viewer_ability).to be_able_to(:read, document)
|
|
552
|
+
expect(viewer_ability).not_to be_able_to(:update, document)
|
|
553
|
+
|
|
554
|
+
expect(editor_ability).to be_able_to(:read, document)
|
|
555
|
+
expect(editor_ability).to be_able_to(:update, document)
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
## Best Practices
|
|
562
|
+
|
|
563
|
+
1. **Keep it readable**: Complex logic should be well-commented
|
|
564
|
+
2. **Use methods**: Extract complex conditions into well-named methods
|
|
565
|
+
3. **Cache expensive queries**: Store team IDs, organization IDs in variables
|
|
566
|
+
4. **Test thoroughly**: Cover all edge cases and state transitions
|
|
567
|
+
5. **Document business rules**: Explain why permissions exist
|
|
568
|
+
6. **Avoid over-engineering**: Start simple, add complexity only when needed
|
|
569
|
+
7. **Monitor performance**: Use database indexes and eager loading
|
|
570
|
+
8. **Separate concerns**: Use different ability classes for different contexts if needed
|
|
571
|
+
9. **Version control**: Track permission changes in version control
|
|
572
|
+
10. **Audit access**: Log permission checks for sensitive operations
|