oso-oso 0.20.0.pre.beta → 0.20.1.pre.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/Gemfile.lock +28 -27
- data/README.md +1 -3
- data/ext/oso-oso/lib/libpolar.dylib +0 -0
- data/ext/oso-oso/lib/libpolar.so +0 -0
- data/ext/oso-oso/lib/polar.dll +0 -0
- data/lib/oso/errors.rb +44 -0
- data/lib/oso/oso.rb +202 -3
- data/lib/oso/polar/data_filtering.rb +56 -35
- data/lib/oso/polar/errors.rb +2 -3
- data/lib/oso/polar/ffi/error.rb +2 -2
- data/lib/oso/polar/ffi/polar.rb +13 -21
- data/lib/oso/polar/ffi/query.rb +1 -2
- data/lib/oso/polar/host.rb +27 -11
- data/lib/oso/polar/polar.rb +116 -102
- data/lib/oso/polar/query.rb +23 -12
- data/lib/oso/version.rb +1 -1
- data/lib/oso.rb +4 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a6c6bd2d2fb38245a5103c0153f943f51060c90
|
4
|
+
data.tar.gz: 33dcb41c1c6bd0aeeec56d88391e1e80a66b6061
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c025329ad3a4eff57b2cf5e7d5ca8430ef825ee287111690f06439a7c6f623068048d27ea70358af8ffb8f9d935b926cdecf54baa848f95da9ebaa23b0c0e39
|
7
|
+
data.tar.gz: 2c4a4114eafb5887805a7b92015f466296ae9eaaa4ffe19f1cc60d4f2ec59b6b580ff80ebe60143e639d7f606d79f343c89e3fdd7e7a52789939af9716b9b2c3
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
oso-oso (0.20.
|
4
|
+
oso-oso (0.20.1.pre.beta)
|
5
5
|
ffi (~> 1.0)
|
6
6
|
|
7
7
|
GEM
|
@@ -19,15 +19,15 @@ GEM
|
|
19
19
|
minitest (~> 5.1)
|
20
20
|
tzinfo (~> 1.1)
|
21
21
|
arel (9.0.0)
|
22
|
-
ast (2.4.
|
23
|
-
backport (1.
|
24
|
-
benchmark (0.1.
|
22
|
+
ast (2.4.2)
|
23
|
+
backport (1.2.0)
|
24
|
+
benchmark (0.1.1)
|
25
25
|
byebug (11.1.3)
|
26
26
|
coderay (1.1.3)
|
27
27
|
concurrent-ruby (1.1.9)
|
28
28
|
diff-lcs (1.4.4)
|
29
29
|
e2mmap (0.1.0)
|
30
|
-
ffi (1.15.
|
30
|
+
ffi (1.15.4)
|
31
31
|
i18n (1.8.10)
|
32
32
|
concurrent-ruby (~> 1.0)
|
33
33
|
jaro_winkler (1.5.4)
|
@@ -37,8 +37,8 @@ GEM
|
|
37
37
|
minitest (5.14.4)
|
38
38
|
nokogiri (1.10.10)
|
39
39
|
mini_portile2 (~> 2.4.0)
|
40
|
-
parallel (1.
|
41
|
-
parser (2.7.
|
40
|
+
parallel (1.20.1)
|
41
|
+
parser (2.7.2.0)
|
42
42
|
ast (~> 2.4.1)
|
43
43
|
pry (0.13.1)
|
44
44
|
coderay (~> 1.1)
|
@@ -48,23 +48,23 @@ GEM
|
|
48
48
|
pry (~> 0.13.0)
|
49
49
|
rainbow (3.0.0)
|
50
50
|
rake (12.3.3)
|
51
|
-
regexp_parser (1.
|
51
|
+
regexp_parser (2.1.1)
|
52
52
|
reverse_markdown (2.0.0)
|
53
53
|
nokogiri
|
54
|
-
rexml (3.2.
|
55
|
-
rspec (3.
|
56
|
-
rspec-core (~> 3.
|
57
|
-
rspec-expectations (~> 3.
|
58
|
-
rspec-mocks (~> 3.
|
59
|
-
rspec-core (3.
|
60
|
-
rspec-support (~> 3.
|
61
|
-
rspec-expectations (3.
|
54
|
+
rexml (3.2.5)
|
55
|
+
rspec (3.10.0)
|
56
|
+
rspec-core (~> 3.10.0)
|
57
|
+
rspec-expectations (~> 3.10.0)
|
58
|
+
rspec-mocks (~> 3.10.0)
|
59
|
+
rspec-core (3.10.1)
|
60
|
+
rspec-support (~> 3.10.0)
|
61
|
+
rspec-expectations (3.10.1)
|
62
62
|
diff-lcs (>= 1.2.0, < 2.0)
|
63
|
-
rspec-support (~> 3.
|
64
|
-
rspec-mocks (3.
|
63
|
+
rspec-support (~> 3.10.0)
|
64
|
+
rspec-mocks (3.10.2)
|
65
65
|
diff-lcs (>= 1.2.0, < 2.0)
|
66
|
-
rspec-support (~> 3.
|
67
|
-
rspec-support (3.
|
66
|
+
rspec-support (~> 3.10.0)
|
67
|
+
rspec-support (3.10.2)
|
68
68
|
rubocop (0.89.1)
|
69
69
|
parallel (~> 1.10)
|
70
70
|
parser (>= 2.7.1.1)
|
@@ -74,10 +74,10 @@ GEM
|
|
74
74
|
rubocop-ast (>= 0.3.0, < 1.0)
|
75
75
|
ruby-progressbar (~> 1.7)
|
76
76
|
unicode-display_width (>= 1.4.0, < 2.0)
|
77
|
-
rubocop-ast (0.
|
78
|
-
parser (>= 2.7.1.
|
79
|
-
ruby-progressbar (1.
|
80
|
-
solargraph (0.39.
|
77
|
+
rubocop-ast (0.8.0)
|
78
|
+
parser (>= 2.7.1.5)
|
79
|
+
ruby-progressbar (1.11.0)
|
80
|
+
solargraph (0.39.17)
|
81
81
|
backport (~> 1.1)
|
82
82
|
benchmark
|
83
83
|
bundler (>= 1.17.2)
|
@@ -92,16 +92,17 @@ GEM
|
|
92
92
|
tilt (~> 2.0)
|
93
93
|
yard (~> 0.9, >= 0.9.24)
|
94
94
|
sqlite3 (1.4.2)
|
95
|
-
thor (1.0
|
95
|
+
thor (1.1.0)
|
96
96
|
thread_safe (0.3.6)
|
97
97
|
tilt (2.0.10)
|
98
98
|
tzinfo (1.2.9)
|
99
99
|
thread_safe (~> 0.1)
|
100
100
|
unicode-display_width (1.7.0)
|
101
|
-
yard (0.9.
|
101
|
+
yard (0.9.26)
|
102
102
|
|
103
103
|
PLATFORMS
|
104
104
|
ruby
|
105
|
+
x86_64-darwin-20
|
105
106
|
|
106
107
|
DEPENDENCIES
|
107
108
|
activerecord
|
@@ -115,4 +116,4 @@ DEPENDENCIES
|
|
115
116
|
yard (~> 0.9.25)
|
116
117
|
|
117
118
|
BUNDLED WITH
|
118
|
-
2.2.
|
119
|
+
2.2.15
|
data/README.md
CHANGED
Binary file
|
data/ext/oso-oso/lib/libpolar.so
CHANGED
Binary file
|
data/ext/oso-oso/lib/polar.dll
CHANGED
Binary file
|
data/lib/oso/errors.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Oso
|
4
|
+
class Error < ::RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
class AuthorizationError < Error
|
8
|
+
end
|
9
|
+
|
10
|
+
# Thrown by the +authorize+, +authorize_field+, and +authorize_request+
|
11
|
+
# methods when the action is not allowed.
|
12
|
+
#
|
13
|
+
# Most of the time, your app should handle this error by returning a 403 HTTP
|
14
|
+
# error to the client.
|
15
|
+
class ForbiddenError < AuthorizationError
|
16
|
+
def initialize
|
17
|
+
super(
|
18
|
+
'Oso ForbiddenError -- The requested action was not allowed for the ' \
|
19
|
+
'given resource. You should handle this error by returning a 403 error ' \
|
20
|
+
'to the client.'
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Thrown by the +authorize+ method of an +Oso+ instance. This error indicates
|
26
|
+
# that the actor is not only not allowed to perform the given action, but also
|
27
|
+
# is not allowed to +"read"+ the given resource.
|
28
|
+
#
|
29
|
+
# Most of the time, your app should handle this error by returning a 404 HTTP
|
30
|
+
# error to the client.
|
31
|
+
#
|
32
|
+
# To control which action is used for the distinction between
|
33
|
+
# +NotFoundError+ and +ForbiddenError+, you can customize the
|
34
|
+
# +read_action+ on your +Oso+ instance.
|
35
|
+
class NotFoundError < AuthorizationError
|
36
|
+
def initialize
|
37
|
+
super(
|
38
|
+
'Oso NotFoundError -- The current user does not have permission to read ' \
|
39
|
+
'the given resource. You should handle this error by returning a 404 ' \
|
40
|
+
'error to the client.'
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/oso/oso.rb
CHANGED
@@ -1,12 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'set'
|
3
4
|
require_relative 'polar/polar'
|
4
5
|
|
5
6
|
module Oso
|
6
7
|
# oso authorization API.
|
7
8
|
class Oso < Polar::Polar
|
8
|
-
|
9
|
-
|
9
|
+
# Create an Oso instance, which is used to configure and enforce an Oso
|
10
|
+
# policy in an app.
|
11
|
+
#
|
12
|
+
# @param forbidden_error [Class] Optionally override the "forbidden" error
|
13
|
+
# class thrown by the `authorize*` methods. Defaults to
|
14
|
+
# {Oso::ForbiddenError}.
|
15
|
+
# @param not_found_error [Class] Optionally override the "not found" error
|
16
|
+
# class thrown by {#authorize}. Defaults to {Oso::NotFoundError}.
|
17
|
+
# @param read_action The action used by the {#authorize} method to
|
18
|
+
# determine whether an authorization failure should
|
19
|
+
# raise a {Oso::NotFoundError} or a {Oso::ForbiddenError}
|
20
|
+
def initialize(not_found_error: NotFoundError, forbidden_error: ForbiddenError, read_action: 'read')
|
21
|
+
super()
|
22
|
+
@not_found_error = not_found_error
|
23
|
+
@forbidden_error = forbidden_error
|
24
|
+
@read_action = read_action
|
10
25
|
end
|
11
26
|
|
12
27
|
# Query the knowledge base to determine whether an actor is allowed to
|
@@ -17,7 +32,191 @@ module Oso
|
|
17
32
|
# @param resource [Object] Object.
|
18
33
|
# @return [Boolean] An access control decision.
|
19
34
|
def allowed?(actor:, action:, resource:)
|
20
|
-
|
35
|
+
query_rule_once('allow', actor, action, resource)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Ensure that +actor+ is allowed to perform +action+ on
|
39
|
+
# +resource+.
|
40
|
+
#
|
41
|
+
# If the action is permitted with an +allow+ rule in the policy, then
|
42
|
+
# this method returns +None+. If the action is not permitted by the
|
43
|
+
# policy, this method will raise an error.
|
44
|
+
#
|
45
|
+
# The error raised by this method depends on whether the actor can perform
|
46
|
+
# the +"read"+ action on the resource. If they cannot read the resource,
|
47
|
+
# then a {Oso::NotFoundError} error is raised. Otherwise, a
|
48
|
+
# {Oso::ForbiddenError} is raised.
|
49
|
+
#
|
50
|
+
# @param actor The actor performing the request.
|
51
|
+
# @param action The action the actor is attempting to perform.
|
52
|
+
# @param resource The resource being accessed.
|
53
|
+
# @param check_read [Boolean] If set to +false+, a {Oso::ForbiddenError} is
|
54
|
+
# always thrown on authorization failures, regardless of whether the actor
|
55
|
+
# can read the resource. Default is +true+.
|
56
|
+
#
|
57
|
+
# @raise [Oso::ForbiddenError] Raised if the actor does not have permission
|
58
|
+
# to perform this action on this resource, but _does_ have +"read"+
|
59
|
+
# permission on the resource.
|
60
|
+
# @raise [Oso::NotFoundError] Raised if the actor does not have permission
|
61
|
+
# to perform this action on this resource and additionally does not have
|
62
|
+
# permission to +"read"+ the resource.
|
63
|
+
def authorize(actor, action, resource, check_read: true)
|
64
|
+
return if query_rule_once('allow', actor, action, resource)
|
65
|
+
|
66
|
+
if check_read && (action == @read_action || !query_rule_once('allow', actor, @read_action, resource))
|
67
|
+
raise @not_found_error
|
68
|
+
end
|
69
|
+
|
70
|
+
raise @forbidden_error
|
71
|
+
end
|
72
|
+
|
73
|
+
# Ensure that +actor+ is allowed to send +request+ to the server.
|
74
|
+
#
|
75
|
+
# Checks the +allow_request+ rule of a policy.
|
76
|
+
#
|
77
|
+
# If the request is permitted with an +allow_request+ rule in the
|
78
|
+
# policy, then this method returns nothing. Otherwise, this method raises
|
79
|
+
# a {Oso::ForbiddenError}.
|
80
|
+
#
|
81
|
+
# @param actor The actor performing the request.
|
82
|
+
# @param request An object representing the request that was sent by the
|
83
|
+
# actor.
|
84
|
+
#
|
85
|
+
# @raise [Oso::ForbiddenError] Raised if the actor does not have permission
|
86
|
+
# to send the request.
|
87
|
+
def authorize_request(actor, request)
|
88
|
+
raise @forbidden_error unless query_rule_once('allow_request', actor, request)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Ensure that +actor+ is allowed to perform +action+ on a given
|
92
|
+
# +resource+'s +field+.
|
93
|
+
#
|
94
|
+
# If the action is permitted by an +allow_field+ rule in the policy,
|
95
|
+
# then this method returns nothing. If the action is not permitted by the
|
96
|
+
# policy, this method will raise a {Oso::ForbiddenError}.
|
97
|
+
#
|
98
|
+
# @param actor The actor performing the request.
|
99
|
+
# @param action The action the actor is attempting to perform on the
|
100
|
+
# field.
|
101
|
+
# @param resource The resource being accessed.
|
102
|
+
# @param field The name of the field being accessed.
|
103
|
+
#
|
104
|
+
# @raise [Oso::ForbiddenError] Raised if the actor does not have permission
|
105
|
+
# to access this field.
|
106
|
+
def authorize_field(actor, action, resource, field)
|
107
|
+
raise @forbidden_error unless query_rule_once('allow_field', actor, action, resource, field)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Determine the actions +actor+ is allowed to take on +resource+.
|
111
|
+
#
|
112
|
+
# Collects all actions allowed by allow rules in the Polar policy for the
|
113
|
+
# given combination of actor and resource.
|
114
|
+
#
|
115
|
+
# @param actor The actor for whom to collect allowed actions
|
116
|
+
# @param resource The resource being accessed
|
117
|
+
# @param allow_wildcard Flag to determine behavior if the policy
|
118
|
+
# includes a wildcard action. E.g., a rule allowing any action:
|
119
|
+
# +allow(_actor, _action, _resource)+. If +true+, the method will
|
120
|
+
# return +Set["*"]+, if +false+, the method will raise an exception.
|
121
|
+
# @return A set of the unique allowed actions.
|
122
|
+
def authorized_actions(actor, resource, allow_wildcard: false) # rubocop:disable Metrics/MethodLength
|
123
|
+
results = query_rule('allow', actor, Polar::Variable.new('action'), resource)
|
124
|
+
actions = Set.new
|
125
|
+
results.each do |result|
|
126
|
+
action = result['action']
|
127
|
+
if action.is_a?(Polar::Variable)
|
128
|
+
return Set['*'] if allow_wildcard
|
129
|
+
|
130
|
+
raise ::Oso::Error,
|
131
|
+
'The result of authorized_actions() contained an '\
|
132
|
+
'"unconstrained" action that could represent any '\
|
133
|
+
'action, but allow_wildcard was set to False. To fix, '\
|
134
|
+
'set allow_wildcard to True and compare with the "*" '\
|
135
|
+
'string.'
|
136
|
+
end
|
137
|
+
actions.add(action)
|
138
|
+
end
|
139
|
+
actions
|
140
|
+
end
|
141
|
+
|
142
|
+
# Determine the fields of +resource+ on which +actor+ is allowed to
|
143
|
+
# perform +action+.
|
144
|
+
#
|
145
|
+
# Uses +allow_field+ rules in the policy to find all allowed fields.
|
146
|
+
#
|
147
|
+
# @param actor The actor for whom to collect allowed fields.
|
148
|
+
# @param action The action being taken on the field.
|
149
|
+
# @param resource The resource being accessed.
|
150
|
+
# @param allow_wildcard Flag to determine behavior if the policy \
|
151
|
+
# includes a wildcard field. E.g., a rule allowing any field: \
|
152
|
+
# +allow_field(_actor, _action, _resource, _field)+. If +true+, the \
|
153
|
+
# method will return +Set["*"]+, if +false+, the method will raise an \
|
154
|
+
# exception.
|
155
|
+
# @return A set of the unique allowed fields.
|
156
|
+
def authorized_fields(actor, action, resource, allow_wildcard: false) # rubocop:disable Metrics/MethodLength
|
157
|
+
results = query_rule('allow_field', actor, action, resource, Polar::Variable.new('field'))
|
158
|
+
fields = Set.new
|
159
|
+
results.each do |result|
|
160
|
+
field = result['field']
|
161
|
+
if field.is_a?(Polar::Variable)
|
162
|
+
return Set['*'] if allow_wildcard
|
163
|
+
|
164
|
+
raise ::Oso::Error,
|
165
|
+
'The result of authorized_fields() contained an '\
|
166
|
+
'"unconstrained" field that could represent any '\
|
167
|
+
'field, but allow_wildcard was set to False. To fix, '\
|
168
|
+
'set allow_wildcard to True and compare with the "*" '\
|
169
|
+
'string.'
|
170
|
+
end
|
171
|
+
fields.add(field)
|
172
|
+
end
|
173
|
+
fields
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns a query for resources of type +cls+ that +actor+ is allowed
|
177
|
+
# to perform +action+ on.
|
178
|
+
#
|
179
|
+
# @param actor The actor whose permissions to check.
|
180
|
+
# @param action The action being taken on the resource.
|
181
|
+
# @param resource_cls The resource being accessed.
|
182
|
+
#
|
183
|
+
# @returns A query for resources accessible to the actor.
|
184
|
+
def authorized_query(actor, action, resource_cls) # rubocop:disable Metrics/MethodLength
|
185
|
+
resource = Polar::Variable.new 'resource'
|
186
|
+
|
187
|
+
results = query_rule(
|
188
|
+
'allow',
|
189
|
+
actor,
|
190
|
+
action,
|
191
|
+
resource,
|
192
|
+
bindings: { 'resource' => type_constraint(resource, resource_cls) },
|
193
|
+
accept_expression: true
|
194
|
+
)
|
195
|
+
|
196
|
+
results = results.each_with_object([]) do |result, out|
|
197
|
+
result.each do |key, val|
|
198
|
+
out.push({ 'bindings' => { key => host.to_polar(val) } })
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
::Oso::Polar::DataFiltering::FilterPlan
|
203
|
+
.parse(self, results, get_class_name(resource_cls))
|
204
|
+
.build_query
|
205
|
+
end
|
206
|
+
|
207
|
+
# Returns the resources of type +resource_cls+ that +actor+ is allowed
|
208
|
+
# to perform +action+ on.
|
209
|
+
#
|
210
|
+
# @param actor The actor whose permissions to check.
|
211
|
+
# @param action The action being taken on the resource.
|
212
|
+
# @param resource_cls The resource being accessed.
|
213
|
+
#
|
214
|
+
# @returns A list of resources accessible to the actor.
|
215
|
+
def authorized_resources(actor, action, resource_cls)
|
216
|
+
q = authorized_query actor, action, resource_cls
|
217
|
+
return [] if q.nil?
|
218
|
+
|
219
|
+
host.types[get_class_name resource_cls].exec_query[q]
|
21
220
|
end
|
22
221
|
end
|
23
222
|
end
|
@@ -7,62 +7,84 @@ module Oso
|
|
7
7
|
# Represents a set of filter sequences that should allow the host
|
8
8
|
# to obtain the records satisfying a query.
|
9
9
|
class FilterPlan
|
10
|
-
include Enumerable
|
11
10
|
attr_reader :result_sets
|
12
11
|
|
13
|
-
def
|
12
|
+
def self.parse(polar, partials, class_name)
|
14
13
|
types = polar.host.serialize_types
|
15
14
|
parsed_json = polar.ffi.build_filter_plan(types, partials, 'resource', class_name)
|
16
|
-
|
17
|
-
|
18
|
-
ResultSet.new polar, rset
|
15
|
+
result_sets = parsed_json['result_sets'].map do |rset|
|
16
|
+
ResultSet.parse polar, rset
|
19
17
|
end
|
18
|
+
|
19
|
+
new polar: polar, result_sets: result_sets
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
|
22
|
+
def initialize(polar:, result_sets:)
|
23
|
+
@polar = polar
|
24
|
+
@result_sets = result_sets
|
24
25
|
end
|
25
26
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
req = requests[i]
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
27
|
+
def build_query # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
28
|
+
combine = nil
|
29
|
+
result_sets.each_with_object([]) do |rs, qb|
|
30
|
+
rs.resolve_order.each_with_object({}) do |i, set_results|
|
31
|
+
req = rs.requests[i]
|
32
|
+
cs = req.constraints.each { |c| c.ground set_results }
|
33
|
+
typ = @polar.host.types[req.class_tag]
|
34
|
+
q = typ.build_query[cs]
|
35
|
+
if i != rs.result_id
|
36
|
+
set_results[i] = typ.exec_query[q]
|
37
|
+
else
|
38
|
+
combine = typ.combine_query
|
39
|
+
qb.push q
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end.reduce(&combine)
|
36
43
|
end
|
37
44
|
|
38
45
|
# Represents a sequence of filters for one set of results
|
39
46
|
class ResultSet
|
40
47
|
attr_reader :requests, :resolve_order, :result_id
|
41
48
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
reqs[req[0].to_i] = Request.
|
49
|
+
def self.parse(polar, parsed_json)
|
50
|
+
resolve_order = parsed_json['resolve_order']
|
51
|
+
result_id = parsed_json['result_id']
|
52
|
+
requests = parsed_json['requests'].each_with_object({}) do |req, reqs|
|
53
|
+
reqs[req[0].to_i] = Request.parse(polar, req[1])
|
47
54
|
end
|
55
|
+
|
56
|
+
new resolve_order: resolve_order, result_id: result_id, requests: requests
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(requests:, resolve_order:, result_id:)
|
60
|
+
@resolve_order = resolve_order
|
61
|
+
@requests = requests
|
62
|
+
@result_id = result_id
|
48
63
|
end
|
64
|
+
end
|
49
65
|
|
50
|
-
|
51
|
-
|
52
|
-
|
66
|
+
# Represents a filter for a result set
|
67
|
+
class Request
|
68
|
+
attr_reader :constraints, :class_tag
|
53
69
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
58
|
-
@class_tag = parsed_json['class_tag']
|
70
|
+
def self.parse(polar, parsed_json)
|
71
|
+
constraints = parsed_json['constraints'].map do |con|
|
72
|
+
Filter.parse polar, con
|
59
73
|
end
|
74
|
+
class_tag = parsed_json['class_tag']
|
75
|
+
|
76
|
+
new(constraints: constraints, class_tag: class_tag)
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(constraints:, class_tag:)
|
80
|
+
@constraints = constraints
|
81
|
+
@class_tag = class_tag
|
60
82
|
end
|
61
83
|
end
|
62
84
|
end
|
63
85
|
|
64
|
-
# Represents relationships between resources, eg.
|
65
|
-
class
|
86
|
+
# Represents relationships between resources, eg. one-one or one-many
|
87
|
+
class Relation
|
66
88
|
attr_reader :kind, :other_type, :my_field, :other_field
|
67
89
|
|
68
90
|
def initialize(kind:, other_type:, my_field:, other_field:)
|
@@ -93,12 +115,13 @@ module Oso
|
|
93
115
|
end
|
94
116
|
|
95
117
|
# Represents a condition that must hold on a resource.
|
96
|
-
class
|
118
|
+
class Filter
|
97
119
|
attr_reader :kind, :field, :value
|
98
120
|
|
99
121
|
CHECKS = {
|
100
122
|
'Eq' => ->(a, b) { a == b },
|
101
123
|
'In' => ->(a, b) { b.include? a },
|
124
|
+
'Neq' => ->(a, b) { a != b },
|
102
125
|
'Contains' => ->(a, b) { a.include? b }
|
103
126
|
}.freeze
|
104
127
|
|
@@ -126,8 +149,6 @@ module Oso
|
|
126
149
|
|
127
150
|
def self.parse(polar, constraint) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
128
151
|
kind = constraint['kind']
|
129
|
-
raise unless %w[Eq In Contains].include? kind
|
130
|
-
|
131
152
|
field = constraint['field']
|
132
153
|
value = constraint['value']
|
133
154
|
|
data/lib/oso/polar/errors.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
module Oso
|
4
4
|
module Polar
|
5
5
|
# Base error type for Oso::Polar.
|
6
|
-
class Error < ::
|
6
|
+
class Error < ::Oso::Error
|
7
7
|
attr_reader :stack_trace
|
8
8
|
|
9
9
|
# @param message [String]
|
@@ -68,7 +68,7 @@ module Oso
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
class DuplicateClassAliasError < PolarRuntimeError # rubocop:disable Style/Documentation
|
71
|
-
# @param
|
71
|
+
# @param name [String]
|
72
72
|
# @param old [Class]
|
73
73
|
# @param new [Class]
|
74
74
|
def initialize(name:, old:, new:)
|
@@ -101,7 +101,6 @@ module Oso
|
|
101
101
|
class ParameterError < ApiError; end
|
102
102
|
|
103
103
|
class ValidationError < Error; end
|
104
|
-
class RolesValidationError < Error; end
|
105
104
|
|
106
105
|
UNEXPECTED_EXPRESSION_MESSAGE = <<~MSG
|
107
106
|
Received Expression from Polar VM. The Expression type is not yet supported in this language.
|
data/lib/oso/polar/ffi/error.rb
CHANGED
@@ -56,7 +56,7 @@ module Oso
|
|
56
56
|
operational_error(subkind, msg: msg, details: details)
|
57
57
|
when 'Parameter'
|
58
58
|
api_error(subkind, msg: msg, details: details)
|
59
|
-
when '
|
59
|
+
when 'Validation'
|
60
60
|
validation_error(msg, details: details)
|
61
61
|
end
|
62
62
|
end
|
@@ -146,7 +146,7 @@ module Oso
|
|
146
146
|
# @return [::Oso::Polar::ValidationError] the object converted into the expected format.
|
147
147
|
private_class_method def self.validation_error(msg, details:)
|
148
148
|
# This is currently the only type of validation error.
|
149
|
-
::Oso::Polar::
|
149
|
+
::Oso::Polar::ValidationError.new(msg, details: details)
|
150
150
|
end
|
151
151
|
end
|
152
152
|
end
|
data/lib/oso/polar/ffi/polar.rb
CHANGED
@@ -14,15 +14,14 @@ module Oso
|
|
14
14
|
ffi_lib FFI::LIB_PATH
|
15
15
|
|
16
16
|
attach_function :new, :polar_new, [], FFI::Polar
|
17
|
-
attach_function :
|
18
|
-
attach_function :validate_roles_config, :polar_validate_roles_config, [FFI::Polar, :string], :int32
|
19
|
-
attach_function :load, :polar_load, [FFI::Polar, :string, :string], :int32
|
17
|
+
attach_function :load, :polar_load, [FFI::Polar, :string], :int32
|
20
18
|
attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], :int32
|
21
19
|
attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
|
22
20
|
attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
|
23
21
|
attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], FFI::Query
|
24
22
|
attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], FFI::Query
|
25
23
|
attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
|
24
|
+
attach_function :register_mro, :polar_register_mro, [FFI::Polar, :string, :string], :int32
|
26
25
|
attach_function :next_message, :polar_next_polar_message, [FFI::Polar], FFI::Message
|
27
26
|
attach_function :free, :polar_free, [FFI::Polar], :int32
|
28
27
|
attach_function(
|
@@ -43,20 +42,6 @@ module Oso
|
|
43
42
|
polar
|
44
43
|
end
|
45
44
|
|
46
|
-
# @raise [FFI::Error] if the FFI call returns an error.
|
47
|
-
def enable_roles
|
48
|
-
result = Rust.enable_roles(self)
|
49
|
-
process_messages
|
50
|
-
handle_error if result.zero?
|
51
|
-
end
|
52
|
-
|
53
|
-
# @raise [FFI::Error] if the FFI call returns an error.
|
54
|
-
def validate_roles_config(config)
|
55
|
-
result = Rust.validate_roles_config(self, JSON.dump(config))
|
56
|
-
process_messages
|
57
|
-
handle_error if result.zero?
|
58
|
-
end
|
59
|
-
|
60
45
|
def build_filter_plan(types, partials, variable, class_tag)
|
61
46
|
types = JSON.dump(types)
|
62
47
|
partials = JSON.dump(partials)
|
@@ -67,11 +52,10 @@ module Oso
|
|
67
52
|
JSON.parse plan
|
68
53
|
end
|
69
54
|
|
70
|
-
# @param
|
71
|
-
# @param filename [String]
|
55
|
+
# @param sources [Array<Source>]
|
72
56
|
# @raise [FFI::Error] if the FFI call returns an error.
|
73
|
-
def load(
|
74
|
-
loaded = Rust.load(self,
|
57
|
+
def load(sources)
|
58
|
+
loaded = Rust.load(self, JSON.dump(sources))
|
75
59
|
process_messages
|
76
60
|
handle_error if loaded.zero?
|
77
61
|
end
|
@@ -133,6 +117,14 @@ module Oso
|
|
133
117
|
handle_error if registered.zero?
|
134
118
|
end
|
135
119
|
|
120
|
+
# @param name [String]
|
121
|
+
# @param mro [Array<Integer>]
|
122
|
+
# @raise [FFI::Error] if the FFI call returns an error.
|
123
|
+
def register_mro(name, mro)
|
124
|
+
registered = Rust.register_mro(self, name, JSON.dump(mro))
|
125
|
+
handle_error if registered.zero?
|
126
|
+
end
|
127
|
+
|
136
128
|
def next_message
|
137
129
|
Rust.next_message(self)
|
138
130
|
end
|
data/lib/oso/polar/ffi/query.rb
CHANGED
@@ -50,8 +50,7 @@ module Oso
|
|
50
50
|
handle_error if res.zero?
|
51
51
|
end
|
52
52
|
|
53
|
-
# @param
|
54
|
-
# @param call_id [Integer]
|
53
|
+
# @param message [String]
|
55
54
|
# @raise [FFI::Error] if the FFI call returns an error.
|
56
55
|
def application_error(message)
|
57
56
|
res = Rust.application_error(self, message)
|
data/lib/oso/polar/host.rb
CHANGED
@@ -36,21 +36,23 @@ module Oso
|
|
36
36
|
|
37
37
|
# For holding type metadata: name, fields, etc.
|
38
38
|
class UserType
|
39
|
-
attr_reader :name, :klass, :id, :fields, :
|
39
|
+
attr_reader :name, :klass, :id, :fields, :build_query, :combine_query, :exec_query
|
40
40
|
|
41
|
-
def initialize(name:, klass:, id:, fields:,
|
41
|
+
def initialize(name:, klass:, id:, fields:, build_query:, combine_query:, exec_query:) # rubocop:disable Metrics/ParameterLists
|
42
42
|
@name = name
|
43
43
|
@klass = klass
|
44
44
|
@id = id
|
45
45
|
# accept symbol keys
|
46
46
|
@fields = fields.each_with_object({}) { |kv, o| o[kv[0].to_s] = kv[1] }
|
47
|
-
@
|
47
|
+
@build_query = build_query
|
48
|
+
@combine_query = combine_query
|
49
|
+
@exec_query = exec_query
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
53
|
# Translate between Polar and the host language (Ruby).
|
52
54
|
class Host # rubocop:disable Metrics/ClassLength
|
53
|
-
# @return [Hash<String,
|
55
|
+
# @return [Hash<String, UserType>]
|
54
56
|
attr_reader :types
|
55
57
|
|
56
58
|
protected
|
@@ -97,19 +99,31 @@ module Oso
|
|
97
99
|
# @return [String] the name the class is cached as.
|
98
100
|
# @raise [DuplicateClassAliasError] if attempting to register a class
|
99
101
|
# under a previously-registered name.
|
100
|
-
def cache_class(cls, name:, fields:
|
102
|
+
def cache_class(cls, name:, fields:, build_query:, combine_query:, exec_query:) # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
101
103
|
raise DuplicateClassAliasError.new name: name, old: get_class(name), new: cls if types.key? name
|
102
104
|
|
103
105
|
types[name] = types[cls] = UserType.new(
|
104
106
|
name: name,
|
105
107
|
klass: PolarClass.new(cls),
|
106
108
|
id: cache_instance(cls),
|
107
|
-
fields: fields,
|
108
|
-
|
109
|
+
fields: fields || {},
|
110
|
+
combine_query: combine_query,
|
111
|
+
exec_query: exec_query,
|
112
|
+
build_query: build_query
|
109
113
|
)
|
110
114
|
name
|
111
115
|
end
|
112
116
|
|
117
|
+
def register_mros # rubocop:disable Metrics/AbcSize
|
118
|
+
types.values.uniq.each do |typ|
|
119
|
+
mro = []
|
120
|
+
typ.klass.get.ancestors.each do |a|
|
121
|
+
mro.push(types[a].id) if types.key?(a)
|
122
|
+
end
|
123
|
+
ffi_polar.register_mro(typ.name, mro)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
113
127
|
# Check if an instance exists in the {#instances} cache.
|
114
128
|
#
|
115
129
|
# @param id [Integer]
|
@@ -181,7 +195,7 @@ module Oso
|
|
181
195
|
|
182
196
|
# Compare two values
|
183
197
|
#
|
184
|
-
# @param
|
198
|
+
# @param operation [String] operation to perform.
|
185
199
|
# @param args [Array<Object>] left and right args to operation.
|
186
200
|
# @raise [PolarRuntimeError] if operation fails or is unsupported.
|
187
201
|
# @return [Boolean]
|
@@ -234,9 +248,9 @@ module Oso
|
|
234
248
|
field_types = {}
|
235
249
|
fields.each do |k, v|
|
236
250
|
field_types[k] =
|
237
|
-
if v.is_a? ::Oso::Polar::DataFiltering::
|
251
|
+
if v.is_a? ::Oso::Polar::DataFiltering::Relation
|
238
252
|
{
|
239
|
-
'
|
253
|
+
'Relation' => {
|
240
254
|
'kind' => v.kind,
|
241
255
|
'other_class_tag' => v.other_type,
|
242
256
|
'my_field' => v.my_field,
|
@@ -293,7 +307,9 @@ module Oso
|
|
293
307
|
{ 'Pattern' => { 'Instance' => { 'tag' => value.tag, 'fields' => dict['Dictionary'] } } }
|
294
308
|
end
|
295
309
|
else
|
296
|
-
|
310
|
+
instance_id = nil
|
311
|
+
instance_id = types[value].id if value.is_a?(Class) && types.key?(value)
|
312
|
+
{ 'ExternalInstance' => { 'instance_id' => cache_instance(value, id: instance_id), 'repr' => nil } }
|
297
313
|
end
|
298
314
|
{ 'value' => value }
|
299
315
|
end
|
data/lib/oso/polar/polar.rb
CHANGED
@@ -27,6 +27,35 @@ def print_error(error)
|
|
27
27
|
warn error.message
|
28
28
|
end
|
29
29
|
|
30
|
+
# Polar source string with optional filename.
|
31
|
+
class Source
|
32
|
+
# @return [String]
|
33
|
+
attr_reader :src, :filename
|
34
|
+
|
35
|
+
# @param src [String]
|
36
|
+
# @param filename [String]
|
37
|
+
def initialize(src, filename: nil)
|
38
|
+
@src = src
|
39
|
+
@filename = filename
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_json(*_args)
|
43
|
+
{ src: src, filename: filename }.to_json
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def filename_to_source(filename)
|
48
|
+
raise Oso::Polar::PolarFileExtensionError, filename unless File.extname(filename) == '.polar'
|
49
|
+
|
50
|
+
src = File.open(filename, &:read)
|
51
|
+
|
52
|
+
raise Oso::Polar::NullByteInPolarFileError if src.chomp("\0").include?("\0")
|
53
|
+
|
54
|
+
Source.new(src, filename: filename)
|
55
|
+
rescue Errno::ENOENT
|
56
|
+
raise Oso::Polar::PolarFileNotFoundError, filename
|
57
|
+
end
|
58
|
+
|
30
59
|
module Oso
|
31
60
|
module Polar
|
32
61
|
# Create and manage an instance of the Polar runtime.
|
@@ -34,11 +63,10 @@ module Oso
|
|
34
63
|
# @return [Host]
|
35
64
|
attr_reader :host
|
36
65
|
|
37
|
-
def initialize
|
66
|
+
def initialize
|
38
67
|
@ffi_polar = FFI::Polar.create
|
39
68
|
@host = Host.new(ffi_polar)
|
40
69
|
@ffi_polar.enrich_message = @host.method(:enrich_message)
|
41
|
-
@polar_roles_enabled = false
|
42
70
|
|
43
71
|
# Register global constants.
|
44
72
|
register_constant nil, name: 'nil'
|
@@ -56,42 +84,6 @@ module Oso
|
|
56
84
|
@ffi_polar
|
57
85
|
end
|
58
86
|
|
59
|
-
def enable_roles # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
60
|
-
return if polar_roles_enabled
|
61
|
-
|
62
|
-
roles_helper = Class.new do
|
63
|
-
def self.join(separator, left, right)
|
64
|
-
[left, right].join(separator)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
register_constant(roles_helper, name: '__oso_internal_roles_helpers__')
|
68
|
-
ffi_polar.enable_roles
|
69
|
-
self.polar_roles_enabled = true
|
70
|
-
|
71
|
-
# validate config
|
72
|
-
validation_query_results = []
|
73
|
-
loop do
|
74
|
-
query = ffi_polar.next_inline_query
|
75
|
-
break if query.nil?
|
76
|
-
|
77
|
-
new_host = host.dup
|
78
|
-
new_host.accept_expression = true
|
79
|
-
results = Query.new(query, host: new_host).to_a
|
80
|
-
raise InlineQueryFailedError, query.source if results.empty?
|
81
|
-
|
82
|
-
validation_query_results.push results
|
83
|
-
end
|
84
|
-
|
85
|
-
# turn bindings back into polar
|
86
|
-
validation_query_results = validation_query_results.map do |results|
|
87
|
-
results.map do |result|
|
88
|
-
{ 'bindings' => result.transform_values { |v| host.to_polar(v) } }
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
ffi_polar.validate_roles_config(validation_query_results)
|
93
|
-
end
|
94
|
-
|
95
87
|
# get the (maybe user-supplied) name of a class.
|
96
88
|
# kind of a hack because of class autoreloading.
|
97
89
|
def get_class_name(klass) # rubocop:disable Metrics/AbcSize
|
@@ -108,61 +100,52 @@ module Oso
|
|
108
100
|
end
|
109
101
|
end
|
110
102
|
|
111
|
-
def get_allowed_resources(actor, action, klass) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
112
|
-
resource = Variable.new 'resource'
|
113
|
-
class_name = get_class_name klass
|
114
|
-
constraint = Expression.new(
|
115
|
-
'And',
|
116
|
-
[Expression.new('Isa', [resource, Pattern.new(class_name, {})])]
|
117
|
-
)
|
118
|
-
|
119
|
-
results = query_rule(
|
120
|
-
'allow',
|
121
|
-
actor,
|
122
|
-
action,
|
123
|
-
resource,
|
124
|
-
bindings: { 'resource' => constraint },
|
125
|
-
accept_expression: true
|
126
|
-
)
|
127
|
-
|
128
|
-
complete = []
|
129
|
-
partial = []
|
130
|
-
|
131
|
-
results.to_a.each do |result|
|
132
|
-
result.to_a.each do |key, val|
|
133
|
-
if val.is_a? Expression
|
134
|
-
partial.push({ 'bindings' => { key => host.to_polar(val) } })
|
135
|
-
else
|
136
|
-
complete.push val
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
filter = ::Oso::Polar::DataFiltering::FilterPlan.new(self, partial, class_name)
|
141
|
-
complete + filter.resolve
|
142
|
-
end
|
143
|
-
|
144
103
|
# Clear all rules and rule sources from the current Polar instance
|
145
104
|
#
|
146
105
|
# @return [self] for chaining.
|
147
106
|
def clear_rules
|
148
107
|
ffi_polar.clear_rules
|
149
|
-
ffi_polar.enable_roles if polar_roles_enabled
|
150
108
|
self
|
151
109
|
end
|
152
110
|
|
153
|
-
# Load
|
111
|
+
# Load Polar policy files.
|
154
112
|
#
|
155
|
-
# @param
|
156
|
-
# @raise [PolarFileExtensionError] if
|
157
|
-
# @raise [PolarFileNotFoundError] if
|
113
|
+
# @param filenames [Array<String>]
|
114
|
+
# @raise [PolarFileExtensionError] if any filename has an invalid extension.
|
115
|
+
# @raise [PolarFileNotFoundError] if any filename does not exist.
|
116
|
+
# @raise [NullByteInPolarFileError] if any file contains a non-terminating null byte.
|
117
|
+
# @raise [Error] if any of the FFI calls raise one.
|
118
|
+
# @raise [InlineQueryFailedError] on the first failed inline query.
|
158
119
|
# @return [self] for chaining.
|
159
|
-
def
|
160
|
-
|
120
|
+
def load_files(filenames = [])
|
121
|
+
return if filenames.empty?
|
161
122
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
123
|
+
sources = filenames.map { |f| filename_to_source f }
|
124
|
+
load_sources(sources)
|
125
|
+
self
|
126
|
+
end
|
127
|
+
|
128
|
+
# Load a Polar policy file.
|
129
|
+
#
|
130
|
+
# @param filename [String]
|
131
|
+
# @raise [PolarFileExtensionError] if filename has an invalid extension.
|
132
|
+
# @raise [PolarFileNotFoundError] if filename does not exist.
|
133
|
+
# @raise [NullByteInPolarFileError] if file contains a non-terminating null byte.
|
134
|
+
# @raise [Error] if any of the FFI calls raise one.
|
135
|
+
# @raise [InlineQueryFailedError] on the first failed inline query.
|
136
|
+
# @return [self] for chaining.
|
137
|
+
#
|
138
|
+
# @deprecated {#load_file} has been deprecated in favor of {#load_files}
|
139
|
+
# as of the 0.20.0 release. Please see changelog for migration
|
140
|
+
# instructions:
|
141
|
+
# https://docs.osohq.com/project/changelogs/2021-09-15.html
|
142
|
+
def load_file(filename)
|
143
|
+
warn <<~WARNING
|
144
|
+
`Oso#load_file` has been deprecated in favor of `Oso#load_files` as of the 0.20.0 release.
|
145
|
+
|
146
|
+
Please see changelog for migration instructions: https://docs.osohq.com/project/changelogs/2021-09-15.html
|
147
|
+
WARNING
|
148
|
+
load_files([filename])
|
166
149
|
end
|
167
150
|
|
168
151
|
# Load a Polar string into the KB.
|
@@ -170,26 +153,13 @@ module Oso
|
|
170
153
|
# @param str [String] Polar string to load.
|
171
154
|
# @param filename [String] Name of Polar source file.
|
172
155
|
# @raise [NullByteInPolarFileError] if str includes a non-terminating null byte.
|
173
|
-
# @raise [InlineQueryFailedError] on the first failed inline query.
|
174
156
|
# @raise [Error] if any of the FFI calls raise one.
|
157
|
+
# @raise [InlineQueryFailedError] on the first failed inline query.
|
175
158
|
# @return [self] for chaining.
|
176
|
-
def load_str(str, filename: nil)
|
159
|
+
def load_str(str, filename: nil)
|
177
160
|
raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
|
178
161
|
|
179
|
-
|
180
|
-
loop do
|
181
|
-
next_query = ffi_polar.next_inline_query
|
182
|
-
break if next_query.nil?
|
183
|
-
|
184
|
-
raise InlineQueryFailedError, next_query.source if Query.new(next_query, host: host).first.nil?
|
185
|
-
end
|
186
|
-
|
187
|
-
# If roles are enabled, re-validate config when new rules are loaded.
|
188
|
-
if polar_roles_enabled
|
189
|
-
self.polar_roles_enabled = false
|
190
|
-
enable_roles
|
191
|
-
end
|
192
|
-
|
162
|
+
load_sources([Source.new(str, filename: filename)])
|
193
163
|
self
|
194
164
|
end
|
195
165
|
|
@@ -227,6 +197,16 @@ module Oso
|
|
227
197
|
query(Predicate.new(name, args: args), host: host, bindings: bindings)
|
228
198
|
end
|
229
199
|
|
200
|
+
# Query for a rule, returning true if it has any results.
|
201
|
+
#
|
202
|
+
# @param name [String]
|
203
|
+
# @param args [Array<Object>]
|
204
|
+
# @return [Boolean] indicating whether the query found at least one result.
|
205
|
+
# @raise [Error] if the FFI call raises one.
|
206
|
+
def query_rule_once(name, *args)
|
207
|
+
query_rule(name, *args).any?
|
208
|
+
end
|
209
|
+
|
230
210
|
# Register a Ruby class with Polar.
|
231
211
|
#
|
232
212
|
# @param cls [Class] the class to register.
|
@@ -235,8 +215,15 @@ module Oso
|
|
235
215
|
# under a previously-registered name.
|
236
216
|
# @raise [FFI::Error] if the FFI call returns an error.
|
237
217
|
# @return [self] for chaining.
|
238
|
-
def register_class(cls, name: nil, fields:
|
239
|
-
name = host.cache_class(
|
218
|
+
def register_class(cls, name: nil, fields: nil, combine_query: nil, build_query: nil, exec_query: nil) # rubocop:disable Metrics/ParameterLists
|
219
|
+
name = host.cache_class(
|
220
|
+
cls,
|
221
|
+
name: name || cls.name,
|
222
|
+
fields: fields,
|
223
|
+
build_query: build_query || maybe_mtd(cls, :build_query),
|
224
|
+
combine_query: combine_query || maybe_mtd(cls, :combine_query),
|
225
|
+
exec_query: exec_query || maybe_mtd(cls, :exec_query)
|
226
|
+
)
|
240
227
|
register_constant(cls, name: name)
|
241
228
|
end
|
242
229
|
|
@@ -256,7 +243,7 @@ module Oso
|
|
256
243
|
# @param files [Array<String>]
|
257
244
|
# @raise [Error] if the FFI call raises one.
|
258
245
|
def repl(files = [])
|
259
|
-
files
|
246
|
+
load_files(files)
|
260
247
|
prompt = "#{FG_BLUE}query>#{RESET} "
|
261
248
|
# Try loading the readline module from the Ruby stdlib. If we get a
|
262
249
|
# LoadError, fall back to the standard REPL with no readline support.
|
@@ -268,9 +255,36 @@ module Oso
|
|
268
255
|
|
269
256
|
private
|
270
257
|
|
258
|
+
def type_constraint(var, cls)
|
259
|
+
Expression.new(
|
260
|
+
'And',
|
261
|
+
[Expression.new('Isa', [var, Pattern.new(get_class_name(cls), {})])]
|
262
|
+
)
|
263
|
+
end
|
264
|
+
|
265
|
+
def maybe_mtd(cls, mtd)
|
266
|
+
cls.respond_to?(mtd) && cls.method(mtd) || nil
|
267
|
+
end
|
268
|
+
|
271
269
|
# @return [FFI::Polar]
|
272
270
|
attr_reader :ffi_polar
|
273
|
-
|
271
|
+
|
272
|
+
# Register MROs, load Polar code, and check inline queries.
|
273
|
+
# @param sources [Array<Source>] Polar sources to load.
|
274
|
+
def load_sources(sources)
|
275
|
+
host.register_mros
|
276
|
+
ffi_polar.load(sources)
|
277
|
+
check_inline_queries
|
278
|
+
end
|
279
|
+
|
280
|
+
def check_inline_queries
|
281
|
+
loop do
|
282
|
+
next_query = ffi_polar.next_inline_query
|
283
|
+
break if next_query.nil?
|
284
|
+
|
285
|
+
raise InlineQueryFailedError, next_query.source if Query.new(next_query, host: host).none?
|
286
|
+
end
|
287
|
+
end
|
274
288
|
|
275
289
|
# The R and L in REPL for systems where readline is available.
|
276
290
|
def repl_readline(prompt)
|
data/lib/oso/polar/query.rb
CHANGED
@@ -18,6 +18,16 @@ module Oso
|
|
18
18
|
bindings.each { |k, v| ffi_query.bind k, host.to_polar(v) }
|
19
19
|
end
|
20
20
|
|
21
|
+
# Create an enumerator that can be polled to advance the query loop. Yields
|
22
|
+
# results one by one.
|
23
|
+
#
|
24
|
+
# @yieldparam [Hash<String, Object>]
|
25
|
+
# @return [Enumerator]
|
26
|
+
# @raise [Error] if any of the FFI calls raise one.
|
27
|
+
def each(&block)
|
28
|
+
run(&block)
|
29
|
+
end
|
30
|
+
|
21
31
|
private
|
22
32
|
|
23
33
|
# @return [Hash<Integer, Enumerator>]
|
@@ -65,10 +75,11 @@ module Oso
|
|
65
75
|
# Fetch the next result from calling a Ruby method and prepare it for
|
66
76
|
# transmission across the FFI boundary.
|
67
77
|
#
|
68
|
-
# @param
|
69
|
-
# @param args [Array<Hash>]
|
78
|
+
# @param attribute [#to_sym]
|
70
79
|
# @param call_id [Integer]
|
71
80
|
# @param instance [Hash<String, Object>]
|
81
|
+
# @param args [Array<Hash>]
|
82
|
+
# @param kwargs [Hash<String, Object>]
|
72
83
|
# @raise [Error] if the FFI call raises one.
|
73
84
|
def handle_call(attribute, call_id:, instance:, args:, kwargs:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
74
85
|
instance = host.to_ruby(instance)
|
@@ -100,12 +111,12 @@ module Oso
|
|
100
111
|
raise unless cls.fields.key? tag
|
101
112
|
|
102
113
|
ref = cls.fields[tag]
|
103
|
-
return host.types[ref] unless ref.is_a? ::Oso::Polar::DataFiltering::
|
114
|
+
return host.types[ref] unless ref.is_a? ::Oso::Polar::DataFiltering::Relation
|
104
115
|
|
105
116
|
case ref.kind
|
106
|
-
when '
|
117
|
+
when 'one'
|
107
118
|
host.types[ref.other_type]
|
108
|
-
when '
|
119
|
+
when 'many'
|
109
120
|
host.types[Array]
|
110
121
|
end
|
111
122
|
end
|
@@ -152,12 +163,12 @@ module Oso
|
|
152
163
|
host.make_instance(cls_name, args: args, kwargs: kwargs, id: id)
|
153
164
|
end
|
154
165
|
|
155
|
-
#
|
166
|
+
# Run the main Polar loop, yielding results as they are emitted from the VM.
|
156
167
|
#
|
157
168
|
# @yieldparam [Hash<String, Object>]
|
158
169
|
# @return [Enumerator]
|
159
170
|
# @raise [Error] if any of the FFI calls raise one.
|
160
|
-
def
|
171
|
+
def run # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
161
172
|
loop do # rubocop:disable Metrics/BlockLength
|
162
173
|
event = ffi_query.next_event
|
163
174
|
case event.kind
|
@@ -227,21 +238,21 @@ module Oso
|
|
227
238
|
return unless typ
|
228
239
|
|
229
240
|
rel = typ.fields[attr]
|
230
|
-
return unless rel.is_a? ::Oso::Polar::DataFiltering::
|
241
|
+
return unless rel.is_a? ::Oso::Polar::DataFiltering::Relation
|
231
242
|
|
232
243
|
rel
|
233
244
|
end
|
234
245
|
|
235
246
|
def handle_relationship(call_id, instance, rel) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
236
|
-
|
237
|
-
constraint = ::Oso::Polar::DataFiltering::
|
247
|
+
typ = host.types[rel.other_type]
|
248
|
+
constraint = ::Oso::Polar::DataFiltering::Filter.new(
|
238
249
|
kind: 'Eq',
|
239
250
|
field: rel.other_field,
|
240
251
|
value: instance.send(rel.my_field)
|
241
252
|
)
|
242
|
-
res =
|
253
|
+
res = typ.exec_query[typ.build_query[[constraint]]]
|
243
254
|
|
244
|
-
if rel.kind == '
|
255
|
+
if rel.kind == 'one'
|
245
256
|
raise "multiple parents: #{res}" unless res.length == 1
|
246
257
|
|
247
258
|
res = res[0]
|
data/lib/oso/version.rb
CHANGED
data/lib/oso.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'oso/oso'
|
4
|
+
require 'oso/errors'
|
4
5
|
require 'oso/polar'
|
5
6
|
require 'oso/version'
|
6
7
|
|
7
|
-
# Top-level namespace for
|
8
|
+
# Top-level namespace for Oso authorization library.
|
8
9
|
module Oso
|
9
|
-
def self.new
|
10
|
-
::Oso::Oso.new
|
10
|
+
def self.new(not_found_error: NotFoundError, forbidden_error: ForbiddenError, read_action: 'read')
|
11
|
+
::Oso::Oso.new(not_found_error: not_found_error, forbidden_error: forbidden_error, read_action: read_action)
|
11
12
|
end
|
12
13
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: oso-oso
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.20.
|
4
|
+
version: 0.20.1.pre.beta
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oso Security, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-09-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ffi
|
@@ -158,6 +158,7 @@ files:
|
|
158
158
|
- ext/oso-oso/lib/libpolar.so
|
159
159
|
- ext/oso-oso/lib/polar.dll
|
160
160
|
- lib/oso.rb
|
161
|
+
- lib/oso/errors.rb
|
161
162
|
- lib/oso/oso.rb
|
162
163
|
- lib/oso/polar.rb
|
163
164
|
- lib/oso/polar/data_filtering.rb
|