Module: Placet

Defined in:
lib/placet/dsl.rb,
lib/placet/engine.rb,
lib/placet/errors.rb,
lib/placet/explain.rb,
lib/placet/runtime.rb,
lib/placet/version.rb,
lib/placet/decision.rb,
lib/placet/document.rb,
lib/placet/test_helpers.rb,
lib/placet/action_registry.rb

Defined Under Namespace

Modules: ActionMatch, Explain, TestHelpers Classes: ActionRegistry, Decision, Definition, DefinitionBuilder, DefinitionError, Denied, Determinant, Engine, Error, PolicyBuilder, Relation, RelationBuilder, ScopePlan, Statement

Constant Summary collapse

VERSION =
"0.1.0"
ACTION_PATTERN_RE =
/\A(\*|(\*|[a-z][a-z0-9_]*):(\*|[a-z][a-z0-9_]*))\z/
CONCRETE_ACTION_RE =
/\A[a-z][a-z0-9_]*:[a-z][a-z0-9_]*\z/
PRINCIPAL_RE =
/\A[a-z][a-z0-9_-]*:[^\s*]+\z/
POLICY_NAME_RE =
/\A[a-z][a-z0-9_-]*\z/

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.on_recheck_divergenceObject

乖離検出時のハンドラ(user, action, record を受け取る)。既定は警告出力



136
137
138
# File 'lib/placet/runtime.rb', line 136

def on_recheck_divergence
  @on_recheck_divergence
end

Class Method Details

.authorize!(user, actions, resource = nil) ⇒ Object



64
65
66
67
68
69
70
# File 'lib/placet/runtime.rb', line 64

def authorize!(user, actions, resource = nil)
  count_enforcement!
  decide_each(user, actions, resource) do |action, decision|
    raise Denied.new(action, decision) unless decision.permit?
  end
  true
end

.decide(user, actions, resource = nil) ⇒ Object

複数 action は AND(fail-fast)。docs/rails-usage.md 4.2



55
56
57
58
59
60
61
62
# File 'lib/placet/runtime.rb', line 55

def decide(user, actions, resource = nil)
  last = nil
  decide_each(user, actions, resource) do |_action, decision|
    last = decision
    return decision unless decision.permit?
  end
  last
end

.decide_with_provenance(principals, action) ⇒ Object

由来つきの principal 集合(Hash)で判定し、determinants に由来(via)を付与する。 Engine は仕様どおり principal の配列のみを受けるため、via の装飾はこの層で行う



76
77
78
79
80
81
82
83
# File 'lib/placet/runtime.rb', line 76

def decide_with_provenance(principals, action)
  decision = engine.decide(principals.keys, action)
  decision.determinants.each do |determinant|
    via = principals[determinant.principal]
    determinant.via = via unless via.nil? || via.empty?
  end
  decision
end

.define(&block) ⇒ Object



11
12
13
14
15
# File 'lib/placet/runtime.rb', line 11

def define(&block)
  DefinitionBuilder.new(definition, registry).instance_eval(&block)
  definition.validate!(registry)
  @engine = nil
end

.definitionObject



7
# File 'lib/placet/runtime.rb', line 7

def definition = (@definition ||= Definition.new)

.derive(type_pattern, &block) ⇒ Object



19
20
21
22
# File 'lib/placet/runtime.rb', line 19

def derive(type_pattern, &block)
  type = type_pattern.delete_suffix(":*").delete_suffix(":")
  ((@derives ||= {})[type] ||= []) << block
end

.enforcementsObject

このスレッドで enforcement(authorize! / scoped)が行われた回数。 PEP ヘルパーの呼び忘れ検知(docs/rails-usage.md 4.4)が参照する



114
# File 'lib/placet/runtime.rb', line 114

def enforcements = Thread.current[:placet_enforcements] || 0

.engineObject



9
# File 'lib/placet/runtime.rb', line 9

def engine = (@engine ||= Engine.new(definition, registry: registry))

.expand_principals(base) ⇒ Object

与えられた principal 集合に derive の 1 段展開を適用する。 戻り値は { principal => 由来の連鎖 (Array) }



37
38
39
40
41
42
43
44
45
46
47
# File 'lib/placet/runtime.rb', line 37

def expand_principals(base)
  out = {}
  base.each { |p| out[p] = [] }
  base.each do |p|
    type, id = p.split(":", 2)
    derive_hooks(type).each do |hook|
      Array(hook.call(id)).each { |derived| out[derived] ||= [p] }
    end
  end
  out
end

.exportObject



138
# File 'lib/placet/runtime.rb', line 138

def export = definition.to_canonical

.permit?(user, actions, resource = nil) ⇒ Boolean

Returns:

  • (Boolean)


72
# File 'lib/placet/runtime.rb', line 72

def permit?(user, actions, resource = nil) = decide(user, actions, resource).permit?

.principals_for(user, resource = nil) ⇒ Object

principal 集合の導出。戻り値は { principal => 由来の連鎖 (Array) }



50
51
52
# File 'lib/placet/runtime.rb', line 50

def principals_for(user, resource = nil)
  with_relation_principals(expand_principals(Array(@resolver&.call(user))), user, resource)
end

.recheck(user, action, records) ⇒ Object

scope 合成の結果(通常は 1 ページ分)へ個体判定を再適用する二段構え。 check / scope に乖離バグがあっても「見えてはいけないものが見える」方向には 倒れない(concept.md 11.5)。乖離レコードは on_recheck_divergence に通知して除外する。 静的 principal の導出(resolver + derive)はレコードに依存しないため 1 回だけ行う



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/placet/runtime.rb', line 120

def recheck(user, action, records)
  base = expand_principals(Array(@resolver&.call(user)))
  records.select do |record|
    principals = with_relation_principals(base, user, record)
    next true if engine.decide(principals.keys, action).permit?

    if (handler = @on_recheck_divergence)
      handler.call(user, action, record)
    else
      warn "placet: scope と check の乖離を検出: action=#{action} record=#{record.inspect}"
    end
    false
  end
end

.register_scope_materializer(matcher, materializer) ⇒ Object

ORM アダプタが ScopePlan の実体化を登録する。matcher が true を返した model に materializer が適用される(後着優先。どれにもマッチしなければ インメモリの既定実装)。定義状態とは独立した登録なので reset! では消えない



102
103
104
# File 'lib/placet/runtime.rb', line 102

def register_scope_materializer(matcher, materializer)
  (@scope_materializers ||= []).unshift([matcher, materializer])
end

.registryObject



8
# File 'lib/placet/runtime.rb', line 8

def registry = (@registry ||= ActionRegistry.new)

.relation(name, resource:, &block) ⇒ Object



24
25
26
27
28
29
30
31
32
33
# File 'lib/placet/runtime.rb', line 24

def relation(name, resource:, &block)
  builder = RelationBuilder.new
  builder.instance_eval(&block)
  unless builder.check_block && builder.scope_block
    raise DefinitionError, "relation は check と scope をペアで宣言する: #{name}"
  end

  ((@relations ||= {})[resource] ||= []) <<
    Relation.new(name: name.to_s, check: builder.check_block, scope: builder.scope_block)
end

.relation_scopes(relations, names, user) ⇒ Object

relation 名の集合を scope 呼び出し結果へ写像する共通ヘルパー (インメモリ実装と ORM アダプタで共有)



108
109
110
# File 'lib/placet/runtime.rb', line 108

def relation_scopes(relations, names, user)
  relations.select { |r| names.include?(r.name) }.map { |r| r.scope.call(user) }
end

.relations_for(klass) ⇒ Object

モデルに登録された relation の一覧(テストヘルパー・アダプタが参照する)



152
# File 'lib/placet/runtime.rb', line 152

def relations_for(klass) = @relations ? @relations.fetch(klass, []) : []

.reset!Object

テスト・再読み込み用: 定義と登録を破棄する(materializer は定義状態ではないため対象外)



141
142
143
144
145
146
147
148
149
# File 'lib/placet/runtime.rb', line 141

def reset!
  @definition = nil
  @registry = nil
  @engine = nil
  @resolver = nil
  @derives = nil
  @relations = nil
  @on_recheck_divergence = nil
end

.resolver(&block) ⇒ Object



17
# File 'lib/placet/runtime.rb', line 17

def resolver(&block) = @resolver = block

.scoped(user, action, model:) ⇒ Object

一覧の scope 合成。Engine の plan を、登録された materializer(ORM アダプタ) またはインメモリの既定実装でコレクションへ写像する



92
93
94
95
96
97
# File 'lib/placet/runtime.rb', line 92

def scoped(user, action, model:)
  count_enforcement!
  rels = relations_for(model)
  plan = engine.scope_plan(principals_for(user).keys, action, relations: rels.map(&:name))
  materializer_for(model).call(plan, rels, user, model)
end

.validate_action!(action, error_class: Error) ⇒ Object

action 検証の公開入口(Engine に委譲)。アダプタの宣言時 fail-fast にも使う



86
87
88
# File 'lib/placet/runtime.rb', line 86

def validate_action!(action, error_class: Error)
  engine.validate_action!(action, error_class: error_class)
end