oso-oso 0.22.1 → 0.25.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 654992655445b1208dd37d8346e2bc969c75a883
4
- data.tar.gz: 67f729e79b4ea0912f23141134e0543b7cca023a
3
+ metadata.gz: 82a8f9d2d54d8179e5a6067c929ec37ab0363250
4
+ data.tar.gz: 28a9724e32230d89c3e6140ab3d914aa6e0bd798
5
5
  SHA512:
6
- metadata.gz: 000e0208a70ac3bcb967a3ee7ad2ecf2d89bcdb03a36abfadf89fbf5ab8da3e5f20b5209d9061d4c0f8a0e8c9cb4eec0723696771824e4866f3e6ac4d07734f8
7
- data.tar.gz: a73f3e21418f3a3445bdcff2e81a50a36cc6d57bec0e5467c9ae7d4e77220924f1a96bd818d52dc3bfebc9ffc780c5ab6463c18df0ff55774fd5325966472a5f
6
+ metadata.gz: cd73f0a8bb0dd364229686d212c8b8cd56512ba9affee5dfd1a1f3b32b466cd793774121e2dd4425a708f7a35dd03bd35568d7097296fd568979cbfce77cffc8
7
+ data.tar.gz: 60a6991495c8666c8cab642e82d0f11988b726d5911ecccd7aec079e5f3041dfb978e21181bd910dbde1b7e551aef29a0d05e901432f2a1a1789e0c9fd16c137
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.22.1)
4
+ oso-oso (0.25.1)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
@@ -21,14 +21,14 @@ GEM
21
21
  arel (9.0.0)
22
22
  ast (2.4.2)
23
23
  backport (1.2.0)
24
- benchmark (0.1.1)
24
+ benchmark (0.2.0)
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
30
  ffi (1.15.4)
31
- i18n (1.8.10)
31
+ i18n (1.8.11)
32
32
  concurrent-ruby (~> 1.0)
33
33
  jaro_winkler (1.5.4)
34
34
  maruku (0.7.3)
@@ -49,7 +49,7 @@ GEM
49
49
  rainbow (3.0.0)
50
50
  rake (12.3.3)
51
51
  regexp_parser (2.1.1)
52
- reverse_markdown (2.0.0)
52
+ reverse_markdown (2.1.1)
53
53
  nokogiri
54
54
  rexml (3.2.5)
55
55
  rspec (3.10.0)
@@ -64,7 +64,7 @@ GEM
64
64
  rspec-mocks (3.10.2)
65
65
  diff-lcs (>= 1.2.0, < 2.0)
66
66
  rspec-support (~> 3.10.0)
67
- rspec-support (3.10.2)
67
+ rspec-support (3.10.3)
68
68
  rubocop (0.89.1)
69
69
  parallel (~> 1.10)
70
70
  parser (>= 2.7.1.1)
@@ -97,12 +97,13 @@ GEM
97
97
  tilt (2.0.10)
98
98
  tzinfo (1.2.9)
99
99
  thread_safe (~> 0.1)
100
- unicode-display_width (1.7.0)
100
+ unicode-display_width (1.8.0)
101
101
  yard (0.9.26)
102
102
 
103
103
  PLATFORMS
104
104
  ruby
105
105
  x86_64-darwin-20
106
+ x86_64-linux
106
107
 
107
108
  DEPENDENCIES
108
109
  activerecord
data/Makefile CHANGED
@@ -7,7 +7,7 @@ install:
7
7
  bundle install
8
8
 
9
9
  test: install rust
10
- bundle exec rake spec
10
+ POLAR_IGNORE_NO_ALLOW_WARNING=1 bundle exec rake spec
11
11
 
12
12
  lint: install
13
13
  bundle exec rubocop
Binary file
Binary file
Binary file
data/lib/oso/oso.rb CHANGED
@@ -181,27 +181,17 @@ module Oso
181
181
  # @param resource_cls The resource being accessed.
182
182
  #
183
183
  # @return 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) } })
184
+ def authorized_query(actor, action, resource_cls)
185
+ if host.use_new_data_filtering?
186
+
187
+ unless host.types[resource_cls].build_query == ::Oso::Polar::Host::DEFAULT_BUILD_QUERY
188
+ warn 'Warning: redundant data filtering configuration detected'
199
189
  end
200
- end
201
190
 
202
- ::Oso::Polar::DataFiltering::FilterPlan
203
- .parse(self, results, get_class_name(resource_cls))
204
- .build_query
191
+ new_authorized_query(actor, action, resource_cls)
192
+ else
193
+ old_authorized_query(actor, action, resource_cls)
194
+ end
205
195
  end
206
196
 
207
197
  # Determine the resources of type +resource_cls+ that +actor+
@@ -213,10 +203,15 @@ module Oso
213
203
  #
214
204
  # @return A list of resources accessible to the actor.
215
205
  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]
206
+ q = authorized_query(actor, action, resource_cls)
207
+
208
+ if host.use_new_data_filtering?
209
+ host.adapter.execute_query q
210
+ elsif q.nil?
211
+ []
212
+ else
213
+ host.types[resource_cls].exec_query[q]
214
+ end
220
215
  end
221
216
 
222
217
  # Register default values for data filtering query functions.
@@ -228,5 +223,9 @@ module Oso
228
223
  host.exec_query = exec_query if exec_query
229
224
  host.combine_query = combine_query if combine_query
230
225
  end
226
+
227
+ def data_filtering_adapter=(adapter)
228
+ host.adapter = adapter
229
+ end
231
230
  end
232
231
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module Data
6
+ class Adapter
7
+ # Example data filtering adapter for ActiveRecord
8
+ class ActiveRecordAdapter < Adapter
9
+ def build_query(filter) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
10
+ types = filter.types
11
+ query = filter.relations.reduce(filter.model.all) do |q, rel|
12
+ rec = types[rel.left].fields[rel.name]
13
+ q.joins(
14
+ "INNER JOIN #{rel.right.table_name} ON " \
15
+ "#{rel.left.table_name}.#{rec.my_field} = " \
16
+ "#{rel.right.table_name}.#{rec.other_field}"
17
+ )
18
+ end
19
+
20
+ filter.conditions.map do |conjs|
21
+ conjs.reduce(query) do |q, conj|
22
+ q.where(*sqlize(conj))
23
+ end
24
+ end.reduce(:or).distinct
25
+ end
26
+
27
+ def execute_query(query)
28
+ query.to_a
29
+ end
30
+
31
+ OPS = {
32
+ 'Eq' => '=', 'In' => 'IN', 'Nin' => 'NOT IN', 'Neq' => '!=',
33
+ 'Lt' => '<', 'Gt' => '>', 'Leq' => '<=', 'Geq' => '>='
34
+ }.freeze
35
+
36
+ private
37
+
38
+ def sqlize(cond)
39
+ args = []
40
+ lhs = add_side cond.left, args
41
+ rhs = add_side cond.right, args
42
+ args.unshift "#{lhs} #{OPS[cond.cmp]} #{rhs}"
43
+ end
44
+
45
+ def add_side(side, args)
46
+ if side.is_a? ::Oso::Polar::Data::Filter::Projection
47
+ "#{side.source.table_name}.#{side.field || side.source.primary_key}"
48
+ else
49
+ args.push side
50
+ '?'
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module Data
6
+ # Abstract data adapter
7
+ #
8
+ # An Adapter has to implement two methods.
9
+ class Adapter
10
+ # Make a query object from a filter
11
+ def build_query(_filter)
12
+ raise "build_query not implemented for #{self}"
13
+ end
14
+
15
+ # Make a list of objects from a query
16
+ def execute_query(_query)
17
+ raise "execute_query not implemented for #{self}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Data filtering interface for Ruby
6
+ module Data
7
+ # Abstract data filter used by the Adapter API.
8
+ class Filter
9
+ attr_reader :model, :relations, :conditions, :types
10
+
11
+ def initialize(model:, relations:, conditions:, types:)
12
+ @model = model
13
+ @relations = relations
14
+ @conditions = conditions
15
+ @types = types
16
+ end
17
+
18
+ def self.parse(polar, blob)
19
+ types = polar.host.types
20
+ model = types[blob['root']].klass.get
21
+ relations = blob['relations'].map do |rel|
22
+ Relation.parse(polar, *rel)
23
+ end
24
+ conditions = blob['conditions'].map do |disj|
25
+ disj.map { |conj| Condition.parse(polar, *conj) }
26
+ end
27
+ new(model: model, relations: relations, conditions: conditions, types: types)
28
+ end
29
+
30
+ Projection = Struct.new(:source, :field)
31
+
32
+ Relation = Struct.new(:left, :name, :right) do
33
+ def self.parse(polar, left, name, right)
34
+ Relation.new(polar.name_to_class(left), name, polar.name_to_class(right))
35
+ end
36
+ end
37
+
38
+ Condition = Struct.new(:left, :cmp, :right) do
39
+ def self.parse(polar, left, cmp, right)
40
+ Condition.new(parse_side(polar, left), cmp, parse_side(polar, right))
41
+ end
42
+
43
+ def self.parse_side(polar, side)
44
+ key = side.keys.first
45
+ val = side[key]
46
+ case key
47
+ when 'Field'
48
+ Projection.new(polar.name_to_class(val[0]), val[1])
49
+ when 'Immediate'
50
+ polar.host.to_ruby('value' => [[val.keys.first, val.values.first]])
51
+ else
52
+ raise key
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'data/adapter'
4
+ require_relative 'data/filter'
@@ -23,11 +23,9 @@ module Oso
23
23
 
24
24
  # Errors from across the FFI boundary.
25
25
 
26
- class SerializationError < PolarRuntimeError; end
27
26
  class UnsupportedError < PolarRuntimeError; end
28
27
  class PolarTypeError < PolarRuntimeError; end
29
28
  class StackOverflowError < PolarRuntimeError; end
30
- class FileLoadingError < PolarRuntimeError; end
31
29
 
32
30
  # Errors originating from this side of the FFI boundary.
33
31
 
@@ -96,10 +94,6 @@ module Oso
96
94
  class UnrecognizedToken < ParseError; end
97
95
  end
98
96
 
99
- # Generic Polar API exception.
100
- class ApiError < Error; end
101
- class ParameterError < ApiError; end
102
-
103
97
  class ValidationError < Error; end
104
98
 
105
99
  UNEXPECTED_EXPRESSION_MESSAGE = <<~MSG
@@ -4,7 +4,7 @@ module Oso
4
4
  module Polar
5
5
  # Polar expression.
6
6
  class Expression
7
- attr_reader :operator, :args
7
+ attr_accessor :operator, :args
8
8
 
9
9
  # @param operator [String]
10
10
  # @param args [Array<Object>]
@@ -6,29 +6,13 @@ module Oso
6
6
  module Polar
7
7
  module FFI
8
8
  # Wrapper class for Error FFI pointer + operations.
9
- class Error < ::FFI::AutoPointer
10
- def to_s
11
- @to_s ||= read_string.force_encoding('UTF-8')
12
- end
13
-
14
- Rust = Module.new do
15
- extend ::FFI::Library
16
- ffi_lib FFI::LIB_PATH
17
-
18
- attach_function :get, :polar_get_error, [], Error
19
- attach_function :free, :string_free, [Error], :int32
20
- end
21
- private_constant :Rust
22
-
9
+ class Error
23
10
  # Check for an FFI error and convert it into a Ruby exception.
24
11
  #
25
12
  # @return [::Oso::Polar::Error] if there's an FFI error.
26
13
  # @return [::Oso::Polar::FFIErrorNotFound] if there isn't one.
27
- def self.get(enrich_message) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
28
- error = Rust.get
29
- return ::Oso::Polar::FFIErrorNotFound if error.null?
30
-
31
- error = JSON.parse(error.to_s)
14
+ def self.get(error_str, enrich_message) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
15
+ error = JSON.parse(error_str.to_s)
32
16
  msg = error['formatted']
33
17
  kind, body = error['kind'].first
34
18
 
@@ -54,8 +38,6 @@ module Oso
54
38
  runtime_error(subkind, msg: msg, details: details)
55
39
  when 'Operational'
56
40
  operational_error(subkind, msg: msg, details: details)
57
- when 'Parameter'
58
- api_error(subkind, msg: msg, details: details)
59
41
  when 'Validation'
60
42
  validation_error(msg, details: details)
61
43
  end
@@ -92,18 +74,14 @@ module Oso
92
74
  # @param msg [String]
93
75
  # @param details [Hash<String, Object>]
94
76
  # @return [::Oso::Polar::PolarRuntimeError] the object converted into the expected format.
95
- private_class_method def self.runtime_error(kind, msg:, details:) # rubocop:disable Metrics/MethodLength
77
+ private_class_method def self.runtime_error(kind, msg:, details:)
96
78
  case kind
97
- when 'Serialization'
98
- ::Oso::Polar::SerializationError.new(msg, details: details)
99
79
  when 'Unsupported'
100
80
  ::Oso::Polar::UnsupportedError.new(msg, details: details)
101
81
  when 'TypeError'
102
82
  ::Oso::Polar::PolarTypeError.new(msg, details: details)
103
83
  when 'StackOverflow'
104
84
  ::Oso::Polar::StackOverflowError.new(msg, details: details)
105
- when 'FileLoading'
106
- ::Oso::Polar::FileLoadingError.new(msg, details: details)
107
85
  else
108
86
  ::Oso::Polar::PolarRuntimeError.new(msg, details: details)
109
87
  end
@@ -124,21 +102,6 @@ module Oso
124
102
  end
125
103
  end
126
104
 
127
- # Map FFI API errors into Ruby exceptions.
128
- #
129
- # @param kind [String]
130
- # @param msg [String]
131
- # @param details [Hash<String, Object>]
132
- # @return [::Oso::Polar::ApiError] the object converted into the expected format.
133
- private_class_method def self.api_error(kind, msg:, details:)
134
- case kind
135
- when 'Parameter'
136
- ::Oso::Polar::ParameterError.new(msg, details: details)
137
- else
138
- ::Oso::Polar::ApiError.new(msg, details: details)
139
- end
140
- end
141
-
142
105
  # Map FFI Validation errors into Ruby exceptions.
143
106
  #
144
107
  # @param msg [String]
@@ -6,7 +6,7 @@ module Oso
6
6
  module Polar
7
7
  module FFI
8
8
  # Wrapper class for Polar FFI pointer + operations.
9
- class Polar < ::FFI::AutoPointer
9
+ class Polar < ::FFI::AutoPointer # rubocop:disable Metrics/ClassLength
10
10
  attr_accessor :enrich_message
11
11
 
12
12
  Rust = Module.new do
@@ -14,21 +14,28 @@ module Oso
14
14
  ffi_lib FFI::LIB_PATH
15
15
 
16
16
  attach_function :new, :polar_new, [], FFI::Polar
17
- attach_function :load, :polar_load, [FFI::Polar, :string], :int32
18
- attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], :int32
17
+ attach_function :load, :polar_load, [FFI::Polar, :string], CResultVoid
18
+ attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], CResultVoid
19
19
  attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
20
20
  attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
21
- attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], FFI::Query
22
- attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], FFI::Query
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
25
- attach_function :next_message, :polar_next_polar_message, [FFI::Polar], FFI::Message
21
+ attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], CResultQuery
22
+ attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], CResultQuery
23
+ attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], CResultVoid
24
+ attach_function :register_mro, :polar_register_mro, [FFI::Polar, :string, :string], CResultVoid
25
+ attach_function :next_message, :polar_next_polar_message, [FFI::Polar], CResultString
26
26
  attach_function :free, :polar_free, [FFI::Polar], :int32
27
+ attach_function :result_free, :result_free, [:pointer], :int32
27
28
  attach_function(
28
29
  :build_filter_plan,
29
30
  :polar_build_filter_plan,
30
31
  [FFI::Polar, :string, :string, :string, :string],
31
- :string
32
+ CResultString
33
+ )
34
+ attach_function(
35
+ :build_data_filter,
36
+ :polar_build_data_filter,
37
+ [FFI::Polar, :string, :string, :string, :string],
38
+ CResultString
32
39
  )
33
40
  end
34
41
  private_constant :Rust
@@ -36,10 +43,7 @@ module Oso
36
43
  # @return [FFI::Polar]
37
44
  # @raise [FFI::Error] if the FFI call returns an error.
38
45
  def self.create
39
- polar = Rust.new
40
- handle_error if polar.null?
41
-
42
- polar
46
+ Rust.new
43
47
  end
44
48
 
45
49
  def build_filter_plan(types, partials, variable, class_tag)
@@ -47,9 +51,19 @@ module Oso
47
51
  partials = JSON.dump(partials)
48
52
  plan = Rust.build_filter_plan(self, types, partials, variable, class_tag)
49
53
  process_messages
50
- handle_error if plan.nil?
54
+ plan = check_result plan
55
+ # TODO(gw) more error checking?
56
+ JSON.parse plan.to_s
57
+ end
58
+
59
+ def build_data_filter(types, partials, variable, class_tag)
60
+ types = JSON.dump(types)
61
+ partials = JSON.dump(partials)
62
+ plan = Rust.build_data_filter(self, types, partials, variable, class_tag)
63
+ process_messages
64
+ plan = check_result plan
51
65
  # TODO(gw) more error checking?
52
- JSON.parse plan
66
+ JSON.parse plan.to_s
53
67
  end
54
68
 
55
69
  # @param sources [Array<Source>]
@@ -57,14 +71,14 @@ module Oso
57
71
  def load(sources)
58
72
  loaded = Rust.load(self, JSON.dump(sources))
59
73
  process_messages
60
- handle_error if loaded.zero?
74
+ check_result loaded
61
75
  end
62
76
 
63
77
  # @raise [FFI::Error] if the FFI call returns an error.
64
78
  def clear_rules
65
79
  cleared = Rust.clear_rules(self)
66
80
  process_messages
67
- handle_error if cleared.zero?
81
+ check_result cleared
68
82
  end
69
83
 
70
84
  # @return [FFI::Query] if there are remaining inline queries.
@@ -79,12 +93,7 @@ module Oso
79
93
  # @return [Integer]
80
94
  # @raise [FFI::Error] if the FFI call returns an error.
81
95
  def new_id
82
- id = Rust.new_id(self)
83
- # TODO(gj): I don't think this error check is correct. If getting a new ID fails on the
84
- # Rust side, it'll probably surface as a panic (e.g., the KB lock is poisoned).
85
- handle_error if id.zero?
86
-
87
- id
96
+ Rust.new_id(self)
88
97
  end
89
98
 
90
99
  # @param str [String] Query string.
@@ -93,9 +102,7 @@ module Oso
93
102
  def new_query_from_str(str)
94
103
  query = Rust.new_query_from_str(self, str, 0)
95
104
  process_messages
96
- handle_error if query.null?
97
-
98
- query
105
+ check_result query
99
106
  end
100
107
 
101
108
  # @param term [Hash<String, Object>]
@@ -104,9 +111,7 @@ module Oso
104
111
  def new_query_from_term(term)
105
112
  query = Rust.new_query_from_term(self, JSON.dump(term), 0)
106
113
  process_messages
107
- handle_error if query.null?
108
-
109
- query
114
+ check_result query
110
115
  end
111
116
 
112
117
  # @param name [String]
@@ -114,7 +119,7 @@ module Oso
114
119
  # @raise [FFI::Error] if the FFI call returns an error.
115
120
  def register_constant(value, name:)
116
121
  registered = Rust.register_constant(self, name, JSON.dump(value))
117
- handle_error if registered.zero?
122
+ check_result registered
118
123
  end
119
124
 
120
125
  # @param name [String]
@@ -122,11 +127,24 @@ module Oso
122
127
  # @raise [FFI::Error] if the FFI call returns an error.
123
128
  def register_mro(name, mro)
124
129
  registered = Rust.register_mro(self, name, JSON.dump(mro))
125
- handle_error if registered.zero?
130
+ check_result registered
126
131
  end
127
132
 
128
133
  def next_message
129
- Rust.next_message(self)
134
+ check_result Rust.next_message(self)
135
+ end
136
+
137
+ def process_message(message, enrich_message)
138
+ message = JSON.parse(message.to_s)
139
+ kind = message['kind']
140
+ msg = enrich_message.call(message['msg'])
141
+
142
+ case kind
143
+ when 'Print'
144
+ puts(msg)
145
+ when 'Warning'
146
+ warn(format('[warning] %<msg>s', msg: msg))
147
+ end
130
148
  end
131
149
 
132
150
  def process_messages
@@ -134,12 +152,19 @@ module Oso
134
152
  message = next_message
135
153
  break if message.null?
136
154
 
137
- message.process(enrich_message)
155
+ process_message(message, enrich_message)
138
156
  end
139
157
  end
140
158
 
141
- def handle_error
142
- raise FFI::Error.get(enrich_message)
159
+ def check_result(res)
160
+ result = res[:result]
161
+ error = res[:error]
162
+ Rust.result_free(res)
163
+
164
+ raise 'internal error: both result and error pointers are not null' if !error.null? && !result.zero?
165
+ raise FFI::Error.get(error, enrich_message) unless error.null?
166
+
167
+ result
143
168
  end
144
169
  end
145
170
  end
@@ -13,15 +13,16 @@ module Oso
13
13
  extend ::FFI::Library
14
14
  ffi_lib FFI::LIB_PATH
15
15
 
16
- attach_function :debug_command, :polar_debug_command, [FFI::Query, :string], :int32
17
- attach_function :call_result, :polar_call_result, [FFI::Query, :uint64, :string], :int32
18
- attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], :int32
19
- attach_function :application_error, :polar_application_error, [FFI::Query, :string], :int32
20
- attach_function :next_event, :polar_next_query_event, [FFI::Query], FFI::QueryEvent
21
- attach_function :next_message, :polar_next_query_message, [FFI::Query], FFI::Message
22
- attach_function :source, :polar_query_source_info, [FFI::Query], FFI::Source
16
+ attach_function :debug_command, :polar_debug_command, [FFI::Query, :string], CResultVoid
17
+ attach_function :call_result, :polar_call_result, [FFI::Query, :uint64, :string], CResultVoid
18
+ attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], CResultVoid
19
+ attach_function :application_error, :polar_application_error, [FFI::Query, :string], CResultVoid
20
+ attach_function :next_event, :polar_next_query_event, [FFI::Query], CResultString
21
+ attach_function :next_message, :polar_next_query_message, [FFI::Query], CResultString
22
+ attach_function :source, :polar_query_source_info, [FFI::Query], CResultString
23
23
  attach_function :free, :query_free, [FFI::Query], :int32
24
- attach_function :bind, :polar_bind, [FFI::Query, :string, :string], :int32
24
+ attach_function :result_free, :result_free, [:pointer], :int32
25
+ attach_function :bind, :polar_bind, [FFI::Query, :string, :string], CResultVoid
25
26
  end
26
27
  private_constant :Rust
27
28
 
@@ -30,15 +31,15 @@ module Oso
30
31
  def debug_command(cmd)
31
32
  res = Rust.debug_command(self, cmd)
32
33
  process_messages
33
- handle_error if res.zero?
34
+ check_result res
34
35
  end
35
36
 
36
- # @param result [String]
37
+ # @param value [Object]
37
38
  # @param call_id [Integer]
38
39
  # @raise [FFI::Error] if the FFI call returns an error.
39
- def call_result(result, call_id:)
40
- res = Rust.call_result(self, call_id, result)
41
- handle_error if res.zero?
40
+ def call_result(value, call_id:)
41
+ res = Rust.call_result(self, call_id, JSON.dump(value))
42
+ check_result res
42
43
  end
43
44
 
44
45
  # @param result [Boolean]
@@ -47,14 +48,14 @@ module Oso
47
48
  def question_result(result, call_id:)
48
49
  result = result ? 1 : 0
49
50
  res = Rust.question_result(self, call_id, result)
50
- handle_error if res.zero?
51
+ check_result res
51
52
  end
52
53
 
53
54
  # @param message [String]
54
55
  # @raise [FFI::Error] if the FFI call returns an error.
55
56
  def application_error(message)
56
57
  res = Rust.application_error(self, message)
57
- handle_error if res.zero?
58
+ check_result res
58
59
  end
59
60
 
60
61
  # @return [::Oso::Polar::QueryEvent]
@@ -62,18 +63,32 @@ module Oso
62
63
  def next_event
63
64
  event = Rust.next_event(self)
64
65
  process_messages
65
- handle_error if event.null?
66
+ event = check_result event
66
67
 
67
68
  ::Oso::Polar::QueryEvent.new(JSON.parse(event.to_s))
68
69
  end
69
70
 
70
71
  def bind(name, value)
71
72
  res = Rust.bind(self, name, JSON.dump(value))
72
- handle_error if res.zero?
73
+ check_result res
73
74
  end
74
75
 
75
76
  def next_message
76
- Rust.next_message(self)
77
+ check_result Rust.next_message(self)
78
+ end
79
+
80
+ def process_message(message, enrich_message)
81
+ message = JSON.parse(message.to_s)
82
+ kind = message['kind']
83
+ msg = message['msg']
84
+ msg = enrich_message.call(msg)
85
+
86
+ case kind
87
+ when 'Print'
88
+ puts(msg)
89
+ when 'Warning'
90
+ warn(format('[warning] %<msg>s', msg: msg))
91
+ end
77
92
  end
78
93
 
79
94
  def process_messages
@@ -81,7 +96,7 @@ module Oso
81
96
  message = next_message
82
97
  break if message.null?
83
98
 
84
- message.process(enrich_message)
99
+ process_message(message, enrich_message)
85
100
  end
86
101
  end
87
102
 
@@ -89,13 +104,24 @@ module Oso
89
104
  # @raise [FFI::Error] if the FFI call returns an error.
90
105
  def source
91
106
  res = Rust.source(self)
92
- handle_error if res.null?
107
+ res = check_result res
93
108
 
94
109
  res.to_s
95
110
  end
96
111
 
97
- def handle_error
98
- raise FFI::Error.get(enrich_message)
112
+ # Unwrap the result by (a) extracting the pointers for
113
+ # result and error, (b) freeing the result pointers, and then
114
+ # (c) either returning the result pointer, or constructing and
115
+ # raising the error.
116
+ def check_result(res)
117
+ result = res[:result]
118
+ error = res[:error]
119
+ Rust.result_free(res)
120
+
121
+ raise 'internal error: both result and error pointers are not null' if !error.null? && !result.zero?
122
+ raise FFI::Error.get(error, enrich_message) unless error.null?
123
+
124
+ result
99
125
  end
100
126
  end
101
127
  end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
6
- # Wrapper class for Source FFI pointer.
7
- class Source < ::FFI::AutoPointer
8
+ # Wrapper class for Rust strings.
9
+ #
10
+ # Since we force all strings to go through this
11
+ # the `AutoPointer` class will handle
12
+ # actually freeing the string when deleting it
13
+ class RustString < ::FFI::AutoPointer
8
14
  # @return [String]
9
15
  def to_s
10
16
  @to_s ||= read_string.force_encoding('UTF-8')
@@ -14,7 +20,7 @@ module Oso
14
20
  extend ::FFI::Library
15
21
  ffi_lib FFI::LIB_PATH
16
22
 
17
- attach_function :free, :string_free, [Message], :int32
23
+ attach_function :free, :string_free, [RustString], :int32
18
24
  end
19
25
 
20
26
  private_constant :Rust
data/lib/oso/polar/ffi.rb CHANGED
@@ -4,6 +4,7 @@ require 'ffi'
4
4
 
5
5
  module Oso
6
6
  module Polar
7
+ # FFI classes shared between all ffi/*.rb modules
7
8
  module FFI
8
9
  LIB = "#{::FFI::Platform::LIBPREFIX}polar.#{::FFI::Platform::LIBSUFFIX}"
9
10
  RELEASE_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
@@ -18,41 +19,50 @@ module Oso
18
19
 
19
20
  # Wrapper class for Polar FFI pointer + operations.
20
21
  class Polar < ::FFI::AutoPointer
22
+ def zero?
23
+ null?
24
+ end
25
+
21
26
  def self.release(ptr)
22
27
  Rust.free(ptr) unless ptr.null?
23
28
  end
24
29
  end
25
30
  # Wrapper class for Query FFI pointer + operations.
26
31
  class Query < ::FFI::AutoPointer
27
- def self.release(ptr)
28
- Rust.free(ptr) unless ptr.null?
32
+ def zero?
33
+ null?
29
34
  end
30
- end
31
- # Wrapper class for QueryEvent FFI pointer + operations.
32
- class QueryEvent < ::FFI::AutoPointer
35
+
33
36
  def self.release(ptr)
34
37
  Rust.free(ptr) unless ptr.null?
35
38
  end
36
39
  end
37
- # Wrapper class for Error FFI pointer + operations.
38
- class Error < ::FFI::AutoPointer
39
- def self.release(ptr)
40
- Rust.free(ptr) unless ptr.null?
40
+
41
+ # Wrapper class for Rust strings FFI pointer + operations.
42
+ class RustString < ::FFI::AutoPointer
43
+ def zero?
44
+ null?
41
45
  end
42
- end
43
- # Wrapper class for Message FFI pointer + operations.
44
- class Message < ::FFI::AutoPointer
46
+
45
47
  def self.release(ptr)
46
48
  Rust.free(ptr) unless ptr.null?
47
49
  end
48
50
  end
49
51
 
50
- # Wrapper class for Source FFI pointer.
51
- class Source < ::FFI::AutoPointer
52
- def self.release(ptr)
53
- Rust.free(ptr) unless ptr.null?
54
- end
52
+ # Helper method to generate a Result type for different
53
+ # inner types
54
+ def self.result(result_klass)
55
+ Class.new(::FFI::Struct) do
56
+ layout :result, result_klass, :error, RustString
57
+ end.by_ref
55
58
  end
59
+
60
+ # Defines the result type version of
61
+ # each of these structs
62
+ # result(T) => { result: T, error: string }
63
+ CResultVoid = result(:int)
64
+ CResultString = result(RustString)
65
+ CResultQuery = result(Query)
56
66
  end
57
67
  private_constant :FFI
58
68
  end
@@ -60,7 +70,5 @@ end
60
70
 
61
71
  require 'oso/polar/ffi/polar'
62
72
  require 'oso/polar/ffi/query'
63
- require 'oso/polar/ffi/query_event'
64
73
  require 'oso/polar/ffi/error'
65
- require 'oso/polar/ffi/message'
66
- require 'oso/polar/ffi/source'
74
+ require 'oso/polar/ffi/rust_string'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'data'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  # Ruby code reloaders (i.e. the one used by rails) swap out the value of
@@ -67,7 +69,7 @@ module Oso
67
69
  public
68
70
 
69
71
  attr_writer :accept_expression
70
- attr_accessor :build_query, :combine_query, :exec_query
72
+ attr_accessor :build_query, :combine_query, :exec_query, :adapter
71
73
 
72
74
  DEFAULT_COMBINE_QUERY = proc { raise 'implement combine_query to use data filtering' }
73
75
  DEFAULT_BUILD_QUERY = proc { raise 'implement build_query to use data filtering' }
@@ -95,9 +97,10 @@ module Oso
95
97
  # @return [Class]
96
98
  # @raise [UnregisteredClassError] if the class has not been registered.
97
99
  def get_class(name)
98
- raise UnregisteredClassError, name unless types.key? name
100
+ typ = types[name]
101
+ raise UnregisteredClassError, name if typ.nil?
99
102
 
100
- types[name].klass.get
103
+ typ.klass.get
101
104
  end
102
105
 
103
106
  # Store a Ruby class in the {#types} cache.
@@ -317,7 +320,18 @@ module Oso
317
320
  else
318
321
  instance_id = nil
319
322
  instance_id = types[value].id if value.is_a?(Class) && types.key?(value)
320
- { 'ExternalInstance' => { 'instance_id' => cache_instance(value, id: instance_id), 'repr' => nil } }
323
+
324
+ # only pass class_repr for registered types
325
+ class_repr = nil
326
+ class_repr = value.class.to_s if types.key?(value.class)
327
+
328
+ {
329
+ 'ExternalInstance' => {
330
+ 'instance_id' => cache_instance(value, id: instance_id),
331
+ 'repr' => nil,
332
+ 'class_repr' => class_repr
333
+ }
334
+ }
321
335
  end
322
336
  { 'value' => value }
323
337
  end
@@ -337,21 +351,25 @@ module Oso
337
351
  value
338
352
  when 'Number'
339
353
  num = value.values.first
340
- if value.key? 'Float'
354
+ case value.keys.first
355
+ when 'Float'
341
356
  case num
342
357
  when 'Infinity'
343
- return Float::INFINITY
358
+ Float::INFINITY
344
359
  when '-Infinity'
345
- return -Float::INFINITY
360
+ -Float::INFINITY
346
361
  when 'NaN'
347
- return Float::NAN
362
+ Float::NAN
348
363
  else
349
364
  unless value['Float'].is_a? Float # rubocop:disable Metrics/BlockNesting
350
365
  raise PolarRuntimeError, "Expected a floating point number, got \"#{value['Float']}\""
351
366
  end
367
+
368
+ num
352
369
  end
370
+ else
371
+ num
353
372
  end
354
- num
355
373
  when 'List'
356
374
  value.map { |el| to_ruby(el) }
357
375
  when 'Dictionary'
@@ -389,6 +407,10 @@ module Oso
389
407
  get_instance(Regexp.last_match[1].to_i).to_s
390
408
  end
391
409
  end
410
+
411
+ def use_new_data_filtering?
412
+ !adapter.nil?
413
+ end
392
414
  end
393
415
  end
394
416
  end
@@ -84,20 +84,8 @@ module Oso
84
84
  @ffi_polar
85
85
  end
86
86
 
87
- # get the (maybe user-supplied) name of a class.
88
- # kind of a hack because of class autoreloading.
89
- def get_class_name(klass) # rubocop:disable Metrics/AbcSize
90
- if host.types.key? klass
91
- host.types[klass].name
92
- elsif host.types.key? klass.name
93
- host.types[klass.name].name
94
- else
95
- rec = host.types.values.find { |v| v.klass.get == klass }
96
- raise "Unknown class `#{klass}`" if rec.nil?
97
-
98
- host.types[klass] = rec
99
- rec.name
100
- end
87
+ def name_to_class(class_name)
88
+ host.types[class_name].klass.get
101
89
  end
102
90
 
103
91
  # Clear all rules and rule sources from the current Polar instance
@@ -259,10 +247,89 @@ module Oso
259
247
 
260
248
  private
261
249
 
250
+ # new/old data filtering core API shared logic
251
+ def partial_query(actor, action, resource_cls) # rubocop:disable Metrics/MethodLength
252
+ var_name = 'resource'
253
+ resource = Variable.new var_name
254
+
255
+ partials = query_rule(
256
+ 'allow',
257
+ actor,
258
+ action,
259
+ resource,
260
+ bindings: { var_name => type_constraint(resource, resource_cls) },
261
+ accept_expression: true
262
+ )
263
+
264
+ partials.each_with_object([]) do |result, out|
265
+ result.each do |key, val|
266
+ out.push prefilter_isas(key, val)
267
+ end
268
+ end
269
+ end
270
+
271
+ def new_authorized_query(actor, action, resource_class)
272
+ partials = partial_query(actor, action, resource_class)
273
+ types = host.serialize_types
274
+ class_name = class_to_name resource_class
275
+ plan = ffi.build_data_filter(types, partials, 'resource', class_name)
276
+ filter = ::Oso::Polar::Data::Filter.parse(self, plan)
277
+ host.adapter.build_query filter
278
+ end
279
+
280
+ def old_authorized_query(actor, action, resource_cls)
281
+ results = partial_query(actor, action, resource_cls)
282
+ ::Oso::Polar::DataFiltering::FilterPlan
283
+ .parse(self, results, class_to_name(resource_cls))
284
+ .build_query
285
+ end
286
+
287
+ # handle Isa constraints in a partial query
288
+ def prefilter_isas(key, val) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
289
+ # this will usually be the case! sometimes not, if it's an instance.
290
+ if val.is_a?(Expression) && val.operator == 'And'
291
+ # get the isas
292
+ isas, othas = val.args.partition do |expr|
293
+ expr.operator == 'Isa' &&
294
+ expr.args[1].is_a?(Pattern) &&
295
+ expr.args[1].fields.empty?
296
+ end
297
+
298
+ # drop all the isas we can verify now, keep everything else
299
+ othas += isas.reject do |isa|
300
+ isa.args[0].is_a? name_to_class isa.args[1].tag
301
+ end
302
+
303
+ # TODO(gw) check the rest of them instead of just adding them?
304
+ val.args = othas
305
+ end
306
+ val = host.to_polar val
307
+ { 'bindings' => { key => val } }
308
+ end
309
+
310
+ # get the (maybe user-supplied) name of a class.
311
+ # kind of a hack because of class autoreloading.
312
+ def class_to_name(klass) # rubocop:disable Metrics/AbcSize
313
+ if (rec = host.types[klass]) || (rec = host.types[klass.name])
314
+ rec.name
315
+ elsif (rec = host.types.values.find { |v| v.klass.get == klass })
316
+ host.types[klass] = rec
317
+ rec.name
318
+ else
319
+ raise NameError, "Unknown class `#{klass}`"
320
+ end
321
+ end
322
+
323
+ def try_class_to_name(klass)
324
+ class_to_name klass
325
+ rescue NameError
326
+ nil
327
+ end
328
+
262
329
  def type_constraint(var, cls)
263
330
  Expression.new(
264
331
  'And',
265
- [Expression.new('Isa', [var, Pattern.new(get_class_name(cls), {})])]
332
+ [Expression.new('Isa', [var, Pattern.new(class_to_name(cls), {})])]
266
333
  )
267
334
  end
268
335
 
@@ -94,8 +94,7 @@ module Oso
94
94
  else
95
95
  instance.__send__(attribute, *args, **kwargs)
96
96
  end
97
- result = JSON.dump(host.to_polar(result))
98
- call_result(result, call_id: call_id)
97
+ call_result(host.to_polar(result), call_id: call_id)
99
98
  rescue ArgumentError, NoMethodError => e
100
99
  application_error(e.message)
101
100
  call_result(nil, call_id: call_id)
@@ -143,7 +142,7 @@ module Oso
143
142
  calls[call_id] = value.lazy
144
143
  end
145
144
 
146
- result = JSON.dump(next_call_result(call_id))
145
+ result = next_call_result(call_id)
147
146
  call_result(result, call_id: call_id)
148
147
  rescue StopIteration
149
148
  call_result(nil, call_id: call_id)
@@ -245,12 +244,32 @@ module Oso
245
244
 
246
245
  def handle_relationship(call_id, instance, rel) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
247
246
  typ = host.types[rel.other_type]
248
- constraint = ::Oso::Polar::DataFiltering::Filter.new(
249
- kind: 'Eq',
250
- field: rel.other_field,
251
- value: instance.send(rel.my_field)
252
- )
253
- res = typ.exec_query[typ.build_query[[constraint]]]
247
+
248
+ if host.use_new_data_filtering?
249
+ cls = typ.klass.get
250
+
251
+ condition = ::Oso::Polar::Data::Filter::Condition.new(
252
+ ::Oso::Polar::Data::Filter::Projection.new(cls, rel.other_field),
253
+ 'Eq',
254
+ instance.send(rel.my_field)
255
+ )
256
+
257
+ filter = ::Oso::Polar::Data::Filter.new(
258
+ model: cls,
259
+ relations: [],
260
+ conditions: [[condition]],
261
+ types: host.types
262
+ )
263
+
264
+ res = host.adapter.execute_query host.adapter.build_query(filter)
265
+ else
266
+ constraint = ::Oso::Polar::DataFiltering::Filter.new(
267
+ kind: 'Eq',
268
+ field: rel.other_field,
269
+ value: instance.send(rel.my_field)
270
+ )
271
+ res = typ.exec_query[typ.build_query[[constraint]]]
272
+ end
254
273
 
255
274
  if rel.kind == 'one'
256
275
  raise "multiple parents: #{res}" unless res.length == 1
@@ -258,8 +277,7 @@ module Oso
258
277
  res = res[0]
259
278
  end
260
279
 
261
- res = JSON.dump host.to_polar res
262
- call_result(res, call_id: call_id)
280
+ call_result(host.to_polar(res), call_id: call_id)
263
281
  end
264
282
  end
265
283
  end
data/lib/oso/polar.rb CHANGED
@@ -11,6 +11,7 @@ require 'oso/polar/query'
11
11
  require 'oso/polar/query_event'
12
12
  require 'oso/polar/variable'
13
13
  require 'oso/polar/data_filtering'
14
+ require 'oso/polar/data'
14
15
 
15
16
  module Oso
16
17
  # Top-level namespace for Polar language library.
data/lib/oso/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oso
4
- VERSION = '0.22.1'
4
+ VERSION = '0.25.1'
5
5
  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.22.1
4
+ version: 0.25.1
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-01 00:00:00.000000000 Z
11
+ date: 2022-01-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -162,16 +162,18 @@ files:
162
162
  - lib/oso/errors.rb
163
163
  - lib/oso/oso.rb
164
164
  - lib/oso/polar.rb
165
+ - lib/oso/polar/data.rb
166
+ - lib/oso/polar/data/adapter.rb
167
+ - lib/oso/polar/data/adapter/active_record_adapter.rb
168
+ - lib/oso/polar/data/filter.rb
165
169
  - lib/oso/polar/data_filtering.rb
166
170
  - lib/oso/polar/errors.rb
167
171
  - lib/oso/polar/expression.rb
168
172
  - lib/oso/polar/ffi.rb
169
173
  - lib/oso/polar/ffi/error.rb
170
- - lib/oso/polar/ffi/message.rb
171
174
  - lib/oso/polar/ffi/polar.rb
172
175
  - lib/oso/polar/ffi/query.rb
173
- - lib/oso/polar/ffi/query_event.rb
174
- - lib/oso/polar/ffi/source.rb
176
+ - lib/oso/polar/ffi/rust_string.rb
175
177
  - lib/oso/polar/host.rb
176
178
  - lib/oso/polar/pattern.rb
177
179
  - lib/oso/polar/polar.rb
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Oso
6
- module Polar
7
- module FFI
8
- # Wrapper class for Message FFI pointer + operations.
9
- class Message < ::FFI::AutoPointer
10
- # @return [String]
11
- def to_s
12
- @to_s ||= read_string.force_encoding('UTF-8')
13
- end
14
-
15
- Rust = Module.new do
16
- extend ::FFI::Library
17
- ffi_lib FFI::LIB_PATH
18
-
19
- attach_function :free, :string_free, [Message], :int32
20
- end
21
-
22
- def process(enrich_message)
23
- message = JSON.parse(to_s)
24
- kind = message['kind']
25
- msg = message['msg']
26
- msg = enrich_message.call(msg)
27
-
28
- case kind
29
- when 'Print'
30
- puts(msg)
31
- when 'Warning'
32
- warn(format('[warning] %<msg>s', msg: msg))
33
- end
34
- end
35
-
36
- private_constant :Rust
37
- end
38
- end
39
- end
40
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Oso
4
- module Polar
5
- module FFI
6
- # Wrapper class for QueryEvent FFI pointer + operations.
7
- class QueryEvent < ::FFI::AutoPointer
8
- # @return [String]
9
- def to_s
10
- @to_s ||= read_string.force_encoding('UTF-8')
11
- end
12
-
13
- Rust = Module.new do
14
- extend ::FFI::Library
15
- ffi_lib FFI::LIB_PATH
16
-
17
- attach_function :free, :string_free, [FFI::QueryEvent], :int32
18
- end
19
- private_constant :Rust
20
- end
21
- end
22
- end
23
- end