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
data/lib/generators/claude/skills_library/rails-authorization-cancancan/references/multi-tenancy.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# Multi-Tenancy Authorization with CanCanCan
|
|
2
|
+
|
|
3
|
+
Managing permissions in multi-tenant applications where users belong to organizations, accounts, or workspaces.
|
|
4
|
+
|
|
5
|
+
## Basic Multi-Tenant Setup
|
|
6
|
+
|
|
7
|
+
### Organization Model
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# app/models/organization.rb
|
|
11
|
+
class Organization < ApplicationRecord
|
|
12
|
+
has_many :users
|
|
13
|
+
has_many :posts
|
|
14
|
+
has_many :projects
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# app/models/user.rb
|
|
18
|
+
class User < ApplicationRecord
|
|
19
|
+
belongs_to :organization
|
|
20
|
+
|
|
21
|
+
enum role: { member: 0, manager: 1, admin: 2 }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# app/models/post.rb
|
|
25
|
+
class Post < ApplicationRecord
|
|
26
|
+
belongs_to :organization
|
|
27
|
+
belongs_to :user
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Ability Class for Multi-Tenancy
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# app/models/ability.rb
|
|
35
|
+
class Ability
|
|
36
|
+
include CanCan::Ability
|
|
37
|
+
|
|
38
|
+
def initialize(user)
|
|
39
|
+
return unless user.present?
|
|
40
|
+
|
|
41
|
+
# Scope all resources to user's organization
|
|
42
|
+
can :read, Post, organization_id: user.organization_id
|
|
43
|
+
can :create, Post, organization_id: user.organization_id
|
|
44
|
+
can :update, Post, organization_id: user.organization_id, user_id: user.id
|
|
45
|
+
can :destroy, Post, organization_id: user.organization_id, user_id: user.id
|
|
46
|
+
|
|
47
|
+
# Managers can manage all posts in their organization
|
|
48
|
+
if user.manager? || user.admin?
|
|
49
|
+
can :manage, Post, organization_id: user.organization_id
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Only admins can manage organization settings
|
|
53
|
+
if user.admin?
|
|
54
|
+
can :manage, Organization, id: user.organization_id
|
|
55
|
+
can :manage, User, organization_id: user.organization_id
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Controller Setup
|
|
62
|
+
|
|
63
|
+
### Setting Organization Context
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# app/controllers/application_controller.rb
|
|
67
|
+
class ApplicationController < ActionController::Base
|
|
68
|
+
before_action :authenticate_user!
|
|
69
|
+
before_action :set_organization
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def set_organization
|
|
74
|
+
@current_organization = current_user.organization
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def current_organization
|
|
78
|
+
@current_organization
|
|
79
|
+
end
|
|
80
|
+
helper_method :current_organization
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Scoping Resources
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# app/controllers/posts_controller.rb
|
|
88
|
+
class PostsController < ApplicationController
|
|
89
|
+
load_and_authorize_resource
|
|
90
|
+
|
|
91
|
+
def index
|
|
92
|
+
# @posts automatically scoped to organization via accessible_by
|
|
93
|
+
@posts = @posts.where(organization_id: current_organization.id)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def create
|
|
97
|
+
@post.organization = current_organization
|
|
98
|
+
if @post.save
|
|
99
|
+
redirect_to @post
|
|
100
|
+
else
|
|
101
|
+
render :new
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def post_params
|
|
108
|
+
params.require(:post).permit(:title, :body)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Account-Based Multi-Tenancy
|
|
114
|
+
|
|
115
|
+
For apps where users can belong to multiple accounts/workspaces:
|
|
116
|
+
|
|
117
|
+
### Models
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
# app/models/user.rb
|
|
121
|
+
class User < ApplicationRecord
|
|
122
|
+
has_many :memberships
|
|
123
|
+
has_many :accounts, through: :memberships
|
|
124
|
+
|
|
125
|
+
def current_account=(account)
|
|
126
|
+
@current_account = account
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def current_account
|
|
130
|
+
@current_account
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# app/models/membership.rb
|
|
135
|
+
class Membership < ApplicationRecord
|
|
136
|
+
belongs_to :user
|
|
137
|
+
belongs_to :account
|
|
138
|
+
|
|
139
|
+
enum role: { member: 0, admin: 1, owner: 2 }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# app/models/account.rb
|
|
143
|
+
class Account < ApplicationRecord
|
|
144
|
+
has_many :memberships
|
|
145
|
+
has_many :users, through: :memberships
|
|
146
|
+
has_many :projects
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Ability with Current Account
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# app/models/ability.rb
|
|
154
|
+
class Ability
|
|
155
|
+
include CanCan::Ability
|
|
156
|
+
|
|
157
|
+
def initialize(user, current_account = nil)
|
|
158
|
+
return unless user.present?
|
|
159
|
+
return unless current_account.present?
|
|
160
|
+
|
|
161
|
+
# Find user's membership in current account
|
|
162
|
+
membership = user.memberships.find_by(account: current_account)
|
|
163
|
+
return unless membership
|
|
164
|
+
|
|
165
|
+
# Base permissions for all members
|
|
166
|
+
can :read, Project, account_id: current_account.id
|
|
167
|
+
|
|
168
|
+
# Members can create projects
|
|
169
|
+
if membership.member? || membership.admin? || membership.owner?
|
|
170
|
+
can :create, Project, account_id: current_account.id
|
|
171
|
+
can :update, Project, account_id: current_account.id, user_id: user.id
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Admins and owners can manage all projects
|
|
175
|
+
if membership.admin? || membership.owner?
|
|
176
|
+
can :manage, Project, account_id: current_account.id
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Only owners can manage account settings
|
|
180
|
+
if membership.owner?
|
|
181
|
+
can :manage, Account, id: current_account.id
|
|
182
|
+
can :manage, Membership, account_id: current_account.id
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Controller with Account Context
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# app/controllers/application_controller.rb
|
|
192
|
+
class ApplicationController < ActionController::Base
|
|
193
|
+
before_action :authenticate_user!
|
|
194
|
+
before_action :set_current_account
|
|
195
|
+
|
|
196
|
+
private
|
|
197
|
+
|
|
198
|
+
def set_current_account
|
|
199
|
+
if params[:account_id]
|
|
200
|
+
@current_account = current_user.accounts.find(params[:account_id])
|
|
201
|
+
current_user.current_account = @current_account
|
|
202
|
+
elsif session[:current_account_id]
|
|
203
|
+
@current_account = current_user.accounts.find_by(id: session[:current_account_id])
|
|
204
|
+
else
|
|
205
|
+
@current_account = current_user.accounts.first
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
session[:current_account_id] = @current_account&.id
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def current_account
|
|
212
|
+
@current_account
|
|
213
|
+
end
|
|
214
|
+
helper_method :current_account
|
|
215
|
+
|
|
216
|
+
def current_ability
|
|
217
|
+
@current_ability ||= Ability.new(current_user, current_account)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Account Switcher in Views
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# app/views/layouts/_account_switcher.html.erb
|
|
226
|
+
<div class="account-switcher">
|
|
227
|
+
<% current_user.accounts.each do |account| %>
|
|
228
|
+
<%= link_to account.name,
|
|
229
|
+
account_path(account),
|
|
230
|
+
class: (account == current_account ? 'active' : '') %>
|
|
231
|
+
<% end %>
|
|
232
|
+
</div>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Row-Level Security Pattern
|
|
236
|
+
|
|
237
|
+
For more complex authorization with nested resources:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
class Ability
|
|
241
|
+
include CanCan::Ability
|
|
242
|
+
|
|
243
|
+
def initialize(user, current_account = nil)
|
|
244
|
+
return unless user.present?
|
|
245
|
+
return unless current_account.present?
|
|
246
|
+
|
|
247
|
+
# Projects
|
|
248
|
+
can :manage, Project, account_id: current_account.id
|
|
249
|
+
|
|
250
|
+
# Tasks belong to projects, which belong to accounts
|
|
251
|
+
can :read, Task, project: { account_id: current_account.id }
|
|
252
|
+
can :create, Task, project: { account_id: current_account.id }
|
|
253
|
+
can :update, Task, project: { account_id: current_account.id }
|
|
254
|
+
|
|
255
|
+
# Comments belong to tasks, which belong to projects, which belong to accounts
|
|
256
|
+
can :read, Comment, task: { project: { account_id: current_account.id } }
|
|
257
|
+
can :create, Comment, task: { project: { account_id: current_account.id } }
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Testing Multi-Tenancy
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
# spec/models/ability_spec.rb
|
|
266
|
+
require 'rails_helper'
|
|
267
|
+
require 'cancan/matchers'
|
|
268
|
+
|
|
269
|
+
RSpec.describe Ability, type: :model do
|
|
270
|
+
let(:organization1) { create(:organization) }
|
|
271
|
+
let(:organization2) { create(:organization) }
|
|
272
|
+
let(:user) { create(:user, organization: organization1) }
|
|
273
|
+
let(:post_in_org1) { create(:post, organization: organization1) }
|
|
274
|
+
let(:post_in_org2) { create(:post, organization: organization2) }
|
|
275
|
+
|
|
276
|
+
subject(:ability) { Ability.new(user) }
|
|
277
|
+
|
|
278
|
+
it 'allows access to own organization resources' do
|
|
279
|
+
expect(ability).to be_able_to(:read, post_in_org1)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'denies access to other organization resources' do
|
|
283
|
+
expect(ability).not_to be_able_to(:read, post_in_org2)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
describe 'with multiple accounts' do
|
|
287
|
+
let(:account1) { create(:account) }
|
|
288
|
+
let(:account2) { create(:account) }
|
|
289
|
+
let(:user) { create(:user) }
|
|
290
|
+
let!(:membership1) { create(:membership, user: user, account: account1, role: :admin) }
|
|
291
|
+
let!(:membership2) { create(:membership, user: user, account: account2, role: :member) }
|
|
292
|
+
|
|
293
|
+
it 'has different permissions based on current account' do
|
|
294
|
+
ability_in_account1 = Ability.new(user, account1)
|
|
295
|
+
ability_in_account2 = Ability.new(user, account2)
|
|
296
|
+
|
|
297
|
+
expect(ability_in_account1).to be_able_to(:manage, Project.new(account: account1))
|
|
298
|
+
expect(ability_in_account2).not_to be_able_to(:manage, Project.new(account: account2))
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Common Pitfalls
|
|
305
|
+
|
|
306
|
+
### 1. Forgetting to Scope Queries
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Bad - exposes all organizations' data
|
|
310
|
+
def index
|
|
311
|
+
@posts = Post.accessible_by(current_ability)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Good - explicitly scope to current organization
|
|
315
|
+
def index
|
|
316
|
+
@posts = current_organization.posts.accessible_by(current_ability)
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 2. Not Setting Organization on Create
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
# Bad - user could potentially set any organization_id
|
|
324
|
+
def create
|
|
325
|
+
@post = Post.new(post_params)
|
|
326
|
+
authorize! :create, @post
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Good - always set organization from context
|
|
330
|
+
def create
|
|
331
|
+
@post = current_organization.posts.new(post_params)
|
|
332
|
+
@post.user = current_user
|
|
333
|
+
authorize! :create, @post
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### 3. Missing Organization in Nested Resources
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
# app/controllers/comments_controller.rb
|
|
341
|
+
class CommentsController < ApplicationController
|
|
342
|
+
before_action :set_post
|
|
343
|
+
|
|
344
|
+
def create
|
|
345
|
+
# Verify post belongs to current organization
|
|
346
|
+
authorize! :read, @post
|
|
347
|
+
|
|
348
|
+
@comment = @post.comments.build(comment_params)
|
|
349
|
+
@comment.user = current_user
|
|
350
|
+
|
|
351
|
+
if @comment.save
|
|
352
|
+
redirect_to @post
|
|
353
|
+
else
|
|
354
|
+
render 'posts/show'
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
private
|
|
359
|
+
|
|
360
|
+
def set_post
|
|
361
|
+
@post = current_organization.posts.find(params[:post_id])
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Best Practices
|
|
367
|
+
|
|
368
|
+
1. **Always scope to tenant**: Use `current_organization.posts` instead of `Post.all`
|
|
369
|
+
2. **Set organization on create**: Automatically assign organization from context
|
|
370
|
+
3. **Verify nested resources**: Ensure parent resources belong to current tenant
|
|
371
|
+
4. **Test cross-tenant access**: Write specs that verify users can't access other tenants' data
|
|
372
|
+
5. **Use database constraints**: Add foreign keys and indexes on organization_id
|
|
373
|
+
6. **Cache ability per account**: Use `@current_ability ||= Ability.new(current_user, current_account)`
|