source_monitor 0.2.1 → 0.3.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 +4 -4
- data/.claude/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +113 -100
- data/Rakefile +2 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +141 -4
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authorization-pundit
|
|
3
|
+
description: Implements policy-based authorization with Pundit for resource access control. Use when adding authorization rules, checking permissions, restricting actions, role-based access, or when user mentions Pundit, policies, authorization, or permissions.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Authorization with Pundit for Rails 8
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Pundit provides policy-based authorization:
|
|
12
|
+
- Plain Ruby policy objects
|
|
13
|
+
- Convention over configuration
|
|
14
|
+
- Easy to test with Minitest
|
|
15
|
+
- Scoped queries for collections
|
|
16
|
+
- Works with any authentication system
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle add pundit
|
|
22
|
+
bin/rails generate pundit:install
|
|
23
|
+
bin/rails generate pundit:policy Event
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## TDD Workflow
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Authorization Progress:
|
|
30
|
+
- [ ] Step 1: Write policy test (RED)
|
|
31
|
+
- [ ] Step 2: Run test (fails)
|
|
32
|
+
- [ ] Step 3: Implement policy
|
|
33
|
+
- [ ] Step 4: Run test (GREEN)
|
|
34
|
+
- [ ] Step 5: Add policy to controller
|
|
35
|
+
- [ ] Step 6: Test integration
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Base Policy
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# app/policies/application_policy.rb
|
|
42
|
+
class ApplicationPolicy
|
|
43
|
+
attr_reader :user, :record
|
|
44
|
+
|
|
45
|
+
def initialize(user, record)
|
|
46
|
+
@user = user
|
|
47
|
+
@record = record
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def index?
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def show?
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create?
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def new?
|
|
63
|
+
create?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def update?
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def edit?
|
|
71
|
+
update?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def destroy?
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Scope
|
|
79
|
+
def initialize(user, scope)
|
|
80
|
+
@user = user
|
|
81
|
+
@scope = scope
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve
|
|
85
|
+
raise NotImplementedError, "Define #resolve in #{self.class}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
attr_reader :user, :scope
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Policy Testing (Minitest)
|
|
96
|
+
|
|
97
|
+
### Basic Policy Test
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# test/policies/event_policy_test.rb
|
|
101
|
+
require "test_helper"
|
|
102
|
+
|
|
103
|
+
class EventPolicyTest < ActiveSupport::TestCase
|
|
104
|
+
setup do
|
|
105
|
+
@account = accounts(:one)
|
|
106
|
+
@user = users(:one) # belongs to @account
|
|
107
|
+
@other_user = users(:other_account) # different account
|
|
108
|
+
@event = events(:one) # belongs to @account
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# -- index --
|
|
112
|
+
test "index? permits any authenticated user" do
|
|
113
|
+
policy = EventPolicy.new(@user, Event)
|
|
114
|
+
assert policy.index?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# -- show --
|
|
118
|
+
test "show? permits user from same account" do
|
|
119
|
+
policy = EventPolicy.new(@user, @event)
|
|
120
|
+
assert policy.show?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
test "show? denies user from different account" do
|
|
124
|
+
policy = EventPolicy.new(@other_user, @event)
|
|
125
|
+
assert_not policy.show?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# -- create --
|
|
129
|
+
test "create? permits user from same account" do
|
|
130
|
+
new_event = Event.new(account: @account)
|
|
131
|
+
policy = EventPolicy.new(@user, new_event)
|
|
132
|
+
assert policy.create?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# -- update --
|
|
136
|
+
test "update? permits user from same account" do
|
|
137
|
+
policy = EventPolicy.new(@user, @event)
|
|
138
|
+
assert policy.update?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
test "update? denies user from different account" do
|
|
142
|
+
policy = EventPolicy.new(@other_user, @event)
|
|
143
|
+
assert_not policy.update?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# -- destroy --
|
|
147
|
+
test "destroy? permits user from same account" do
|
|
148
|
+
policy = EventPolicy.new(@user, @event)
|
|
149
|
+
assert policy.destroy?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
test "destroy? denies user from different account" do
|
|
153
|
+
policy = EventPolicy.new(@other_user, @event)
|
|
154
|
+
assert_not policy.destroy?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# -- Scope --
|
|
158
|
+
test "Scope returns events for user account only" do
|
|
159
|
+
scope = EventPolicy::Scope.new(@user, Event).resolve
|
|
160
|
+
|
|
161
|
+
scope.each do |event|
|
|
162
|
+
assert_equal @user.account_id, event.account_id
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
test "Scope excludes other account events" do
|
|
167
|
+
other_event = events(:other_account)
|
|
168
|
+
scope = EventPolicy::Scope.new(@user, Event).resolve
|
|
169
|
+
|
|
170
|
+
assert_not_includes scope, other_event
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Role-Based Policy Test
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# test/policies/event_policy_test.rb (role-based extension)
|
|
179
|
+
class EventPolicyRoleTest < ActiveSupport::TestCase
|
|
180
|
+
setup do
|
|
181
|
+
@admin = users(:admin)
|
|
182
|
+
@member = users(:one)
|
|
183
|
+
@event = events(:one)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
test "destroy? permits admin" do
|
|
187
|
+
policy = EventPolicy.new(@admin, @event)
|
|
188
|
+
assert policy.destroy?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
test "publish? permits owner for draft events" do
|
|
192
|
+
@event.update(status: :draft)
|
|
193
|
+
policy = EventPolicy.new(@member, @event)
|
|
194
|
+
assert policy.publish?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
test "publish? denies for non-draft events" do
|
|
198
|
+
@event.update(status: :published)
|
|
199
|
+
policy = EventPolicy.new(@member, @event)
|
|
200
|
+
assert_not policy.publish?
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Policy Implementation
|
|
206
|
+
|
|
207
|
+
### Basic Policy (Account-Scoped)
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# app/policies/event_policy.rb
|
|
211
|
+
class EventPolicy < ApplicationPolicy
|
|
212
|
+
def index?
|
|
213
|
+
true
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def show?
|
|
217
|
+
owner?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def create?
|
|
221
|
+
true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def update?
|
|
225
|
+
owner?
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def destroy?
|
|
229
|
+
owner?
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
def owner?
|
|
235
|
+
record.account_id == user.account_id
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
class Scope < ApplicationPolicy::Scope
|
|
239
|
+
def resolve
|
|
240
|
+
scope.where(account_id: user.account_id)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Role-Based Policy
|
|
247
|
+
|
|
248
|
+
```ruby
|
|
249
|
+
# app/policies/event_policy.rb
|
|
250
|
+
class EventPolicy < ApplicationPolicy
|
|
251
|
+
def index?
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def show?
|
|
256
|
+
owner? || admin?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def create?
|
|
260
|
+
member_or_above?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def update?
|
|
264
|
+
owner_or_admin?
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def destroy?
|
|
268
|
+
admin?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def publish?
|
|
272
|
+
owner_or_admin? && record.draft?
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
private
|
|
276
|
+
|
|
277
|
+
def owner?
|
|
278
|
+
record.account_id == user.account_id
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def admin?
|
|
282
|
+
user.admin?
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def member_or_above?
|
|
286
|
+
user.member? || user.admin?
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def owner_or_admin?
|
|
290
|
+
owner? || admin?
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
class Scope < ApplicationPolicy::Scope
|
|
294
|
+
def resolve
|
|
295
|
+
if user.admin?
|
|
296
|
+
scope.all
|
|
297
|
+
else
|
|
298
|
+
scope.where(account_id: user.account_id)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Controller Integration
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# app/controllers/events_controller.rb
|
|
309
|
+
class EventsController < ApplicationController
|
|
310
|
+
def index
|
|
311
|
+
@events = policy_scope(Event)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def show
|
|
315
|
+
@event = Event.find(params[:id])
|
|
316
|
+
authorize @event
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def create
|
|
320
|
+
@event = current_account.events.build(event_params)
|
|
321
|
+
authorize @event
|
|
322
|
+
|
|
323
|
+
if @event.save
|
|
324
|
+
redirect_to @event, notice: t(".success")
|
|
325
|
+
else
|
|
326
|
+
render :new, status: :unprocessable_entity
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def destroy
|
|
331
|
+
@event = Event.find(params[:id])
|
|
332
|
+
authorize @event
|
|
333
|
+
@event.destroy
|
|
334
|
+
redirect_to events_path, notice: t(".success")
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Ensuring Authorization
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
# app/controllers/application_controller.rb
|
|
343
|
+
class ApplicationController < ActionController::Base
|
|
344
|
+
include Pundit::Authorization
|
|
345
|
+
|
|
346
|
+
after_action :verify_authorized, except: :index
|
|
347
|
+
after_action :verify_policy_scoped, only: :index
|
|
348
|
+
|
|
349
|
+
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
|
350
|
+
|
|
351
|
+
private
|
|
352
|
+
|
|
353
|
+
def user_not_authorized
|
|
354
|
+
flash[:alert] = t("pundit.not_authorized")
|
|
355
|
+
redirect_back(fallback_location: root_path)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Testing Controller Authorization
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# test/controllers/events_controller_test.rb
|
|
364
|
+
require "test_helper"
|
|
365
|
+
|
|
366
|
+
class EventsControllerTest < ActionDispatch::IntegrationTest
|
|
367
|
+
setup do
|
|
368
|
+
@user = users(:one)
|
|
369
|
+
@other_user = users(:other_account)
|
|
370
|
+
@event = events(:one) # belongs to @user's account
|
|
371
|
+
@other_event = events(:other_account)
|
|
372
|
+
sign_in @user
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
test "allows access to own events" do
|
|
376
|
+
get event_path(@event)
|
|
377
|
+
assert_response :success
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
test "denies access to other account events" do
|
|
381
|
+
get event_path(@other_event)
|
|
382
|
+
assert_redirected_to root_path
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
test "allows deletion of own events" do
|
|
386
|
+
assert_difference("Event.count", -1) do
|
|
387
|
+
delete event_path(@event)
|
|
388
|
+
end
|
|
389
|
+
assert_redirected_to events_path
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
test "denies deletion of other account events" do
|
|
393
|
+
assert_no_difference("Event.count") do
|
|
394
|
+
delete event_path(@other_event)
|
|
395
|
+
end
|
|
396
|
+
assert_redirected_to root_path
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## View Integration
|
|
402
|
+
|
|
403
|
+
```erb
|
|
404
|
+
<%# app/views/events/show.html.erb %>
|
|
405
|
+
<h1><%= @event.name %></h1>
|
|
406
|
+
|
|
407
|
+
<% if policy(@event).edit? %>
|
|
408
|
+
<%= link_to t("common.edit"), edit_event_path(@event) %>
|
|
409
|
+
<% end %>
|
|
410
|
+
|
|
411
|
+
<% if policy(@event).destroy? %>
|
|
412
|
+
<%= button_to t("common.delete"), @event, method: :delete,
|
|
413
|
+
data: { confirm: t("common.confirm_delete") } %>
|
|
414
|
+
<% end %>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Headless Policies
|
|
418
|
+
|
|
419
|
+
For actions not tied to a specific record:
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
# app/policies/dashboard_policy.rb
|
|
423
|
+
class DashboardPolicy < ApplicationPolicy
|
|
424
|
+
def initialize(user, _record = nil)
|
|
425
|
+
@user = user
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def show?
|
|
429
|
+
true
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def admin_panel?
|
|
433
|
+
user.admin?
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
# Controller
|
|
440
|
+
authorize :dashboard, :admin_panel?
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Error Messages
|
|
444
|
+
|
|
445
|
+
```yaml
|
|
446
|
+
# config/locales/en.yml
|
|
447
|
+
en:
|
|
448
|
+
pundit:
|
|
449
|
+
not_authorized: You are not authorized to perform this action.
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Checklist
|
|
453
|
+
|
|
454
|
+
- [ ] Policy test written first (RED)
|
|
455
|
+
- [ ] Policy inherits from ApplicationPolicy
|
|
456
|
+
- [ ] Scope defined for collections
|
|
457
|
+
- [ ] Controller uses `authorize` and `policy_scope`
|
|
458
|
+
- [ ] `verify_authorized` after_action enabled
|
|
459
|
+
- [ ] Views use `policy(@record).action?`
|
|
460
|
+
- [ ] Error handling configured
|
|
461
|
+
- [ ] Multi-tenancy enforced in Scope
|
|
462
|
+
- [ ] All tests GREEN
|