oso-oso 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e511f86c5a330c1bb071824398085362def8ba7
4
- data.tar.gz: da3baaad23fb6e28c8ef0558bbf1deb659c03700
3
+ metadata.gz: 8c00a51f40789d269beb2d63e29ad12b76565d94
4
+ data.tar.gz: 8f66dfdb31532125fd3593ddcfb7c3e76fd81df5
5
5
  SHA512:
6
- metadata.gz: 9d55c689e8ffc0e3f7b533f54d3e622f77bd7f563182fc7b9dfc047a7a9b1554ed02711fbb8326f55fed76529dde2040d6f829bd4439a63773c9bb58ffb8c8a3
7
- data.tar.gz: 60b1c8f83803656373a108a64159636292bc91c1840292e7c01ea0c74961533d076ceaa6576010ecdf9dc062bc60c6b112e9900b726db227a322aa52c41f40e0
6
+ metadata.gz: 6ac0a230d40ecab86a7896249eb875fa43a16705a0594c9b55acd9d21215bff572ce1abb18da0593455f1a3c749354453808b779bed3dbf14e171f8b26b7511c
7
+ data.tar.gz: 910cccfb441f515b20bcd1c21fc02a3f293b4445ed4526e2b8a2a921d97062df23156bc2726587269139a79edd687bd8d5b9fa79776bbc5910045f2205e58923
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.2.5)
4
+ oso-oso (0.2.6)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
data/Makefile CHANGED
@@ -8,3 +8,6 @@ install: rust
8
8
 
9
9
  test: install
10
10
  bundle exec rake spec
11
+
12
+ repl:
13
+ bundle exec oso
data/bin/oso ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env -S bundle exec ruby
2
+ #-*-ruby-*-
3
+
4
+ require 'bundler/setup'
5
+ require 'oso'
6
+
7
+ Oso.new.repl(load: true)
Binary file
Binary file
@@ -1,47 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'polar/polar'
4
+
3
5
  module Oso
4
6
  # Oso authorization API.
5
- class Oso
7
+ class Oso < Polar::Polar
6
8
  def initialize
7
- @polar = ::Oso::Polar.new
9
+ super
8
10
  register_class(Http, name: 'Http')
9
11
  register_class(PathMapper, name: 'PathMapper')
10
12
  end
11
13
 
12
- def load_file(file)
13
- polar.load_file(file)
14
- end
15
-
16
- def load_str(str)
17
- polar.load_str(str)
18
- end
19
-
20
- def register_class(cls, name: nil) # rubocop:disable Naming/MethodParameterName
21
- if block_given?
22
- polar.register_class(cls, name: name, from_polar: Proc.new)
23
- else
24
- polar.register_class(cls, name: name)
25
- end
26
- end
27
-
28
14
  def allow(actor:, action:, resource:)
29
- polar.query_pred('allow', args: [actor, action, resource]).next
15
+ query_predicate('allow', actor, action, resource).next
30
16
  true
31
17
  rescue StopIteration
32
18
  false
33
19
  end
34
-
35
- def query_predicate(name, *args)
36
- polar.query_pred(name, args: args)
37
- end
38
-
39
- def load_queued_files
40
- polar.load_queued_files
41
- end
42
-
43
- private
44
-
45
- attr_reader :polar
46
20
  end
47
21
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'oso/polar/errors'
4
4
  require 'oso/polar/ffi'
5
+ require 'oso/polar/host'
5
6
  require 'oso/polar/polar'
6
7
  require 'oso/polar/predicate'
7
8
  require 'oso/polar/query'
@@ -40,6 +40,7 @@ module Oso
40
40
  class DuplicateInstanceRegistrationError < PolarRuntimeError; end
41
41
  class InvalidCallError < PolarRuntimeError; end
42
42
  class InvalidConstructorError < PolarRuntimeError; end
43
+ class InvalidQueryTypeError < PolarRuntimeError; end
43
44
  class InlineQueryFailedError < PolarRuntimeError; end
44
45
  class NullByteInPolarFileError < PolarRuntimeError; end
45
46
  class UnexpectedPolarTypeError < PolarRuntimeError; end
@@ -6,9 +6,11 @@ module Oso
6
6
  module Polar
7
7
  module FFI
8
8
  LIB = ::FFI::Platform::LIBPREFIX + 'polar.' + ::FFI::Platform::LIBSUFFIX
9
- LIB_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
10
- # @TODO: Fall back to this if there's no release build libs. Easier for dev.
11
- # LIB_PATH = File.expand_path(File.join(__dir__, "../../../../../target/debug/#{LIB}"))
9
+ RELEASE_PATH = File.expand_path(File.join(__dir__, "../../../ext/oso-oso/lib/#{LIB}"))
10
+ DEV_PATH = File.expand_path(File.join(__dir__, "../../../../../target/debug/#{LIB}"))
11
+ # If the lib exists in the ext/ dir, use it. Otherwise, fall back to
12
+ # checking the local Rust target dir.
13
+ LIB_PATH = File.file?(RELEASE_PATH) ? RELEASE_PATH : DEV_PATH
12
14
 
13
15
  # Wrapper classes defined upfront to fix Ruby loading issues. Actual
14
16
  # implementations live in the sibling `ffi/` directory and are `require`d
@@ -15,7 +15,6 @@ module Oso
15
15
  attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
16
16
  attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string], FFI::Query
17
17
  attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string], FFI::Query
18
- attach_function :new_query_from_repl, :polar_query_from_repl, [FFI::Polar], FFI::Query
19
18
  attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
20
19
  attach_function :free, :polar_free, [FFI::Polar], :int32
21
20
  end
@@ -76,15 +75,6 @@ module Oso
76
75
  query
77
76
  end
78
77
 
79
- # @return [FFI::Query]
80
- # @raise [FFI::Error] if the FFI call returns an error.
81
- def new_query_from_repl
82
- query = Rust.new_query_from_repl(self)
83
- raise FFI::Error.get if query.null?
84
-
85
- query
86
- end
87
-
88
78
  # @param name [String]
89
79
  # @param value [Hash<String, Object>]
90
80
  # @raise [FFI::Error] if the FFI call returns an error.
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ # Translate between Polar and the host language (Ruby).
6
+ class Host
7
+ protected
8
+ # @return [FFI::Polar]
9
+ attr_reader :ffi_polar
10
+ # @return [Hash<String, Class>]
11
+ attr_reader :classes
12
+ # @return [Hash<String, Object>]
13
+ attr_reader :constructors
14
+ # @return [Hash<Integer, Object>]
15
+ attr_reader :instances
16
+
17
+ public
18
+ def initialize(ffi_polar)
19
+ @ffi_polar = ffi_polar
20
+ @classes = {}
21
+ @constructors = {}
22
+ @instances = {}
23
+ end
24
+
25
+ def initialize_copy(other)
26
+ @ffi_polar = other.ffi_polar
27
+ @classes = other.classes.dup
28
+ @constructors = other.constructors.dup
29
+ @instances = other.instances.dup
30
+ end
31
+
32
+ # Fetch a Ruby class from the {#classes} cache.
33
+ #
34
+ # @param name [String]
35
+ # @return [Class]
36
+ # @raise [UnregisteredClassError] if the class has not been registered.
37
+ def get_class(name)
38
+ raise UnregisteredClassError, name unless classes.key? name
39
+
40
+ classes[name]
41
+ end
42
+
43
+ # Store a Ruby class in the {#classes} cache.
44
+ #
45
+ # @param cls [Class] the class to cache
46
+ # @param name [String] the name to cache the class as. Defaults to the name of the class.
47
+ # @return [String] the name the class is cached as.
48
+ # @raise [UnregisteredClassError] if the class has not been registered.
49
+ def cache_class(cls, name:, constructor:)
50
+ name = cls.name if name.nil?
51
+ raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
52
+
53
+ classes[name] = cls
54
+ if constructor.nil?
55
+ constructors[name] = :new
56
+ elsif constructor.respond_to? :call
57
+ constructors[name] = constructor
58
+ else
59
+ raise InvalidConstructorError
60
+ end
61
+ name
62
+ end
63
+
64
+ # Fetch a constructor from the {#constructors} cache.
65
+ #
66
+ # @param name [String]
67
+ # @return [Symbol] if constructor is the default of `:new`.
68
+ # @return [Proc] if a custom constructor was registered.
69
+ # @raise [UnregisteredConstructorError] if the constructor has not been registered.
70
+ def get_constructor(name)
71
+ raise MissingConstructorError, name unless constructors.key? name
72
+
73
+ constructors[name]
74
+ end
75
+
76
+ # Check if an instance has been cached.
77
+ #
78
+ # @param id [Integer]
79
+ # @return [Boolean]
80
+ def instance?(id)
81
+ case id
82
+ when Integer
83
+ instances.key? id
84
+ else
85
+ instances.value? id
86
+ end
87
+ end
88
+
89
+ # Fetch a Ruby instance from the {#instances} cache.
90
+ #
91
+ # @param id [Integer]
92
+ # @return [Object]
93
+ # @raise [UnregisteredInstanceError] if the ID has not been registered.
94
+ def get_instance(id)
95
+ raise UnregisteredInstanceError, id unless instance? id
96
+
97
+ instances[id]
98
+ end
99
+
100
+ # Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
101
+ #
102
+ # @param instance [Object]
103
+ # @param id [Integer]
104
+ # @return [Integer]
105
+ def cache_instance(instance, id: nil)
106
+ id = ffi_polar.new_id if id.nil?
107
+ instances[id] = instance
108
+ id
109
+ end
110
+
111
+ # Construct and cache a Ruby instance.
112
+ #
113
+ # @param cls_name [String]
114
+ # @param fields [Hash<String, Hash>]
115
+ # @param id [Integer]
116
+ # @raise [PolarRuntimeError] if instance construction fails.
117
+ def make_instance(cls_name, fields:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
118
+ constructor = get_constructor(cls_name)
119
+ fields = Hash[fields.map { |k, v| [k.to_sym, to_ruby(v)] }]
120
+ instance = if constructor == :new
121
+ if fields.empty?
122
+ get_class(cls_name).__send__(:new)
123
+ else
124
+ get_class(cls_name).__send__(:new, **fields)
125
+ end
126
+ elsif fields.empty?
127
+ constructor.call
128
+ else
129
+ constructor.call(**fields)
130
+ end
131
+ cache_instance(instance, id: id)
132
+ rescue StandardError => e
133
+ raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
134
+ end
135
+
136
+ # Check if the left class is more specific than the right class
137
+ # with respect to the given instance.
138
+ #
139
+ # @param instance_id [Integer]
140
+ # @param left_tag [String]
141
+ # @param right_tag [String]
142
+ # @return [Boolean]
143
+ def subspecializer?(instance_id, left_tag:, right_tag:)
144
+ mro = get_instance(instance_id).class.ancestors
145
+ mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
146
+ rescue StandardError
147
+ false
148
+ end
149
+
150
+ # Check if instance is an instance of class.
151
+ #
152
+ # @param instance_id [Integer]
153
+ # @param class_tag [String]
154
+ # @return [Boolean]
155
+ def isa?(instance_id, class_tag:)
156
+ instance = get_instance(instance_id)
157
+ cls = get_class(class_tag)
158
+ instance.is_a? cls
159
+ rescue PolarRuntimeError
160
+ false
161
+ end
162
+
163
+ # Check if two instances unify
164
+ #
165
+ # @param left_instance_id [Integer]
166
+ # @param right_instance_id [Integer]
167
+ # @return [Boolean]
168
+ def unify?(left_instance_id, right_instance_id)
169
+ left_instance = get_instance(left_instance_id)
170
+ right_instance = get_instance(right_instance_id)
171
+ left_instance == right_instance
172
+ rescue PolarRuntimeError
173
+ false
174
+ end
175
+
176
+ # Turn a Ruby value into a Polar term that's ready to be sent across the
177
+ # FFI boundary.
178
+ #
179
+ # @param value [Object]
180
+ # @return [Hash<String, Object>]
181
+ def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
182
+ value = case true # rubocop:disable Lint/LiteralAsCondition
183
+ when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
184
+ { 'Boolean' => value }
185
+ when value.instance_of?(Integer)
186
+ { 'Number' => { 'Integer' => value } }
187
+ when value.instance_of?(Float)
188
+ { 'Number' => { 'Float' => value } }
189
+ when value.instance_of?(String)
190
+ { 'String' => value }
191
+ when value.instance_of?(Array)
192
+ { 'List' => value.map { |el| to_polar_term(el) } }
193
+ when value.instance_of?(Hash)
194
+ { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
195
+ when value.instance_of?(Predicate)
196
+ { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
197
+ when value.instance_of?(Variable)
198
+ # This is supported so that we can query for unbound variables
199
+ { 'Variable' => value }
200
+ else
201
+ { 'ExternalInstance' => { 'instance_id' => cache_instance(value) } }
202
+ end
203
+ { 'value' => value }
204
+ end
205
+
206
+ # Turn a Polar term passed across the FFI boundary into a Ruby value.
207
+ #
208
+ # @param data [Hash<String, Object>]
209
+ # @option data [Integer] :id
210
+ # @option data [Integer] :offset Character offset of the term in its source string.
211
+ # @option data [Hash<String, Object>] :value
212
+ # @return [Object]
213
+ # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
214
+ def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
215
+ tag, value = data['value'].first
216
+ case tag
217
+ when 'String', 'Boolean'
218
+ value
219
+ when 'Number'
220
+ value.values.first
221
+ when 'List'
222
+ value.map { |el| to_ruby(el) }
223
+ when 'Dictionary'
224
+ value['fields'].transform_values { |v| to_ruby(v) }
225
+ when 'ExternalInstance'
226
+ get_instance(value['instance_id'])
227
+ when 'Call'
228
+ Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
229
+ else
230
+ raise UnexpectedPolarTypeError, tag
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
@@ -1,21 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'pp'
4
5
  require 'set'
5
6
 
6
7
  module Oso
7
8
  module Polar
8
9
  # Create and manage an instance of the Polar runtime.
9
- class Polar # rubocop:disable Metrics/ClassLength
10
+ class Polar
11
+ # @return [Host]
12
+ attr_reader :host
13
+
10
14
  def initialize
11
- @ffi_instance = FFI::Polar.create
12
- @calls = {}
13
- @classes = {}
14
- @constructors = {}
15
- @instances = {}
15
+ @ffi_polar = FFI::Polar.create
16
+ @host = Host.new(ffi_polar)
16
17
  @load_queue = Set.new
17
18
  end
18
19
 
20
+ # Replace the current Polar instance but retain all registered classes and constructors.
21
+ def clear
22
+ load_queue.clear
23
+ @ffi_polar = FFI::Polar.create
24
+ end
25
+
19
26
  # Enqueue a Polar policy file for loading into the KB.
20
27
  #
21
28
  # @param name [String]
@@ -38,23 +45,32 @@ module Oso
38
45
  def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
39
46
  raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
40
47
 
41
- ffi_instance.load_str(str, filename: filename)
48
+ ffi_polar.load_str(str, filename: filename)
42
49
  loop do
43
- next_query = ffi_instance.next_inline_query
50
+ next_query = ffi_polar.next_inline_query
44
51
  break if next_query.nil?
45
52
 
46
53
  begin
47
- Query.new(next_query, polar: self).results.next
54
+ Query.new(next_query, host: host).results.next
48
55
  rescue StopIteration
49
56
  raise InlineQueryFailedError
50
57
  end
51
58
  end
52
59
  end
53
60
 
54
- # Replace the current Polar instance but retain all registered classes and
55
- # constructors.
56
- def clear
57
- @ffi_instance = FFI::Polar.create
61
+ # Query for a predicate, parsing it if necessary.
62
+ def query(query)
63
+ load_queued_files
64
+ new_host = host.dup
65
+ case query
66
+ when String
67
+ ffi_query = ffi_polar.new_query_from_str(query)
68
+ when Predicate
69
+ ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
70
+ else
71
+ raise InvalidQueryTypeError
72
+ end
73
+ Query.new(ffi_query, host: new_host).results
58
74
  end
59
75
 
60
76
  # Query for a predicate.
@@ -62,245 +78,69 @@ module Oso
62
78
  # @param name [String]
63
79
  # @param args [Array<Object>]
64
80
  # @raise [Error] if the FFI call raises one.
65
- def query_pred(name, args:)
66
- clear_query_state
67
- load_queued_files
68
- pred = Predicate.new(name, args: args)
69
- query_ffi_instance = ffi_instance.new_query_from_term(to_polar_term(pred))
70
- Query.new(query_ffi_instance, polar: self).results
81
+ def query_predicate(name, *args)
82
+ query(Predicate.new(name, args: args))
71
83
  end
72
84
 
73
85
  # Start a REPL session.
74
86
  #
75
87
  # @raise [Error] if the FFI call raises one.
76
- def repl # rubocop:disable Metrics/MethodLength
77
- clear_query_state
88
+ def repl(load: false) # rubocop:disable Metrics/MethodLength
89
+ ARGV.map { |f| load_file(f) } if load
78
90
  load_queued_files
91
+
79
92
  loop do
80
- query = Query.new(ffi_instance.new_query_from_repl, polar: self)
81
- results = query.results.to_a
93
+ print('> ')
94
+ begin
95
+ query = STDIN.readline.chomp.chomp(';')
96
+ rescue EOFError
97
+ return
98
+ end
99
+
100
+ begin
101
+ ffi_query = ffi_polar.new_query_from_str(query)
102
+ rescue ParseError => e
103
+ puts("Parse error: " + e.to_s)
104
+ next
105
+ end
106
+
107
+ begin
108
+ results = Query.new(ffi_query, host: host).results.to_a
109
+ rescue PolarRuntimeError => e
110
+ puts(e.to_s)
111
+ next
112
+ end
113
+
82
114
  if results.empty?
83
- puts 'False'
115
+ pp false
84
116
  else
85
117
  results.each do |result|
86
- puts result
118
+ if result.empty?
119
+ pp true
120
+ else
121
+ pp result
122
+ end
87
123
  end
88
124
  end
89
125
  end
90
126
  end
91
127
 
92
- # Get a unique ID from Polar.
93
- #
94
- # @return [Integer]
95
- # @raise [Error] if the FFI call raises one.
96
- def new_id
97
- ffi_instance.new_id
98
- end
99
-
100
128
  # Register a Ruby class with Polar.
101
129
  #
102
130
  # @param cls [Class]
103
131
  # @param name [String]
104
132
  # @param from_polar [Proc]
105
133
  # @raise [InvalidConstructorError] if provided an invalid 'from_polar' constructor.
106
- def register_class(cls, name: nil, from_polar: nil) # rubocop:disable Naming/MethodParameterName
107
- # TODO(gj): should this take 3 args: cls (Class), constructor_cls
108
- # (Option<Class>) that defaults to cls, and constructor_method
109
- # (Option<Symbol>) that defaults to :new?
110
- name = cls.name if name.nil?
111
- raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
112
-
113
- classes[name] = cls
114
- if from_polar.nil?
115
- constructors[name] = :new
116
- elsif from_polar.respond_to? :call
117
- constructors[name] = from_polar
118
- else
119
- raise InvalidConstructorError
134
+ def register_class(cls, name: nil, from_polar: nil)
135
+ if block_given?
136
+ from_polar = Proc.new
120
137
  end
121
-
138
+ name = host.cache_class(cls, name: name, constructor: from_polar)
122
139
  register_constant(name, value: cls)
123
140
  end
124
141
 
125
142
  def register_constant(name, value:)
126
- ffi_instance.register_constant(name, value: to_polar_term(value))
127
- end
128
-
129
- # Register a Ruby method call, wrapping the call result in a generator if
130
- # it isn't already one.
131
- #
132
- # @param method [#to_sym]
133
- # @param call_id [Integer]
134
- # @param instance [Hash]
135
- # @param args [Array<Hash>]
136
- # @raise [InvalidCallError] if the method doesn't exist on the instance or
137
- # the args passed to the method are invalid.
138
- def register_call(method, call_id:, instance:, args:)
139
- return if calls.key?(call_id)
140
-
141
- args = args.map { |a| to_ruby(a) }
142
- if instance["value"].has_key? "ExternalInstance"
143
- instance_id = instance["value"]["ExternalInstance"]["instance_id"]
144
- instance = get_instance(instance_id)
145
- else
146
- instance = to_ruby(instance)
147
- end
148
- result = instance.__send__(method, *args)
149
- result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
150
- calls[call_id] = result.lazy
151
- rescue ArgumentError, NoMethodError
152
- raise InvalidCallError
153
- end
154
-
155
- # Retrieve the next result from a registered call and pass it to {#to_polar_term}.
156
- #
157
- # @param id [Integer]
158
- # @return [Hash]
159
- # @raise [StopIteration] if the call has been exhausted.
160
- def next_call_result(id)
161
- to_polar_term(calls[id].next)
162
- end
163
-
164
- # Construct and cache a Ruby instance.
165
- #
166
- # @param cls_name [String]
167
- # @param fields [Hash<String, Hash>]
168
- # @param id [Integer]
169
- # @raise [PolarRuntimeError] if instance construction fails.
170
- def make_instance(cls_name, fields:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
171
- constructor = get_constructor(cls_name)
172
- fields = Hash[fields.map { |k, v| [k.to_sym, to_ruby(v)] }]
173
- instance = if constructor == :new
174
- if fields.empty?
175
- get_class(cls_name).__send__(:new)
176
- else
177
- get_class(cls_name).__send__(:new, **fields)
178
- end
179
- elsif fields.empty?
180
- constructor.call
181
- else
182
- constructor.call(**fields)
183
- end
184
- cache_instance(instance, id: id)
185
- rescue StandardError => e
186
- raise PolarRuntimeError, "Error constructing instance of #{cls_name}: #{e}"
187
- end
188
-
189
- # Check if an instance has been cached.
190
- #
191
- # @param id [Integer]
192
- # @return [Boolean]
193
- def instance?(id)
194
- instances.key? id
195
- end
196
-
197
- # Fetch a Ruby instance from the {#instances} cache.
198
- #
199
- # @param id [Integer]
200
- # @return [Object]
201
- # @raise [UnregisteredInstanceError] if the ID has not been registered.
202
- def get_instance(id)
203
- raise UnregisteredInstanceError, id unless instance? id
204
-
205
- instances[id]
206
- end
207
-
208
- # Check if the left class is more specific than the right class for the
209
- # given instance.
210
- #
211
- # @param instance_id [Integer]
212
- # @param left_tag [String]
213
- # @param right_tag [String]
214
- # @return [Boolean]
215
- def subspecializer?(instance_id, left_tag:, right_tag:)
216
- mro = get_instance(instance_id).class.ancestors
217
- mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
218
- rescue StandardError
219
- false
220
- end
221
-
222
- # Check if instance is an instance of class.
223
- #
224
- # @param instance_id [Integer]
225
- # @param class_tag [String]
226
- # @return [Boolean]
227
- def isa?(instance_id, class_tag:)
228
- instance = get_instance(instance_id)
229
- cls = get_class(class_tag)
230
- instance.is_a? cls
231
- rescue PolarRuntimeError
232
- false
233
- end
234
-
235
- # Check if two instances unify
236
- #
237
- # @param left_instance_id [Integer]
238
- # @param right_instance_id [Integer]
239
- # @return [Boolean]
240
- def unify?(left_instance_id, right_instance_id)
241
- left_instance = get_instance(left_instance_id)
242
- right_instance = get_instance(right_instance_id)
243
- left_instance == right_instance
244
- rescue PolarRuntimeError
245
- false
246
- end
247
-
248
- # Turn a Ruby value into a Polar term that's ready to be sent across the
249
- # FFI boundary.
250
- #
251
- # @param value [Object]
252
- # @return [Hash<String, Object>]
253
- def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
254
- value = case true # rubocop:disable Lint/LiteralAsCondition
255
- when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
256
- { 'Boolean' => value }
257
- when value.instance_of?(Integer)
258
- { 'Number' => { 'Integer' => value } }
259
- when value.instance_of?(Float)
260
- { 'Number' => { 'Float' => value } }
261
- when value.instance_of?(String)
262
- { 'String' => value }
263
- when value.instance_of?(Array)
264
- { 'List' => value.map { |el| to_polar_term(el) } }
265
- when value.instance_of?(Hash)
266
- { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
267
- when value.instance_of?(Predicate)
268
- { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
269
- when value.instance_of?(Variable)
270
- # This is supported so that we can query for unbound variables
271
- { 'Variable' => value }
272
- else
273
- { 'ExternalInstance' => { 'instance_id' => cache_instance(value) } }
274
- end
275
- { 'value' => value }
276
- end
277
-
278
- # Turn a Polar term passed across the FFI boundary into a Ruby value.
279
- #
280
- # @param data [Hash<String, Object>]
281
- # @option data [Integer] :id
282
- # @option data [Integer] :offset Character offset of the term in its source string.
283
- # @option data [Hash<String, Object>] :value
284
- # @return [Object]
285
- # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
286
- def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
287
- tag, value = data['value'].first
288
- case tag
289
- when 'String', 'Boolean'
290
- value
291
- when 'Number'
292
- value.values.first
293
- when 'List'
294
- value.map { |el| to_ruby(el) }
295
- when 'Dictionary'
296
- value['fields'].transform_values { |v| to_ruby(v) }
297
- when 'ExternalInstance'
298
- get_instance(value['instance_id'])
299
- when 'Call'
300
- Predicate.new(value['name'], args: value['args'].map { |a| to_ruby(a) })
301
- else
302
- raise UnexpectedPolarTypeError, tag
303
- end
143
+ ffi_polar.register_constant(name, value: host.to_polar_term(value))
304
144
  end
305
145
 
306
146
  # Load all queued files, flushing the {#load_queue}.
@@ -313,69 +153,10 @@ module Oso
313
153
 
314
154
  private
315
155
 
316
- # @return [Hash<Integer, Enumerator>]
317
- attr_reader :calls
318
- # @return [Hash<String, Class>]
319
- attr_reader :classes
320
- # @return [Hash<String, Object>]
321
- attr_reader :constructors
322
156
  # @return [FFI::Polar]
323
- attr_reader :ffi_instance
324
- # @return [Hash<Integer, Object>]
325
- attr_reader :instances
157
+ attr_reader :ffi_polar
326
158
  # @return [Array<String>]
327
159
  attr_reader :load_queue
328
-
329
- # Clear the instance and call caches.
330
- def clear_query_state
331
- calls.clear
332
- instances.clear
333
- end
334
-
335
- # Query for a Polar string.
336
- #
337
- # @param str [String]
338
- # @return [Enumerator]
339
- def query_str(str)
340
- clear_query_state
341
- load_queued_files
342
- query_ffi_instance = ffi_instance.new_query_from_str(str)
343
- Query.new(query_ffi_instance, polar: self).results
344
- end
345
-
346
- # Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
347
- #
348
- # @param instance [Object]
349
- # @param id [Integer]
350
- # @return [Integer]
351
- def cache_instance(instance, id: nil)
352
- id = new_id if id.nil?
353
- instances[id] = instance
354
- id
355
- end
356
-
357
- # Fetch a Ruby class from the {#classes} cache.
358
- #
359
- # @param name [String]
360
- # @return [Class]
361
- # @raise [UnregisteredClassError] if the class has not been registered.
362
- def get_class(name)
363
- raise UnregisteredClassError, name unless classes.key? name
364
-
365
- classes[name]
366
- end
367
-
368
- # Fetch a constructor from the {#constructors} cache.
369
- #
370
- # @param name [String]
371
- # @return [Symbol] if constructor is the default of `:new`.
372
- # @return [Proc] if a custom constructor was registered.
373
- # @raise [UnregisteredConstructorError] if the constructor has not been registered.
374
- def get_constructor(name)
375
- raise MissingConstructorError, name unless constructors.key? name
376
-
377
- constructors[name]
378
- end
379
160
  end
380
161
  end
381
162
  end
@@ -6,20 +6,58 @@ module Oso
6
6
  class Query
7
7
  attr_reader :results
8
8
 
9
- # @param ffi_instance [FFI::Query]
10
- # @param polar [Polar]
11
- def initialize(ffi_instance, polar:)
12
- @ffi_instance = ffi_instance
13
- @polar = polar
9
+ # @param ffi_query [FFI::Query]
10
+ # @param ffi_polar [FFI::Polar]
11
+ def initialize(ffi_query, host:)
12
+ @calls = {}
13
+ @ffi_query = ffi_query
14
+ @host = host
14
15
  @results = start
15
16
  end
16
17
 
17
18
  private
18
19
 
20
+ # @return [Hash<Integer, Enumerator>]
21
+ attr_reader :calls
19
22
  # @return [FFI::Query]
20
- attr_reader :ffi_instance
21
- # @return [Polar]
22
- attr_reader :polar
23
+ attr_reader :ffi_query
24
+ # @return [Host]
25
+ attr_reader :host
26
+
27
+ # Send result of predicate check across FFI boundary.
28
+ #
29
+ # @param result [Boolean]
30
+ # @param call_id [Integer]
31
+ # @raise [Error] if the FFI call raises one.
32
+ def question_result(result, call_id:)
33
+ ffi_query.question_result(result, call_id: call_id)
34
+ end
35
+
36
+ # Register a Ruby method call, wrapping the call result in a generator if
37
+ # it isn't already one.
38
+ #
39
+ # @param method [#to_sym]
40
+ # @param call_id [Integer]
41
+ # @param instance [Hash]
42
+ # @param args [Array<Hash>]
43
+ # @raise [InvalidCallError] if the method doesn't exist on the instance or
44
+ # the args passed to the method are invalid.
45
+ def register_call(method, call_id:, instance:, args:)
46
+ return if calls.key?(call_id)
47
+
48
+ args = args.map { |a| host.to_ruby(a) }
49
+ if instance['value'].key? 'ExternalInstance'
50
+ instance_id = instance['value']['ExternalInstance']['instance_id']
51
+ instance = host.get_instance(instance_id)
52
+ else
53
+ instance = host.to_ruby(instance)
54
+ end
55
+ result = instance.__send__(method, *args)
56
+ result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
57
+ calls[call_id] = result.lazy
58
+ rescue ArgumentError, NoMethodError
59
+ raise InvalidCallError
60
+ end
23
61
 
24
62
  # Send next result of Ruby method call across FFI boundary.
25
63
  #
@@ -27,16 +65,16 @@ module Oso
27
65
  # @param call_id [Integer]
28
66
  # @raise [Error] if the FFI call raises one.
29
67
  def call_result(result, call_id:)
30
- ffi_instance.call_result(result, call_id: call_id)
68
+ ffi_query.call_result(result, call_id: call_id)
31
69
  end
32
70
 
33
- # Send result of predicate check across FFI boundary.
71
+ # Retrieve the next result from a registered call and pass it to {#to_polar_term}.
34
72
  #
35
- # @param result [Boolean]
36
- # @param call_id [Integer]
37
- # @raise [Error] if the FFI call raises one.
38
- def question_result(result, call_id:)
39
- ffi_instance.question_result(result, call_id: call_id)
73
+ # @param id [Integer]
74
+ # @return [Hash]
75
+ # @raise [StopIteration] if the call has been exhausted.
76
+ def next_call_result(id)
77
+ host.to_polar_term(calls[id].next)
40
78
  end
41
79
 
42
80
  # Fetch the next result from calling a Ruby method and prepare it for
@@ -48,8 +86,8 @@ module Oso
48
86
  # @param instance_id [Integer]
49
87
  # @raise [Error] if the FFI call raises one.
50
88
  def handle_call(method, call_id:, instance:, args:)
51
- polar.register_call(method, call_id: call_id, instance: instance, args: args)
52
- result = JSON.dump(polar.next_call_result(call_id))
89
+ register_call(method, call_id: call_id, instance: instance, args: args)
90
+ result = JSON.dump(next_call_result(call_id))
53
91
  call_result(result, call_id: call_id)
54
92
  rescue InvalidCallError, StopIteration
55
93
  call_result(nil, call_id: call_id)
@@ -65,19 +103,19 @@ module Oso
65
103
  def start # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
66
104
  Enumerator.new do |yielder| # rubocop:disable Metrics/BlockLength
67
105
  loop do # rubocop:disable Metrics/BlockLength
68
- event = ffi_instance.next_event
106
+ event = ffi_query.next_event
69
107
  case event.kind
70
108
  when 'Done'
71
109
  break
72
110
  when 'Result'
73
- yielder << event.data['bindings'].transform_values { |v| polar.to_ruby(v) }
111
+ yielder << event.data['bindings'].transform_values { |v| host.to_ruby(v) }
74
112
  when 'MakeExternal'
75
113
  id = event.data['instance_id']
76
- raise DuplicateInstanceRegistrationError, id if polar.instance? id
114
+ raise DuplicateInstanceRegistrationError, id if host.instance? id
77
115
 
78
116
  cls_name = event.data['instance']['tag']
79
117
  fields = event.data['instance']['fields']['fields']
80
- polar.make_instance(cls_name, fields: fields, id: id)
118
+ host.make_instance(cls_name, fields: fields, id: id)
81
119
  when 'ExternalCall'
82
120
  call_id = event.data['call_id']
83
121
  instance = event.data['instance']
@@ -88,24 +126,24 @@ module Oso
88
126
  instance_id = event.data['instance_id']
89
127
  left_tag = event.data['left_class_tag']
90
128
  right_tag = event.data['right_class_tag']
91
- answer = polar.subspecializer?(instance_id, left_tag: left_tag, right_tag: right_tag)
129
+ answer = host.subspecializer?(instance_id, left_tag: left_tag, right_tag: right_tag)
92
130
  question_result(answer, call_id: event.data['call_id'])
93
131
  when 'ExternalIsa'
94
132
  instance_id = event.data['instance_id']
95
133
  class_tag = event.data['class_tag']
96
- answer = polar.isa?(instance_id, class_tag: class_tag)
134
+ answer = host.isa?(instance_id, class_tag: class_tag)
97
135
  question_result(answer, call_id: event.data['call_id'])
98
136
  when 'ExternalUnify'
99
137
  left_instance_id = event.data['left_instance_id']
100
138
  right_instance_id = event.data['right_instance_id']
101
- answer = polar.unify?(left_instance_id, right_instance_id)
139
+ answer = host.unify?(left_instance_id, right_instance_id)
102
140
  question_result(answer, call_id: event.data['call_id'])
103
141
  when 'Debug'
104
142
  puts event.data['message'] if event.data['message']
105
143
  print '> '
106
144
  input = $stdin.gets.chomp!
107
- command = JSON.dump(polar.to_polar_term(input))
108
- ffi_instance.debug_command(command)
145
+ command = JSON.dump(host.to_polar_term(input))
146
+ ffi_query.debug_command(command)
109
147
  else
110
148
  raise "Unhandled event: #{JSON.dump(event.inspect)}"
111
149
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oso
4
- VERSION = '0.2.5'
4
+ VERSION = '0.2.6'
5
5
  end
@@ -18,11 +18,11 @@ Gem::Specification.new do |spec|
18
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
19
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
20
  files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
- files += Dir['ext/oso-oso/lib/*']
21
+ files + Dir['ext/oso-oso/lib/*']
22
22
  end
23
23
 
24
- spec.bindir = 'exe'
25
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.bindir = 'bin'
25
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
26
26
  spec.require_paths = ['lib']
27
27
 
28
28
  # Runtime dependencies
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.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oso Security
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-21 00:00:00.000000000 Z
11
+ date: 2020-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -97,7 +97,8 @@ dependencies:
97
97
  description:
98
98
  email:
99
99
  - support@osohq.com
100
- executables: []
100
+ executables:
101
+ - oso
101
102
  extensions: []
102
103
  extra_rdoc_files: []
103
104
  files:
@@ -109,8 +110,7 @@ files:
109
110
  - Makefile
110
111
  - README.md
111
112
  - Rakefile
112
- - bin/console
113
- - bin/setup
113
+ - bin/oso
114
114
  - ext/oso-oso/lib/libpolar.dylib
115
115
  - ext/oso-oso/lib/libpolar.so
116
116
  - ext/oso-oso/lib/polar.dll
@@ -125,6 +125,7 @@ files:
125
125
  - lib/oso/polar/ffi/polar.rb
126
126
  - lib/oso/polar/ffi/query.rb
127
127
  - lib/oso/polar/ffi/query_event.rb
128
+ - lib/oso/polar/host.rb
128
129
  - lib/oso/polar/polar.rb
129
130
  - lib/oso/polar/predicate.rb
130
131
  - lib/oso/polar/query.rb
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require 'bundler/setup'
5
- require 'oso'
6
-
7
- polar = Oso::Polar.new.tap do |p| # rubocop:disable Lint/UselessAssignment
8
- p.load_str('f(1); f(2); g(1); g(2); h(2); k(x) := f(x), h(x), g(x);')
9
- puts 'f(x)', p.send(:query_str, 'f(x)').to_a
10
- puts 'k(x)', p.send(:query_str, 'k(x)').to_a
11
-
12
- p.load_str('foo(1, 2); foo(3, 4); foo(5, 6);')
13
- expected = [{ 'x' => 1, 'y' => 2 }, { 'x' => 3, 'y' => 4 }, { 'x' => 5, 'y' => 6 }]
14
- raise 'AssertionError' if p.send(:query_str, 'foo(x, y)').to_a != expected
15
-
16
- class TestClass # rubocop:disable Style/Documentation
17
- def my_method
18
- 1
19
- end
20
- end
21
-
22
- p.register_class(TestClass)
23
-
24
- p.load_str('external(x, 3) := x = new TestClass{}.my_method;')
25
- results = p.send(:query_str, 'external(1, x)')
26
- p results.next
27
-
28
- # p.load_str('testDebug() := debug(), foo(x, y), k(y);')
29
- # p.send(:query_str, 'testDebug()').next
30
- end
31
-
32
- require 'pry'
33
- Pry.start
data/bin/setup DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install