placet-rails 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db4bfcb4a459bf5fa9fa800a098e9ac53b7270e18c4d6e169188b6aec61f5858
4
+ data.tar.gz: 5bd4e349f816f0dab05a594330e13b367b0573ba0767f51c085064fdfa6f1cd7
5
+ SHA512:
6
+ metadata.gz: d1bef0f2695c4ff710efc1769b95e1376f82cb46addb4821181caf2a7aef4bf12f939ecc2ebb87bdf2eb3252de9622b915e74eda9ed5a1b15f49328d39fa913a
7
+ data.tar.gz: 1e1fd5cabd3b03328dc796df594e2a8ca1fdd68f8c76b64b3137dd56012335f988ea154d9e0b8a236f3a7d635b92eb4b85727039b2fc4309398005d5075c0761
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yugo TERADA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # placet-rails
2
+
3
+ > placet の Rails アダプタ — 宣言的な per-action 認可と ActiveRecord への scope 合成
4
+
5
+ [placet](../../README.md) を Rails に統合するアダプタ gem。設計の全体像は [docs/rails-usage.md](../../docs/rails-usage.md) を参照。
6
+
7
+ ## インストール
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ gem "placet-rails" # placet 本体も依存として入る
12
+ ```
13
+
14
+ ## 提供するもの
15
+
16
+ - **コントローラ統合(PEP ヘルパー)** — `placet_permit`(check / scope の 2 モード、複数 action は AND)、`placet_resource`(RESTful 規約の展開)、`placet_public`(明示オプトアウト)、`placet_verify!`(enforcement 漏れの検知)、`placet_scope` / `placet_permit?` ヘルパー
17
+ - **ActiveRecord への scope 写像** — コアの ScopePlan(empty / all / union ± exclude)を主キーのサブクエリとして `ActiveRecord::Relation` に合成する。返るのは素の Relation なので、ページネーション・ORDER・`count` がそのまま動く
18
+ - **Railtie** — `config/placet/**/*.rb` の定義を起動時にロード(不備は起動エラー)。`rails placet:export` / `rails placet:endpoints` タスク
19
+
20
+ ## 使い方の骨子
21
+
22
+ ```ruby
23
+ class ApplicationController < ActionController::Base
24
+ include Placet::Rails::Controller
25
+ placet_verify!
26
+
27
+ rescue_from Placet::Denied do |error|
28
+ Rails.logger.info(error.decision.to_h) # 根拠は監査ログへ
29
+ head :forbidden # ユーザーには理由を出さない
30
+ end
31
+ end
32
+
33
+ class PostsController < ApplicationController
34
+ before_action :set_post, only: %i[update destroy]
35
+ placet_resource Post
36
+ placet_permit "post:publish", only: :publish, resource: -> { @post }
37
+
38
+ def index = render(json: placet_scope.order(created_at: :desc).page(params[:page]))
39
+ def show = render(json: placet_scope.find(params[:id])) # 見えなければ 404
40
+ end
41
+ ```
42
+
43
+ ## テストの実行
44
+
45
+ ```sh
46
+ cd packages/rails
47
+ bundle install
48
+ for f in test/*_test.rb; do bundle exec ruby -Ilib -Itest "$f"; done
49
+ ```
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Placet
4
+ module Rails
5
+ # ScopePlan → ActiveRecord::Relation の写像(concept.md 8.4 の集合演算を SQL に落とす)。
6
+ # relation の scope 同士の構造差(joins の有無など)に依存しないよう、
7
+ # 和集合・差集合は主キーのサブクエリとして合成する
8
+ module ActiveRecordScope
9
+ module_function
10
+
11
+ def compose(plan, relations, user, model)
12
+ return model.none if plan.kind == "empty"
13
+
14
+ pk = model.primary_key
15
+ base =
16
+ if plan.kind == "all"
17
+ model.all
18
+ else
19
+ Placet.relation_scopes(relations, plan.include_relations, user)
20
+ .map { |scope| model.where(pk => scope.select(pk)) }
21
+ .reduce { |a, b| a.or(b) }
22
+ end
23
+ Placet.relation_scopes(relations, plan.exclude_relations, user)
24
+ .reduce(base) { |rel, scope| rel.where.not(pk => scope.select(pk)) }
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ # ActiveRecord モデルに対する ScopePlan の実体化として登録する
31
+ Placet.register_scope_materializer(
32
+ ->(model) { defined?(ActiveRecord::Base) && model.is_a?(Class) && model <= ActiveRecord::Base },
33
+ Placet::Rails::ActiveRecordScope.method(:compose)
34
+ )
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Placet
6
+ module Rails
7
+ # placet_verify! が enforcement 漏れを検出したときに送出される
8
+ class VerificationError < Placet::Error; end
9
+
10
+ # コントローラ統合(PEP ヘルパー。docs/rails-usage.md Section 4)
11
+ #
12
+ # class ApplicationController < ActionController::Base
13
+ # include Placet::Rails::Controller
14
+ # placet_verify!
15
+ # end
16
+ #
17
+ # class PostsController < ApplicationController
18
+ # before_action :set_post, only: %i[update destroy]
19
+ # placet_resource Post
20
+ # placet_permit "post:publish", only: :publish, resource: -> { @post }
21
+ # end
22
+ module Controller
23
+ extend ActiveSupport::Concern
24
+
25
+ included do
26
+ class_attribute :placet_declarations, default: {}, instance_writer: false
27
+ class_attribute :placet_public_actions, default: [], instance_writer: false
28
+ class_attribute :placet_verify_enabled, default: false, instance_writer: false
29
+ class_attribute :placet_enforcer_installed, default: false, instance_writer: false
30
+ helper_method :placet_permit?, :placet_user if respond_to?(:helper_method)
31
+ end
32
+
33
+ class_methods do
34
+ # action ごとの権限宣言。
35
+ # via: :check — before_action として authorize! を実行(403)
36
+ # via: :scope — placet_scope で合成済みコレクションを取得(絞り込み / 404)
37
+ # 複数 action は AND。via: :scope は単一 action のみ(docs/rails-usage.md 4.2)
38
+ def placet_permit(actions, only:, via: :check, resource: nil, model: nil)
39
+ actions = Array(actions).map(&:to_s)
40
+ if via == :scope && actions.size > 1
41
+ raise Placet::DefinitionError, "via: :scope の宣言は単一 action のみ: #{actions.inspect}"
42
+ end
43
+
44
+ # typo は「静かな全拒否 / 空 scope」になるため宣言時に検査する(検証規則はコアに一元化)
45
+ actions.each { |action| Placet.validate_action!(action, error_class: Placet::DefinitionError) }
46
+
47
+ declaration = { actions: actions, via: via, resource: resource, model: model }
48
+ if via == :scope && model.nil?
49
+ # 規約推論(resource セグメント → モデル名)は宣言時に解決しておく。
50
+ # リロードと相性を保つため、クラス参照ではなく名前文字列を保持する
51
+ declaration[:model_name] = actions.first.split(":", 2).first.camelize
52
+ end
53
+
54
+ merged = Array(only).map(&:to_s).to_h { |name| [name, (placet_declarations[name] || []) + [declaration]] }
55
+ self.placet_declarations = placet_declarations.merge(merged)
56
+
57
+ install_placet_enforcer if via == :check
58
+ end
59
+
60
+ # RESTful CRUD の規約展開(docs/rails-usage.md 4.1)。
61
+ # index/show → <resource>:view (scope)、create/update/destroy → check。
62
+ # リソースのロードは肩代わりしない: update/destroy は @<resource> を参照する
63
+ def placet_resource(model)
64
+ name = model.name.underscore
65
+ ref = -> { instance_variable_get(:"@#{name}") }
66
+ placet_permit "#{name}:view", only: %i[index show], via: :scope, model: model
67
+ placet_permit "#{name}:create", only: :create
68
+ placet_permit "#{name}:update", only: :update, resource: ref
69
+ placet_permit "#{name}:delete", only: :destroy, resource: ref
70
+ end
71
+
72
+ # 認可不要のエンドポイントを明示的にオプトアウトする(verify の対象から外す唯一の方法)
73
+ def placet_public(only:)
74
+ self.placet_public_actions = placet_public_actions + Array(only).map(&:to_s)
75
+ end
76
+
77
+ # enforcement(authorize! / scoped)が一度も行われずにアクションが終わったら raise。
78
+ # development / test で必ず有効化することを推奨(docs/rails-usage.md 4.4)
79
+ def placet_verify!
80
+ return if placet_verify_enabled
81
+
82
+ self.placet_verify_enabled = true
83
+ prepend_before_action { @_placet_enforcements_before = Placet.enforcements }
84
+ after_action :placet_verify_enforcement!
85
+ end
86
+
87
+ private
88
+
89
+ # check モードの enforcement は、宣言ハッシュを唯一の真実として 1 本の
90
+ # before_action で行う(endpoints タスク・verify と同じデータを見る)。
91
+ # 最初の check 宣言の位置で登録されるため、リソースをロードする
92
+ # before_action はそれより前に宣言しておくこと
93
+ def install_placet_enforcer
94
+ return if placet_enforcer_installed
95
+
96
+ self.placet_enforcer_installed = true
97
+ before_action :placet_enforce!
98
+ end
99
+ end
100
+
101
+ # アプリ側で上書き可能。既定は current_user
102
+ def placet_user = current_user
103
+
104
+ # via: :scope の宣言から合成済みコレクション(AR なら Relation)を返す
105
+ def placet_scope(model = nil)
106
+ declaration = placet_scope_declaration!
107
+ target = model || declaration[:model] || declaration[:model_name].constantize
108
+ Placet.scoped(placet_user, declaration[:actions].first, model: target)
109
+ end
110
+
111
+ # 表示制御用。enforcement にはカウントされない
112
+ def placet_permit?(actions, resource = nil) = Placet.permit?(placet_user, actions, resource)
113
+
114
+ # 二段構えの再チェック(docs/rails-usage.md Section 6)。scope で絞った結果
115
+ # (通常は 1 ページ分)へ個体判定を再適用し、check / scope の乖離があっても
116
+ # 見えてはいけないレコードを返さない。action 省略時は scope 宣言から引き継ぐ
117
+ def placet_recheck(records, action: nil)
118
+ action ||= placet_scope_declaration![:actions].first
119
+ Placet.recheck(placet_user, action, records)
120
+ end
121
+
122
+ private
123
+
124
+ def placet_scope_declaration!
125
+ (placet_declarations[action_name] || []).find { |d| d[:via] == :scope } ||
126
+ raise(Placet::Error, "#{self.class.name}##{action_name} に via: :scope の宣言がない")
127
+ end
128
+
129
+ def placet_enforce!
130
+ (placet_declarations[action_name] || []).each do |declaration|
131
+ next unless declaration[:via] == :check
132
+
133
+ target = declaration[:resource] && instance_exec(&declaration[:resource])
134
+ Placet.authorize!(placet_user, declaration[:actions], target)
135
+ end
136
+ end
137
+
138
+ def placet_verify_enforcement!
139
+ return if placet_public_actions.include?(action_name)
140
+ return if Placet.enforcements > (@_placet_enforcements_before || 0)
141
+
142
+ raise VerificationError,
143
+ "#{self.class.name}##{action_name} で認可が行われていない。" \
144
+ "placet_permit / placet_resource で宣言するか、placet_scope / Placet.authorize! を呼ぶか、" \
145
+ "認可不要なら placet_public で明示すること"
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Placet
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ config.placet = ActiveSupport::OrderedOptions.new
7
+
8
+ # config/placet/**/*.rb(1 ファイル 1 ポリシー + actions.rb 等)を起動時にロードする。
9
+ # 定義の不備は DefinitionError となり、起動が失敗する(fail fast)
10
+ initializer "placet.load_definitions", after: :load_config_initializers do |app|
11
+ paths = app.config.placet.definition_paths ||
12
+ [app.root.join("config", "placet", "**", "*.rb").to_s]
13
+ paths.flat_map { |pattern| Dir[pattern.to_s].sort }.each { |file| load file }
14
+ end
15
+
16
+ # 再チェック(Placet.recheck / placet_recheck)で乖離を検出したときの既定通知
17
+ initializer "placet.recheck_divergence_logging" do
18
+ Placet.on_recheck_divergence ||= lambda do |_user, action, record|
19
+ ::Rails.logger.warn(
20
+ "placet: scope と check の乖離を検出 action=#{action} " \
21
+ "record=#{record.class}##{record.respond_to?(:id) ? record.id : record.inspect}"
22
+ )
23
+ end
24
+ end
25
+
26
+ rake_tasks do
27
+ load File.expand_path("../../tasks/placet.rake", __dir__)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Placet
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "placet"
4
+ require_relative "rails/version"
5
+ require_relative "rails/controller"
6
+ require_relative "rails/active_record_scope"
7
+ require_relative "rails/railtie" if defined?(::Rails::Railtie)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "placet/rails"
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :placet do
4
+ desc "コンパイル済みの正規形(canonical form)を JSON で出力する"
5
+ task export: :environment do
6
+ require "json"
7
+ puts JSON.pretty_generate(Placet.export)
8
+ end
9
+
10
+ desc "principal の実効権限、または principal 集合 × action の決定根拠を表示する"
11
+ task :explain, %i[subject action] => :environment do |_t, args|
12
+ subject = args[:subject].to_s.strip
13
+ action = args[:action].to_s.strip
14
+ if subject.empty?
15
+ abort 'usage: rails "placet:explain[tenant:acme]" / ' \
16
+ 'rails "placet:explain[role:editor flag:suspended, post:update]"'
17
+ end
18
+
19
+ principals = subject.split(/\s+/)
20
+ if action.empty?
21
+ abort "principal の説明は 1 つずつ指定してください" if principals.size > 1
22
+ puts Placet::Explain.principal_report(principals.first)
23
+ else
24
+ puts Placet::Explain.decision_report(principals, action)
25
+ end
26
+ end
27
+
28
+ desc "コントローラの宣言から エンドポイント → 必要権限 の一覧を出力する"
29
+ task endpoints: :environment do
30
+ Rails.application.eager_load!
31
+ controllers = ActionController::Base.descendants
32
+ .select { |c| c.respond_to?(:placet_declarations) }
33
+ .sort_by(&:name)
34
+ controllers.each do |controller|
35
+ controller.placet_declarations.sort.each do |action_name, declarations|
36
+ declarations.each do |decl|
37
+ mode = decl[:via] == :scope ? "(scoped)" : "(403)"
38
+ puts format("%-40s %-30s %s", "#{controller.name}##{action_name}", decl[:actions].join(" AND "), mode)
39
+ end
40
+ end
41
+ controller.placet_public_actions.each do |action_name|
42
+ puts format("%-40s %-30s %s", "#{controller.name}##{action_name}", "-", "(public)")
43
+ end
44
+ end
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: placet-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yugo TERADA
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: placet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: actionpack
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '7.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '7.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: railties
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '7.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '7.1'
69
+ description: 'placet-rails integrates the placet authorization library with Rails:
70
+ per-action permission declarations (placet_permit / placet_resource), an enforcement
71
+ verification hook, and mapping of placet scope plans onto ActiveRecord relations
72
+ for list filtering.'
73
+ email:
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - LICENSE
79
+ - README.md
80
+ - lib/placet-rails.rb
81
+ - lib/placet/rails.rb
82
+ - lib/placet/rails/active_record_scope.rb
83
+ - lib/placet/rails/controller.rb
84
+ - lib/placet/rails/railtie.rb
85
+ - lib/placet/rails/version.rb
86
+ - lib/tasks/placet.rake
87
+ homepage: https://github.com/aspick/placet
88
+ licenses:
89
+ - MIT
90
+ metadata:
91
+ homepage_uri: https://github.com/aspick/placet
92
+ source_code_uri: https://github.com/aspick/placet
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '3.0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.4.18
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Rails adapter for placet — declarative per-action authorization and ActiveRecord
112
+ scope composition
113
+ test_files: []