verifica 1.0.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.
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifica
4
+ # Base class for all Verifica exceptions
5
+ #
6
+ # @api public
7
+ class Error < StandardError
8
+ # @return [String] detailed description of the error if it's available or +message+ if not
9
+ #
10
+ # @api public
11
+ def explain
12
+ message
13
+ end
14
+ end
15
+
16
+ # Raised when {#action} on the given {#resource} isn't allowed for authorization {#subject} (e.g. current user)
17
+ #
18
+ # @api public
19
+ class AuthorizationError < Error
20
+ # @api private
21
+ attr_reader :result
22
+
23
+ # @api private
24
+ def initialize(result)
25
+ @result = result
26
+ super(result.message)
27
+ end
28
+
29
+ # (see AuthorizationResult#subject)
30
+ def subject
31
+ result.subject
32
+ end
33
+
34
+ # (see AuthorizationResult#subject_type)
35
+ def subject_type
36
+ result.subject_type
37
+ end
38
+
39
+ # (see AuthorizationResult#subject_id)
40
+ def subject_id
41
+ result.subject_id
42
+ end
43
+
44
+ # (see AuthorizationResult#subject_sids)
45
+ def subject_sids
46
+ result.subject_sids
47
+ end
48
+
49
+ # (see AuthorizationResult#resource)
50
+ def resource
51
+ result.resource
52
+ end
53
+
54
+ # (see AuthorizationResult#resource_type)
55
+ def resource_type
56
+ result.resource_type
57
+ end
58
+
59
+ # (see AuthorizationResult#resource_id)
60
+ def resource_id
61
+ result.resource_id
62
+ end
63
+
64
+ # (see AuthorizationResult#action)
65
+ def action
66
+ result.action
67
+ end
68
+
69
+ # (see AuthorizationResult#acl)
70
+ def acl
71
+ result.acl
72
+ end
73
+
74
+ # (see AuthorizationResult#context)
75
+ def context
76
+ result.context
77
+ end
78
+
79
+ # (see AuthorizationResult#explain)
80
+ def explain
81
+ result.explain
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Verifica
6
+ # Configuration object for resources registered in {Authorizer}
7
+ #
8
+ # @see Verifica.authorizer Usage examples
9
+ # @note Use {Configuration#register_resource} instead of this class directly
10
+ #
11
+ # @api public
12
+ class ResourceConfiguration
13
+ # @return [Symbol] type of the resource
14
+ #
15
+ # @api public
16
+ attr_reader :resource_type
17
+
18
+ # @return [Set<Symbol>] set of actions possible for this resource type
19
+ #
20
+ # @api public
21
+ attr_reader :possible_actions
22
+
23
+ # @return [#call] Access Control List provider for this resource type
24
+ #
25
+ # @api public
26
+ attr_reader :acl_provider
27
+
28
+ # @see Verifica.authorizer Usage examples
29
+ # @note Use {Configuration#register_resource} instead of this constructor directly
30
+ #
31
+ # @api public
32
+ def initialize(resource_type, possible_actions, acl_provider)
33
+ @resource_type = resource_type.to_sym
34
+ @possible_actions = action_set(possible_actions).freeze
35
+ if acl_provider.nil?
36
+ raise Error, "'#{@resource_type}' resource acl_provider should not be nil"
37
+ end
38
+ @acl_provider = acl_provider
39
+ freeze
40
+ end
41
+
42
+ private def action_set(possible_actions)
43
+ if possible_actions.empty?
44
+ raise Error, "Empty possible actions for '#{@resource_type}' resource. Probably a bug?"
45
+ end
46
+
47
+ action_set = possible_actions.map(&:to_sym).to_set
48
+ if action_set.size < possible_actions.size
49
+ duplicates = possible_actions.tally.select { |_, count| count > 1 }.keys
50
+ raise Error, "'#{duplicates}' possible actions for '#{@resource_type}' resource are specified several times. " \
51
+ "Probably code copy-paste and a bug?"
52
+ end
53
+
54
+ action_set
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifica
4
+ # Security Identifier (SID)
5
+ #
6
+ # Typically SID is an immutable string (you could use other objects too, string just makes it easier to understand)
7
+ # which describes certain fact about a security subject
8
+ # (current user, external service with given API key and scope of permissions, etc.).
9
+ # Each subject has a list of SIDs associated with it.
10
+ # For example, SIDs of a superuser may look like: +["root"]+,
11
+ # and SIDs of a regular user with ID +123+ may look like: +["authenticated", "user:123"]+.
12
+ #
13
+ # Essentially SIDs act as a link between the security subject and Access Control List for each resource in your system.
14
+ #
15
+ # @note This is an optional, convenience module. It adds methods that represent SIDs common for many web applications
16
+ # so you'll spend less time inventing your own convention. But you are free to use any other convention for SIDs.
17
+ #
18
+ # @example
19
+ # class User
20
+ # include Verifica::Sid
21
+ #
22
+ # def id
23
+ # # ...
24
+ # end
25
+ #
26
+ # def superuser?
27
+ # # ...
28
+ # end
29
+ #
30
+ # def org_id
31
+ # # ...
32
+ # end
33
+ #
34
+ # def subject_sids(**)
35
+ # if superuser?
36
+ # [root_sid]
37
+ # else
38
+ # [authenticated_sid, user_sid(id), organization_sid(org_id)]
39
+ # end
40
+ # end
41
+ # end
42
+ #
43
+ # @see Acl
44
+ module Sid
45
+ ANONYMOUS_SID = "anonymous"
46
+ AUTHENTICATED_SID = "authenticated"
47
+ ROOT_SID = "root"
48
+ private_constant :ANONYMOUS_SID, :AUTHENTICATED_SID, :ROOT_SID
49
+
50
+ # Security Identifier of the anonymous subject. Essentially this is a public SID.
51
+ # Use it when certain resources need to be available to anyone.
52
+ #
53
+ # @example
54
+ # class PostAclProvider
55
+ # include Verifica::Sid
56
+ #
57
+ # def call(post, **)
58
+ # Verifica::Acl.build do |acl|
59
+ # if post.public?
60
+ # acl.allow anonymous_sid, [:read]
61
+ # end
62
+ #
63
+ # # ...
64
+ # end
65
+ # end
66
+ # end
67
+ #
68
+ # @return [String]
69
+ #
70
+ # @api public
71
+ def anonymous_sid
72
+ ANONYMOUS_SID
73
+ end
74
+
75
+ # Security Identifier of any authenticated subject (current user, external service, etc.).
76
+ #
77
+ # @example
78
+ # class PostAclProvider
79
+ # include Verifica::Sid
80
+ #
81
+ # def call(post)
82
+ # Verifica::Acl.build do |acl|
83
+ # if post.public?
84
+ # acl.allow authenticated_sid, [:read, :comment]
85
+ # end
86
+ #
87
+ # # ...
88
+ # end
89
+ # end
90
+ # end
91
+ #
92
+ # @return [String]
93
+ #
94
+ # @api public
95
+ def authenticated_sid
96
+ AUTHENTICATED_SID
97
+ end
98
+
99
+ # Security Identifier of the superuser. The name is taken from Unix terminology as it provides a clear separation
100
+ # between true admins and semi-admins common in web applications (e.g. organization admin, chat room admin, etc.).
101
+ # Typically you allow all actions for this SID on all resources.
102
+ #
103
+ # @example
104
+ # class PostAclProvider
105
+ # include Verifica::Sid
106
+ #
107
+ # ALL_ACTIONS = [:read, :write, :delete, :comment]
108
+ # ROOT_ACL = Acl.build { |acl| acl.allow root_sid, ALL_ACTIONS }
109
+ #
110
+ # def call(post, **)
111
+ # ROOT_ACL.build do |acl|
112
+ # if post.public?
113
+ # acl.allow authenticated_sid, [:read, :comment]
114
+ # end
115
+ #
116
+ # # ...
117
+ # end
118
+ # end
119
+ # end
120
+ #
121
+ # @return [String]
122
+ #
123
+ # @api public
124
+ def root_sid
125
+ ROOT_SID
126
+ end
127
+
128
+ # Security Identifier of the regular user with given +user_id+.
129
+ #
130
+ # @note An argument can't be +nil+ for safety reasons.
131
+ # +nil+ can cause unpredictable consequences like two separate users sharing the same SID and access rights
132
+ #
133
+ # @example
134
+ # class PostAclProvider
135
+ # include Verifica::Sid
136
+ #
137
+ # def call(post, **)
138
+ # Verifica::Acl.build do |acl|
139
+ # acl.allow user_sid(post.author_id), [:read, :comment, :write, :delete]
140
+ #
141
+ # # ...
142
+ # end
143
+ # end
144
+ # end
145
+ #
146
+ # @return [String]
147
+ #
148
+ # @api public
149
+ def user_sid(user_id)
150
+ if user_id.nil?
151
+ raise ArgumentError, "Nil 'user_id' is unsafe. Use empty string if you absolutely need this behavior"
152
+ end
153
+
154
+ "user:#{user_id}".freeze
155
+ end
156
+
157
+ # Security Identifier of the subject with given +role_id+.
158
+ #
159
+ # @note (see #user_sid)
160
+ #
161
+ # @example
162
+ # class PostAclProvider
163
+ # include Verifica::Sid
164
+ #
165
+ # def call(post, **)
166
+ # Verifica::Acl.build do |acl|
167
+ # acl.allow role_sid("moderator"), [:read, :comment, :delete]
168
+ #
169
+ # # ...
170
+ # end
171
+ # end
172
+ # end
173
+ #
174
+ # @return [String]
175
+ #
176
+ # @api public
177
+ def role_sid(role_id)
178
+ if role_id.nil?
179
+ raise ArgumentError, "Nil 'role_id' is unsafe. Use empty string if you absolutely need this behavior"
180
+ end
181
+
182
+ "role:#{role_id}".freeze
183
+ end
184
+
185
+ # Security Identifier of the subject who is a member of the organization with given +organization_id+
186
+ #
187
+ # @note (see #user_sid)
188
+ #
189
+ # @example
190
+ # class PostAclProvider
191
+ # include Verifica::Sid
192
+ #
193
+ # def call(post, **)
194
+ # Verifica::Acl.build do |acl|
195
+ # if post.internal?
196
+ # acl.allow organization_sid(post.organization_id), [:read, :comment]
197
+ # end
198
+ #
199
+ # # ...
200
+ # end
201
+ # end
202
+ # end
203
+ #
204
+ # @return [String]
205
+ #
206
+ # @api public
207
+ def organization_sid(organization_id)
208
+ if organization_id.nil?
209
+ raise ArgumentError, "Nil 'organization_id' is unsafe. Use empty string if you absolutely need this behavior"
210
+ end
211
+
212
+ "org:#{organization_id}".freeze
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Verifica
4
+ VERSION = "1.0.0"
5
+ end
data/lib/verifica.rb ADDED
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "verifica/acl"
4
+ require_relative "verifica/authorization_result"
5
+ require_relative "verifica/authorizer"
6
+ require_relative "verifica/configuration"
7
+ require_relative "verifica/errors"
8
+ require_relative "verifica/sid"
9
+ require_relative "verifica/version"
10
+
11
+ # Verifica is Ruby's most scalable authorization solution ready to handle sophisticated authorization rules.
12
+ #
13
+ # - Framework and database agnostic
14
+ # - Scalable. Start from 10, grow to 10M records in the database while having the same authorization architecture
15
+ # - Supports any actor in your application. Traditional +current_user+, external service, API client, you name it
16
+ # - No global state. Only local, immutable objects
17
+ # - Plain old Ruby, zero dependencies, no magic
18
+ #
19
+ # Verifica is designed around Access Control List. ACL clearly separates authorization rules definition
20
+ # (who can do what for any given resource) and execution (can +current_user+ delete this post?).
21
+ #
22
+ # @example
23
+ # require 'verifica'
24
+ #
25
+ # User = Struct.new(:id, :role, keyword_init: true) do
26
+ # # Verifica expects each security subject to respond to #subject_id, #subject_type, and #subject_sids
27
+ # alias_method :subject_id, :id
28
+ # def subject_type = :user
29
+ #
30
+ # def subject_sids(**)
31
+ # role == "root" ? ["root"] : ["authenticated", "user:#{id}"]
32
+ # end
33
+ # end
34
+ #
35
+ # Video = Struct.new(:id, :author_id, :public, keyword_init: true) do
36
+ # # Verifica expects each secured resource to respond to #resource_id, and #resource_type
37
+ # alias_method :resource_id, :id
38
+ # def resource_type = :video
39
+ # end
40
+ #
41
+ # video_acl_provider = lambda do |video, **|
42
+ # Verifica::Acl.build do |acl|
43
+ # acl.allow "root", [:read, :write, :delete, :comment]
44
+ # acl.allow "user:#{video.author_id}", [:read, :write, :delete, :comment]
45
+ #
46
+ # if video.public
47
+ # acl.allow "authenticated", [:read, :comment]
48
+ # end
49
+ # end
50
+ # end
51
+ #
52
+ # authorizer = Verifica.authorizer do |config|
53
+ # config.register_resource :video, [:read, :write, :delete, :comment], video_acl_provider
54
+ # end
55
+ #
56
+ # public_video = Video.new(id: 1, author_id: 1000, public: true)
57
+ # private_video = Video.new(id: 2, author_id: 1000, public: true)
58
+ #
59
+ # superuser = User.new(id: 777, role: "root")
60
+ # video_author = User.new(id: 1000, role: "user")
61
+ # other_user = User.new(id: 2000, role: "user")
62
+ #
63
+ # authorizer.authorized?(superuser, private_video, :delete)
64
+ # # true
65
+ #
66
+ # authorizer.authorized?(video_author, private_video, :delete)
67
+ # # true
68
+ #
69
+ # authorizer.authorized?(other_user, private_video, :read)
70
+ # # false
71
+ #
72
+ # authorizer.authorized?(other_user, public_video, :comment)
73
+ # # true
74
+ #
75
+ # authorizer.authorize(other_user, public_video, :write)
76
+ # # raises Verifica::AuthorizationError: Authorization FAILURE. Subject 'user' id='2000'. Resource 'video' id='1'. Action 'write'
77
+ #
78
+ # @api public
79
+ module Verifica
80
+ EMPTY_ARRAY = [].freeze
81
+ private_constant :EMPTY_ARRAY
82
+
83
+ # Empty, frozen Access Control List. Semantically means that no actions are allowed
84
+ #
85
+ # @api public
86
+ EMPTY_ACL = Verifica::Acl.new(EMPTY_ARRAY).freeze
87
+
88
+ # Creates a new {Configuration} and yields it to the given block
89
+ #
90
+ # @example
91
+ # post_acl_provider = lambda do |post, **|
92
+ # Verifica::Acl.build do |acl|
93
+ # acl.allow "root", [:read, :write, :delete, :comment]
94
+ # acl.allow "user:#{post.author_id}", [:read, :write, :delete, :comment]
95
+ #
96
+ # if post.public
97
+ # acl.allow "authenticated", [:read, :comment]
98
+ # end
99
+ # end
100
+ # end
101
+ #
102
+ # user_acl_provider = lambda do |user, **|
103
+ # Verifica::Acl.build do |acl|
104
+ # acl.allow "root", [:read, :write, :delete]
105
+ # acl.allow "user:#{user.id}", [:read, :write]
106
+ # end
107
+ # end
108
+ #
109
+ # authorizer = Verifica.authorizer do |config|
110
+ # config.register_resource :post, [:read, :write, :delete, :comment], post_acl_provider
111
+ # config.register_resource :user, [:read, :write, :delete], user_acl_provider
112
+ # end
113
+ #
114
+ # @return [Authorizer] a new Authorizer configured by the given block
115
+ #
116
+ # @api public
117
+ def self.authorizer
118
+ config = Configuration.new
119
+ yield config
120
+ Authorizer.new(config.resources)
121
+ end
122
+ end
data/verifica.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/verifica/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "verifica"
7
+ spec.authors = ["Maxim Gurin"]
8
+ spec.email = ["mg@maximgurin.com"]
9
+ spec.version = Verifica::VERSION
10
+ spec.license = "MIT"
11
+
12
+ spec.summary = "The most scalable authorization solution for Ruby"
13
+ spec.homepage = "https://github.com/maximgurin/verifica"
14
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "verifica.gemspec", "lib/**/*"]
15
+ spec.bindir = "bin"
16
+ spec.executables = []
17
+ spec.require_paths = ["lib"]
18
+ spec.description = <<~DESCRIPTION
19
+ Verifica is Ruby's most scalable authorization solution, ready to handle sophisticated authorization rules.
20
+ Verifica is framework and database agnostic and designed around Access Control Lists.
21
+ ACL powers a straightforward and unified authorization flow for any user and resource,
22
+ regardless of how tricky the authorization rules are.
23
+
24
+ Verifica aims to solve the issue when authorization rules become too complex to be expressed in a single
25
+ SQL query. And at the same time the database is too big to execute these rules in the application code.
26
+ DESCRIPTION
27
+
28
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
29
+ spec.metadata["homepage_uri"] = spec.homepage
30
+ spec.metadata["changelog_uri"] = "https://github.com/maximgurin/verifica/blob/main/CHANGELOG.md"
31
+ spec.metadata["source_code_uri"] = "https://github.com/maximgurin/verifica"
32
+ spec.metadata["bug_tracker_uri"] = "https://github.com/maximgurin/verifica/issues"
33
+ spec.metadata["rubygems_mfa_required"] = "true"
34
+
35
+ spec.required_ruby_version = ">= 3.0.0"
36
+
37
+ spec.add_development_dependency "bundler"
38
+ spec.add_development_dependency "rake"
39
+ spec.add_development_dependency "rspec"
40
+ spec.add_development_dependency "yard"
41
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: verifica
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Maxim Gurin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-01-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: |
70
+ Verifica is Ruby's most scalable authorization solution, ready to handle sophisticated authorization rules.
71
+ Verifica is framework and database agnostic and designed around Access Control Lists.
72
+ ACL powers a straightforward and unified authorization flow for any user and resource,
73
+ regardless of how tricky the authorization rules are.
74
+
75
+ Verifica aims to solve the issue when authorization rules become too complex to be expressed in a single
76
+ SQL query. And at the same time the database is too big to execute these rules in the application code.
77
+ email:
78
+ - mg@maximgurin.com
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - CHANGELOG.md
84
+ - LICENSE
85
+ - README.md
86
+ - lib/verifica.rb
87
+ - lib/verifica/ace.rb
88
+ - lib/verifica/acl.rb
89
+ - lib/verifica/acl_builder.rb
90
+ - lib/verifica/authorization_result.rb
91
+ - lib/verifica/authorizer.rb
92
+ - lib/verifica/configuration.rb
93
+ - lib/verifica/errors.rb
94
+ - lib/verifica/resource_configuration.rb
95
+ - lib/verifica/sid.rb
96
+ - lib/verifica/version.rb
97
+ - verifica.gemspec
98
+ homepage: https://github.com/maximgurin/verifica
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ allowed_push_host: https://rubygems.org
103
+ homepage_uri: https://github.com/maximgurin/verifica
104
+ changelog_uri: https://github.com/maximgurin/verifica/blob/main/CHANGELOG.md
105
+ source_code_uri: https://github.com/maximgurin/verifica
106
+ bug_tracker_uri: https://github.com/maximgurin/verifica/issues
107
+ rubygems_mfa_required: 'true'
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 3.0.0
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.4.3
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: The most scalable authorization solution for Ruby
127
+ test_files: []