verifica 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []