policier 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +5 -0
- data/.rubocop.yml +27 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +6 -0
- data/README.md +72 -0
- data/Rakefile +12 -0
- data/lib/policier/condition.rb +105 -0
- data/lib/policier/condition_union.rb +27 -0
- data/lib/policier/context.rb +70 -0
- data/lib/policier/policy.rb +9 -0
- data/lib/policier/runner.rb +41 -0
- data/lib/policier/scope_union.rb +39 -0
- data/lib/policier/version.rb +5 -0
- data/lib/policier.rb +15 -0
- data/sig/policier.rbs +4 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4f772e41751bc6c685d9f0eb2bfa2690979e58657cdc81b596f2be976d9610c9
|
4
|
+
data.tar.gz: 2eea37673f5297331a41eacc79ad3279de900a3e359fef5d4f0debcf81e4edbf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8e163c80ec8a5a9257b5ca86291a1891c43e1aac5f6ce49eea3aa7264d67eb3e1ab8c33a18a8dab8ce832628bd88241029b6c18bb8c1b4cc23f14663023e54c8
|
7
|
+
data.tar.gz: 747fbf5ae7fe59199397531fafea6a62641bb1307013ae798b72e7d512a2f08a20edd2c0d6162ac9e1c3a14f397d641d8dc3f60a9f74ba83e3158f4f1ff5e6b4
|
data/.editorconfig
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.0
|
3
|
+
|
4
|
+
Style/StringLiterals:
|
5
|
+
EnforcedStyle: double_quotes
|
6
|
+
|
7
|
+
Style/StringLiteralsInInterpolation:
|
8
|
+
EnforcedStyle: double_quotes
|
9
|
+
|
10
|
+
Layout/FirstArrayElementIndentation:
|
11
|
+
EnforcedStyle: consistent
|
12
|
+
|
13
|
+
Layout/FirstHashElementIndentation:
|
14
|
+
EnforcedStyle: consistent
|
15
|
+
|
16
|
+
Style/Documentation:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Metrics/MethodLength:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Metrics/AbcSize:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Metrics/BlockLength:
|
26
|
+
Exclude:
|
27
|
+
- test/**/*
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.0
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Policier
|
2
|
+
|
3
|
+
A gem to build ACL policies with style. Comparing to Pundit and Cancan, the gem tries to focus on DSL and structure to enforce writing ACLS in the way you can read them afterwards.
|
4
|
+
Policier consists of two big sections:
|
5
|
+
|
6
|
+
## Conditions
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
# Activates whe current user is super
|
10
|
+
class SuperuserCondition < Policier::Condition
|
11
|
+
self.data = Struct.new(:authorized_at)
|
12
|
+
|
13
|
+
# This is the main check, it's happenuing always when any policy
|
14
|
+
# is applied against the context (from controller or GraphQL)
|
15
|
+
#
|
16
|
+
# If it's veritied, condition is activated and causes extension
|
17
|
+
# of access rights (see below)
|
18
|
+
verify_with do
|
19
|
+
fail! if payload[:user].blank?
|
20
|
+
fail! unless payload[:user].is_superadmin
|
21
|
+
|
22
|
+
# This is not very useful since data is located next to payload
|
23
|
+
# so this is only here as an example
|
24
|
+
data[:authorized_at] = context[:user].authorized_at
|
25
|
+
end
|
26
|
+
|
27
|
+
# Additional check that can be quuickly used on top of verified
|
28
|
+
# condition to make conditions anagement more flexible.
|
29
|
+
# Think of it as of a Trait in FactoryBot
|
30
|
+
also_ensure(:it_wasnt_thursday) do |important_date|
|
31
|
+
fail! if important_date.wday == 3
|
32
|
+
end
|
33
|
+
|
34
|
+
# You can nhave as many as you want
|
35
|
+
also_ensure(:it_was_thursday) do |important_date|
|
36
|
+
fail! unless important_date.wday == 3
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
## Policies
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# Creates a dynamic scope over Person model that starts withb Person.none
|
45
|
+
# and extends when conditions activate
|
46
|
+
|
47
|
+
class PersonPolicy < Policier::Policy
|
48
|
+
scope(Person) do
|
49
|
+
# Collector argument allows you to propagate values you had during
|
50
|
+
# condition verification into actual policy
|
51
|
+
allow SuperuserCondition.and_it_wasnt_thursday(2.weeks.ago)
|
52
|
+
scope where(id: 5000)
|
53
|
+
scope where(id: 6000)
|
54
|
+
end
|
55
|
+
|
56
|
+
# This syntax allows you to combine several conditions and it runs
|
57
|
+
# if any of them activated for eahc of them
|
58
|
+
allow SuperuserCondition | AnotherCnndition
|
59
|
+
scope where('id < 1000')
|
60
|
+
end
|
61
|
+
|
62
|
+
# Thirsdays are the best
|
63
|
+
allow SuperuserCondition.and_it_was_thursday(2.weeks.ago)
|
64
|
+
scope all
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
As the outcome of this policy, if no conditions activate, `Person.count`
|
71
|
+
will be 0. And the every activated condition triggers `to` that get merged
|
72
|
+
and you get access to all of parts of relational scope.
|
data/Rakefile
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/inflector"
|
4
|
+
|
5
|
+
require_relative "condition_union"
|
6
|
+
|
7
|
+
module Policier
|
8
|
+
module ConditionResolve
|
9
|
+
def resolve
|
10
|
+
Context.current.ensure_condiiton(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def union
|
14
|
+
resolve.union
|
15
|
+
end
|
16
|
+
|
17
|
+
def |(other)
|
18
|
+
union | other
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Condition
|
23
|
+
class FailedException < StandardError; end
|
24
|
+
|
25
|
+
extend ConditionResolve
|
26
|
+
|
27
|
+
class << self
|
28
|
+
attr_accessor :data_class
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :data
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@context = Context.current
|
35
|
+
@data = @context.init_data(self.class, self.class.data_class) if self.class.data_class.present?
|
36
|
+
@failed = false
|
37
|
+
@executed = false
|
38
|
+
end
|
39
|
+
|
40
|
+
def depend_on!(condition_klass)
|
41
|
+
condition = condition_klass.resolve
|
42
|
+
return fail! if condition.failed?
|
43
|
+
|
44
|
+
condition.data
|
45
|
+
end
|
46
|
+
|
47
|
+
def fail!
|
48
|
+
raise FailedException
|
49
|
+
end
|
50
|
+
|
51
|
+
def payload
|
52
|
+
@context.payload
|
53
|
+
end
|
54
|
+
|
55
|
+
def failed?
|
56
|
+
@failed
|
57
|
+
end
|
58
|
+
|
59
|
+
def verify
|
60
|
+
return self if @executed
|
61
|
+
|
62
|
+
@failed ||= !instance_exec_with_failures(&self.class.verification_block)
|
63
|
+
@executed = true
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def override!(failing: false, data_replacement: {})
|
68
|
+
@failed = failing
|
69
|
+
@executed = true
|
70
|
+
data_replacement.each { |k, v| data[k] = v }
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def instance_exec_with_failures(*args, &block)
|
75
|
+
instance_exec(*args, &block)
|
76
|
+
true
|
77
|
+
rescue FailedException
|
78
|
+
false
|
79
|
+
end
|
80
|
+
|
81
|
+
def union
|
82
|
+
ConditionUnion.new(self)
|
83
|
+
end
|
84
|
+
|
85
|
+
class << self
|
86
|
+
attr_reader :verification_block
|
87
|
+
attr_accessor :collector
|
88
|
+
|
89
|
+
def verify_with(&block)
|
90
|
+
@verification_block = block
|
91
|
+
end
|
92
|
+
|
93
|
+
def also_ensure(name, &block)
|
94
|
+
define_method :"and_#{name}" do |data|
|
95
|
+
@failed ||= !instance_exec_with_failures(data, &block)
|
96
|
+
self
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def handle
|
101
|
+
Dry::Inflector.new.underscore(name).gsub("/", "_").to_sym
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Policier
|
4
|
+
class ConditionUnion
|
5
|
+
attr_reader :conditions, :context
|
6
|
+
|
7
|
+
def initialize(*conditions)
|
8
|
+
@context = Context.current
|
9
|
+
@conditions = []
|
10
|
+
conditions.each { |c| self | c }
|
11
|
+
end
|
12
|
+
|
13
|
+
def |(other)
|
14
|
+
other = other.resolve if other.is_a?(Class)
|
15
|
+
@conditions << other
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def union
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def payload
|
24
|
+
@context.payload
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/inflector"
|
4
|
+
|
5
|
+
require_relative "condition_union"
|
6
|
+
|
7
|
+
module Policier
|
8
|
+
class Context
|
9
|
+
class NotInScopeException < StandardError; end
|
10
|
+
class ScopeStartedEvaluation < StandardError; end
|
11
|
+
class DuplicatePayloadKey < StandardError; end
|
12
|
+
|
13
|
+
THREAD_CURRENT_KEY = :policier_context_current
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def current
|
17
|
+
raise NotInScopeException unless Thread.current[THREAD_CURRENT_KEY]
|
18
|
+
|
19
|
+
Thread.current[THREAD_CURRENT_KEY]
|
20
|
+
end
|
21
|
+
|
22
|
+
def scope(payload = {})
|
23
|
+
if Thread.current[THREAD_CURRENT_KEY].present? && Context.current.evaluation_started?
|
24
|
+
raise ScopeStartedEvaluation
|
25
|
+
end
|
26
|
+
|
27
|
+
if Thread.current[THREAD_CURRENT_KEY].blank?
|
28
|
+
Thread.current[THREAD_CURRENT_KEY] = new(payload)
|
29
|
+
else
|
30
|
+
raise ScopeStartedEvaluation if Context.current.evaluation_started? && payload.any?
|
31
|
+
|
32
|
+
Context.current.payload.merge!(payload)
|
33
|
+
end
|
34
|
+
|
35
|
+
yield
|
36
|
+
ensure
|
37
|
+
Thread.current[THREAD_CURRENT_KEY] = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :payload
|
42
|
+
|
43
|
+
def initialize(payload)
|
44
|
+
@payload = payload
|
45
|
+
@conditions = {}
|
46
|
+
@data = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def init_data(condition_class, data_class)
|
50
|
+
@data[condition_class] = data_class.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def mock_condition(*condtion_classes, failed: false, data_replacement: {})
|
54
|
+
condtion_classes.each do |condition_class|
|
55
|
+
@condition[condition_class] ||= condition_class.new.iverride!(
|
56
|
+
failed: failed,
|
57
|
+
data_replacement: data_replacement
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def evaluation_started?
|
63
|
+
@conditions.present?
|
64
|
+
end
|
65
|
+
|
66
|
+
def ensure_condiiton(condition_class)
|
67
|
+
@conditions[condition_class] ||= condition_class.new.verify
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/inflector"
|
4
|
+
|
5
|
+
require_relative "condition_union"
|
6
|
+
|
7
|
+
module Policier
|
8
|
+
class Runner
|
9
|
+
module DSL
|
10
|
+
def self.extended(_base)
|
11
|
+
attr_reader :model
|
12
|
+
end
|
13
|
+
|
14
|
+
def scope(model, &block)
|
15
|
+
@model = model
|
16
|
+
@scope = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
runner = Runner.new(self)
|
21
|
+
runner.instance_eval(&@scope)
|
22
|
+
runner.scope_union
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :scope_union
|
27
|
+
|
28
|
+
def initialize(policy)
|
29
|
+
@policy = policy
|
30
|
+
@scope_union = ScopeUnion.new(policy.model)
|
31
|
+
end
|
32
|
+
|
33
|
+
def allow(condition_union, &block)
|
34
|
+
condition_union.union.conditions.each do |condition|
|
35
|
+
next if condition.failed?
|
36
|
+
|
37
|
+
@scope_union.instance_exec(condition, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Policier
|
4
|
+
class ScopeUnion
|
5
|
+
attr_reader :relation
|
6
|
+
|
7
|
+
def initialize(model = nil)
|
8
|
+
@context = Context.current
|
9
|
+
@model = model
|
10
|
+
@relation = model.none if model.present?
|
11
|
+
@visible = false
|
12
|
+
@allowed_methods = Set.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def can?(method)
|
16
|
+
@allowed_methods.include?(method)
|
17
|
+
end
|
18
|
+
|
19
|
+
def visible?
|
20
|
+
@visible
|
21
|
+
end
|
22
|
+
|
23
|
+
def view
|
24
|
+
@visible = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def scope(update)
|
28
|
+
@relation = @relation.or(update)
|
29
|
+
end
|
30
|
+
|
31
|
+
def exec(method)
|
32
|
+
@allowed_methods.add(method)
|
33
|
+
end
|
34
|
+
|
35
|
+
def payload
|
36
|
+
@context.payload
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/policier.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# frozen_string_literal: tue
|
3
|
+
|
4
|
+
require_relative "policier/version"
|
5
|
+
require_relative "policier/context"
|
6
|
+
require_relative "policier/policy"
|
7
|
+
require_relative "policier/runner"
|
8
|
+
require_relative "policier/condition"
|
9
|
+
require_relative "policier/condition_union"
|
10
|
+
require_relative "policier/scope_union"
|
11
|
+
|
12
|
+
module Policier
|
13
|
+
class Error < StandardError; end
|
14
|
+
# Your code goes here...
|
15
|
+
end
|
data/sig/policier.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: policier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Boris Staal
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-inflector
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.0
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
- boris@staal.io
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".editorconfig"
|
35
|
+
- ".rubocop.yml"
|
36
|
+
- ".ruby-version"
|
37
|
+
- ".vscode/settings.json"
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- lib/policier.rb
|
41
|
+
- lib/policier/condition.rb
|
42
|
+
- lib/policier/condition_union.rb
|
43
|
+
- lib/policier/context.rb
|
44
|
+
- lib/policier/policy.rb
|
45
|
+
- lib/policier/runner.rb
|
46
|
+
- lib/policier/scope_union.rb
|
47
|
+
- lib/policier/version.rb
|
48
|
+
- sig/policier.rbs
|
49
|
+
homepage: https://github.com/inossidabile/policier
|
50
|
+
licenses: []
|
51
|
+
metadata:
|
52
|
+
homepage_uri: https://github.com/inossidabile/policier
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.0.0
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.5.3
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: The policier
|
72
|
+
test_files: []
|