oso-oso 0.4.0 → 0.7.0

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: 919322c6735cfeb3615da5dd85b4188c59e8716d
4
- data.tar.gz: 1c881b365f4098e3bbdad3152168e82ab31df87f
3
+ metadata.gz: 73b2001f3721fd450aaebcdf348ac8d05f1452f1
4
+ data.tar.gz: bf829788c21e9eb8b91ce05504800df3b13fd4fe
5
5
  SHA512:
6
- metadata.gz: b898b3636cdb1cfa5fe909634292cf030bd60ccf768e964cb258c7e1c076d34b52e953bf8b37b27e2cc42926c44efdbd7b600dc21fea63e7144834870d80db1f
7
- data.tar.gz: 1e38944c4a55b81b3dbecbf1864be035632bd840e0f709380c8b14bfaecf657a1abb273b74b171aae35df9210beddb7e42f4c3547af74e482d0db4b214ca987f
6
+ metadata.gz: 1590f8019ec1a5e13f8bb5b5f39a65a6e304d427312ea6996e41d6dfb56c0632a31d780dc55a9815734c7e3b6861d823a36878f46b26441d39c51296114b3086
7
+ data.tar.gz: 5dbcc691578bdcf01864895ab305aa9a97f7924e0f77a7ce33193dc047c99253f48f1ddfdfba65e290580ca6558132c6473911e5a54a5a64542e6f701f67796d
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-oso (0.4.0)
4
+ oso-oso (0.7.0)
5
5
  ffi (~> 1.0)
6
6
 
7
7
  GEM
@@ -49,19 +49,19 @@ GEM
49
49
  diff-lcs (>= 1.2.0, < 2.0)
50
50
  rspec-support (~> 3.9.0)
51
51
  rspec-support (3.9.3)
52
- rubocop (0.89.0)
52
+ rubocop (0.89.1)
53
53
  parallel (~> 1.10)
54
54
  parser (>= 2.7.1.1)
55
55
  rainbow (>= 2.2.2, < 4.0)
56
56
  regexp_parser (>= 1.7)
57
57
  rexml
58
- rubocop-ast (>= 0.1.0, < 1.0)
58
+ rubocop-ast (>= 0.3.0, < 1.0)
59
59
  ruby-progressbar (~> 1.7)
60
60
  unicode-display_width (>= 1.4.0, < 2.0)
61
61
  rubocop-ast (0.3.0)
62
62
  parser (>= 2.7.1.4)
63
63
  ruby-progressbar (1.10.1)
64
- solargraph (0.39.13)
64
+ solargraph (0.39.14)
65
65
  backport (~> 1.1)
66
66
  benchmark
67
67
  bundler (>= 1.17.2)
@@ -88,8 +88,8 @@ DEPENDENCIES
88
88
  pry-byebug (~> 3.9.0)
89
89
  rake (~> 12.0)
90
90
  rspec (~> 3.0)
91
- rubocop (~> 0.89.0)
92
- solargraph (~> 0.39.8)
91
+ rubocop (~> 0.89.1)
92
+ solargraph (~> 0.39.14)
93
93
  yard (~> 0.9.25)
94
94
 
95
95
  BUNDLED WITH
data/Makefile CHANGED
@@ -3,17 +3,17 @@
3
3
  rust:
4
4
  $(MAKE) -C ../.. rust-build
5
5
 
6
- install: rust
6
+ install:
7
7
  bundle install
8
8
 
9
- test: install
9
+ test: install rust
10
10
  bundle exec rake spec
11
11
 
12
- lint:
12
+ lint: install
13
13
  bundle exec rubocop
14
14
 
15
- typecheck:
15
+ typecheck: install
16
16
  bundle exec solargraph typecheck
17
17
 
18
- repl: install
18
+ repl: install rust
19
19
  bundle exec oso
data/bin/oso CHANGED
@@ -4,4 +4,4 @@
4
4
  require 'bundler/setup'
5
5
  require 'oso'
6
6
 
7
- Oso.new.repl(load: true)
7
+ Oso.new.repl(ARGV)
Binary file
Binary file
@@ -3,7 +3,7 @@
3
3
  require_relative 'polar/polar'
4
4
 
5
5
  module Oso
6
- # Oso authorization API.
6
+ # oso authorization API.
7
7
  class Oso < Polar::Polar
8
8
  def initialize
9
9
  super
@@ -11,6 +11,13 @@ module Oso
11
11
  register_class(PathMapper, name: 'PathMapper')
12
12
  end
13
13
 
14
+ # Query the knowledge base to determine whether an actor is allowed to
15
+ # perform an action upon a resource.
16
+ #
17
+ # @param actor [Object] Subject.
18
+ # @param action [Object] Verb.
19
+ # @param resource [Object] Object.
20
+ # @return [Boolean] An access control decision.
14
21
  def allowed?(actor:, action:, resource:)
15
22
  query_rule('allow', actor, action, resource).next
16
23
  true
@@ -27,36 +27,24 @@ module Oso
27
27
  class UnsupportedError < PolarRuntimeError; end
28
28
  class PolarTypeError < PolarRuntimeError; end
29
29
  class StackOverflowError < PolarRuntimeError; end
30
+ class FileLoadingError < PolarRuntimeError; end
30
31
 
31
32
  # Errors originating from this side of the FFI boundary.
32
33
 
33
34
  class UnregisteredClassError < PolarRuntimeError; end
34
- class MissingConstructorError < PolarRuntimeError; end
35
35
  class UnregisteredInstanceError < PolarRuntimeError; end
36
36
  class DuplicateInstanceRegistrationError < PolarRuntimeError; end
37
+
38
+ # TODO: I think this should probably have some arguments to say what the call is
37
39
  class InvalidCallError < PolarRuntimeError; end
38
40
  class InvalidConstructorError < PolarRuntimeError; end
39
41
  class InvalidQueryTypeError < PolarRuntimeError; end
40
- class InlineQueryFailedError < PolarRuntimeError; end
41
42
  class NullByteInPolarFileError < PolarRuntimeError; end
42
43
  class UnexpectedPolarTypeError < PolarRuntimeError; end
43
- class PolarFileAlreadyLoadedError < PolarRuntimeError # rubocop:disable Style/Documentation
44
- # @param file [String]
45
- def initialize(file)
46
- super("File #{file} has already been loaded.")
47
- end
48
- end
49
- class PolarFileContentsChangedError < PolarRuntimeError # rubocop:disable Style/Documentation
50
- # @param file [String]
51
- def initialize(file)
52
- super("A file with the name #{file}, but different contents, has already been loaded.")
53
- end
54
- end
55
- class PolarFileNameChangedError < PolarRuntimeError # rubocop:disable Style/Documentation
56
- # @param file [String]
57
- # @param existing [String]
58
- def initialize(file, existing)
59
- super("A file with the same contents as #{file} named #{existing} has already been loaded.")
44
+ class InlineQueryFailedError < PolarRuntimeError; # rubocop:disable Style/Documentation
45
+ # @param source [String]
46
+ def initialize(source)
47
+ super("Inline query failed: #{source}")
60
48
  end
61
49
  end
62
50
  class PolarFileExtensionError < PolarRuntimeError # rubocop:disable Style/Documentation
@@ -79,6 +67,12 @@ module Oso
79
67
  end
80
68
  end
81
69
 
70
+ class UnimplementedOperationError < PolarRuntimeError # rubocop:disable Style/Documentation
71
+ def initialize(operation)
72
+ super("#{operation} are unimplemented in the oso Ruby library")
73
+ end
74
+ end
75
+
82
76
  # Generic operational exception.
83
77
  class OperationalError < Error; end
84
78
  class UnknownError < OperationalError; end
@@ -37,7 +37,20 @@ module Oso
37
37
  # Wrapper class for Error FFI pointer + operations.
38
38
  class Error < ::FFI::AutoPointer
39
39
  def self.release(ptr)
40
- Rust.free(ptr)
40
+ Rust.free(ptr) unless ptr.null?
41
+ end
42
+ end
43
+ # Wrapper class for Message FFI pointer + operations.
44
+ class Message < ::FFI::AutoPointer
45
+ def self.release(ptr)
46
+ Rust.free(ptr) unless ptr.null?
47
+ end
48
+ end
49
+
50
+ # Wrapper class for Source FFI pointer.
51
+ class Source < ::FFI::AutoPointer
52
+ def self.release(ptr)
53
+ Rust.free(ptr) unless ptr.null?
41
54
  end
42
55
  end
43
56
  end
@@ -49,3 +62,5 @@ require 'oso/polar/ffi/polar'
49
62
  require 'oso/polar/ffi/query'
50
63
  require 'oso/polar/ffi/query_event'
51
64
  require 'oso/polar/ffi/error'
65
+ require 'oso/polar/ffi/message'
66
+ require 'oso/polar/ffi/source'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
@@ -83,6 +85,8 @@ module Oso
83
85
  ::Oso::Polar::PolarTypeError.new(msg, details: details)
84
86
  when 'StackOverflow'
85
87
  ::Oso::Polar::StackOverflowError.new(msg, details: details)
88
+ when 'FileLoading'
89
+ ::Oso::Polar::FileLoadingError.new(msg, details: details)
86
90
  else
87
91
  ::Oso::Polar::PolarRuntimeError.new(msg, details: details)
88
92
  end
@@ -0,0 +1,39 @@
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
23
+ message = JSON.parse(to_s)
24
+ kind = message['kind']
25
+ msg = message['msg']
26
+
27
+ case kind
28
+ when 'Print'
29
+ puts(msg)
30
+ when 'Warning'
31
+ warn(format('[warning] %<msg>s', msg: msg))
32
+ end
33
+ end
34
+
35
+ private_constant :Rust
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
@@ -10,12 +12,14 @@ module Oso
10
12
  ffi_lib FFI::LIB_PATH
11
13
 
12
14
  attach_function :new, :polar_new, [], FFI::Polar
13
- attach_function :load_str, :polar_load, [FFI::Polar, :string, :string], :int32
15
+ attach_function :load, :polar_load, [FFI::Polar, :string, :string], :int32
16
+ attach_function :clear_rules, :polar_clear_rules, [FFI::Polar], :int32
14
17
  attach_function :next_inline_query, :polar_next_inline_query, [FFI::Polar, :uint32], FFI::Query
15
18
  attach_function :new_id, :polar_get_external_id, [FFI::Polar], :uint64
16
19
  attach_function :new_query_from_str, :polar_new_query, [FFI::Polar, :string, :uint32], FFI::Query
17
20
  attach_function :new_query_from_term, :polar_new_query_from_term, [FFI::Polar, :string, :uint32], FFI::Query
18
21
  attach_function :register_constant, :polar_register_constant, [FFI::Polar, :string, :string], :int32
22
+ attach_function :next_message, :polar_next_polar_message, [FFI::Polar], FFI::Message
19
23
  attach_function :free, :polar_free, [FFI::Polar], :int32
20
24
  end
21
25
  private_constant :Rust
@@ -32,8 +36,17 @@ module Oso
32
36
  # @param src [String]
33
37
  # @param filename [String]
34
38
  # @raise [FFI::Error] if the FFI call returns an error.
35
- def load_str(src, filename: nil)
36
- raise FFI::Error.get if Rust.load_str(self, src, filename).zero?
39
+ def load(src, filename: nil)
40
+ loaded = Rust.load(self, src, filename)
41
+ process_messages
42
+ raise FFI::Error.get if loaded.zero?
43
+ end
44
+
45
+ # @raise [FFI::Error] if the FFI call returns an error.
46
+ def clear_rules
47
+ cleared = Rust.clear_rules(self)
48
+ process_messages
49
+ raise FFI::Error.get if cleared.zero?
37
50
  end
38
51
 
39
52
  # @return [FFI::Query] if there are remaining inline queries.
@@ -41,6 +54,7 @@ module Oso
41
54
  # @raise [FFI::Error] if the FFI call returns an error.
42
55
  def next_inline_query
43
56
  query = Rust.next_inline_query(self, 0)
57
+ process_messages
44
58
  query.null? ? nil : query
45
59
  end
46
60
 
@@ -60,6 +74,7 @@ module Oso
60
74
  # @raise [FFI::Error] if the FFI call returns an error.
61
75
  def new_query_from_str(str)
62
76
  query = Rust.new_query_from_str(self, str, 0)
77
+ process_messages
63
78
  raise FFI::Error.get if query.null?
64
79
 
65
80
  query
@@ -70,6 +85,7 @@ module Oso
70
85
  # @raise [FFI::Error] if the FFI call returns an error.
71
86
  def new_query_from_term(term)
72
87
  query = Rust.new_query_from_term(self, JSON.dump(term), 0)
88
+ process_messages
73
89
  raise FFI::Error.get if query.null?
74
90
 
75
91
  query
@@ -78,8 +94,22 @@ module Oso
78
94
  # @param name [String]
79
95
  # @param value [Hash<String, Object>]
80
96
  # @raise [FFI::Error] if the FFI call returns an error.
81
- def register_constant(name, value:)
82
- raise FFI::Error.get if Rust.register_constant(self, name, JSON.dump(value)).zero?
97
+ def register_constant(value, name:)
98
+ registered = Rust.register_constant(self, name, JSON.dump(value))
99
+ raise FFI::Error.get if registered.zero?
100
+ end
101
+
102
+ def next_message
103
+ Rust.next_message(self)
104
+ end
105
+
106
+ def process_messages
107
+ loop do
108
+ message = next_message
109
+ break if message.null?
110
+
111
+ message.process
112
+ end
83
113
  end
84
114
  end
85
115
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  module FFI
@@ -14,6 +16,8 @@ module Oso
14
16
  attach_function :question_result, :polar_question_result, [FFI::Query, :uint64, :int32], :int32
15
17
  attach_function :application_error, :polar_application_error, [FFI::Query, :string], :int32
16
18
  attach_function :next_event, :polar_next_query_event, [FFI::Query], FFI::QueryEvent
19
+ attach_function :next_message, :polar_next_query_message, [FFI::Query], FFI::Message
20
+ attach_function :source, :polar_query_source_info, [FFI::Query], FFI::Source
17
21
  attach_function :free, :query_free, [FFI::Query], :int32
18
22
  end
19
23
  private_constant :Rust
@@ -22,6 +26,7 @@ module Oso
22
26
  # @raise [FFI::Error] if the FFI call returns an error.
23
27
  def debug_command(cmd)
24
28
  res = Rust.debug_command(self, cmd)
29
+ process_messages
25
30
  raise FFI::Error.get if res.zero?
26
31
  end
27
32
 
@@ -54,10 +59,33 @@ module Oso
54
59
  # @raise [FFI::Error] if the FFI call returns an error.
55
60
  def next_event
56
61
  event = Rust.next_event(self)
62
+ process_messages
57
63
  raise FFI::Error.get if event.null?
58
64
 
59
65
  ::Oso::Polar::QueryEvent.new(JSON.parse(event.to_s))
60
66
  end
67
+
68
+ def next_message
69
+ Rust.next_message(self)
70
+ end
71
+
72
+ def process_messages
73
+ loop do
74
+ message = next_message
75
+ break if message.null?
76
+
77
+ message.process
78
+ end
79
+ end
80
+
81
+ # @return [String]
82
+ # @raise [FFI::Error] if the FFI call returns an error.
83
+ def source
84
+ res = Rust.source(self)
85
+ raise FFI::Error.get if res.null?
86
+
87
+ res.to_s
88
+ end
61
89
  end
62
90
  end
63
91
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Oso
4
+ module Polar
5
+ module FFI
6
+ # Wrapper class for Source FFI pointer.
7
+ class Source < ::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, [Message], :int32
18
+ end
19
+
20
+ private_constant :Rust
21
+ end
22
+ end
23
+ end
24
+ end
@@ -10,8 +10,6 @@ module Oso
10
10
  attr_reader :ffi_polar
11
11
  # @return [Hash<String, Class>]
12
12
  attr_reader :classes
13
- # @return [Hash<String, Object>]
14
- attr_reader :constructors
15
13
  # @return [Hash<Integer, Object>]
16
14
  attr_reader :instances
17
15
 
@@ -20,14 +18,12 @@ module Oso
20
18
  def initialize(ffi_polar)
21
19
  @ffi_polar = ffi_polar
22
20
  @classes = {}
23
- @constructors = {}
24
21
  @instances = {}
25
22
  end
26
23
 
27
24
  def initialize_copy(other)
28
25
  @ffi_polar = other.ffi_polar
29
26
  @classes = other.classes.dup
30
- @constructors = other.constructors.dup
31
27
  @instances = other.instances.dup
32
28
  end
33
29
 
@@ -44,38 +40,19 @@ module Oso
44
40
 
45
41
  # Store a Ruby class in the {#classes} cache.
46
42
  #
47
- # @param cls [Class] the class to cache
48
- # @param name [String] the name to cache the class as. Defaults to the name of the class.
43
+ # @param cls [Class] the class to cache.
44
+ # @param name [String] the name to cache the class as.
49
45
  # @return [String] the name the class is cached as.
50
- # @raise [UnregisteredClassError] if the class has not been registered.
51
- def cache_class(cls, name:, constructor:) # rubocop:disable Metrics/MethodLength
52
- name = cls.name if name.nil?
46
+ # @raise [DuplicateClassAliasError] if attempting to register a class
47
+ # under a previously-registered name.
48
+ def cache_class(cls, name:)
53
49
  raise DuplicateClassAliasError, name: name, old: get_class(name), new: cls if classes.key? name
54
50
 
55
51
  classes[name] = cls
56
- if constructor.nil?
57
- constructors[name] = :new
58
- elsif constructor.respond_to? :call
59
- constructors[name] = constructor
60
- else
61
- raise InvalidConstructorError
62
- end
63
52
  name
64
53
  end
65
54
 
66
- # Fetch a constructor from the {#constructors} cache.
67
- #
68
- # @param name [String]
69
- # @return [Symbol] if constructor is the default of `:new`.
70
- # @return [Proc] if a custom constructor was registered.
71
- # @raise [UnregisteredConstructorError] if the constructor has not been registered.
72
- def get_constructor(name)
73
- raise MissingConstructorError, name unless constructors.key? name
74
-
75
- constructors[name]
76
- end
77
-
78
- # Check if an instance has been cached.
55
+ # Check if an instance exists in the {#instances} cache.
79
56
  #
80
57
  # @param id [Integer]
81
58
  # @return [Boolean]
@@ -99,11 +76,12 @@ module Oso
99
76
  instances[id]
100
77
  end
101
78
 
102
- # Cache a Ruby instance, fetching a {#new_id} if one isn't provided.
79
+ # Cache a Ruby instance in the {#instances} cache, fetching a new id if
80
+ # one isn't provided.
103
81
  #
104
82
  # @param instance [Object]
105
- # @param id [Integer]
106
- # @return [Integer]
83
+ # @param id [Integer] the instance ID. Generated via FFI if not provided.
84
+ # @return [Integer] the instance ID.
107
85
  def cache_instance(instance, id: nil)
108
86
  id = ffi_polar.new_id if id.nil?
109
87
  instances[id] = instance
@@ -112,30 +90,17 @@ module Oso
112
90
 
113
91
  # Construct and cache a Ruby instance.
114
92
  #
115
- # @param cls_name [String]
116
- # @param initargs [Hash<String, Hash>]
117
- # @param id [Integer]
93
+ # @param cls_name [String] name of the instance's class.
94
+ # @param args [Array<Object>] positional args to the constructor.
95
+ # @param kwargs [Hash<String, Object>] keyword args to the constructor.
96
+ # @param id [Integer] the instance ID.
118
97
  # @raise [PolarRuntimeError] if instance construction fails.
119
- def make_instance(cls_name, initargs:, id:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
120
- constructor = get_constructor(cls_name)
121
- instance = if constructor == :new
122
- if initargs.empty?
123
- get_class(cls_name).__send__(:new)
124
- elsif initargs.is_a? Array
125
- get_class(cls_name).__send__(:new, *initargs)
126
- elsif initargs.is_a? Hash
127
- get_class(cls_name).__send__(:new, **initargs)
128
- else
129
- raise PolarRuntimeError, "Bad initargs: #{initargs}"
130
- end
131
- elsif initargs.empty?
132
- constructor.call
133
- elsif initargs.is_a? Array
134
- constructor.call(*initargs)
135
- elsif initargs.is_a? Hash
136
- constructor.call(**initargs)
98
+ # @return [Integer] the instance ID.
99
+ def make_instance(cls_name, args:, kwargs:, id:)
100
+ instance = if kwargs.empty? # This check is for Ruby < 2.7.
101
+ get_class(cls_name).__send__(:new, *args)
137
102
  else
138
- raise PolarRuntimeError, "Bad initargs: #{initargs}"
103
+ get_class(cls_name).__send__(:new, *args, **kwargs)
139
104
  end
140
105
  cache_instance(instance, id: id)
141
106
  rescue StandardError => e
@@ -151,9 +116,9 @@ module Oso
151
116
  # @return [Boolean]
152
117
  def subspecializer?(instance_id, left_tag:, right_tag:)
153
118
  mro = get_instance(instance_id).class.ancestors
154
- mro.index(get_class(left_tag)) < mro.index(get_class(right_tag))
155
- rescue StandardError
156
- false
119
+ left_index = mro.index(get_class(left_tag))
120
+ right_index = mro.index(get_class(right_tag))
121
+ left_index && right_index && left_index < right_index
157
122
  end
158
123
 
159
124
  # Check if instance is an instance of class.
@@ -165,8 +130,6 @@ module Oso
165
130
  instance = to_ruby(instance)
166
131
  cls = get_class(class_tag)
167
132
  instance.is_a? cls
168
- rescue PolarRuntimeError
169
- false
170
133
  end
171
134
 
172
135
  # Check if two instances unify
@@ -178,8 +141,6 @@ module Oso
178
141
  left_instance = get_instance(left_instance_id)
179
142
  right_instance = get_instance(right_instance_id)
180
143
  left_instance == right_instance
181
- rescue PolarRuntimeError
182
- false
183
144
  end
184
145
 
185
146
  # Turn a Ruby value into a Polar term that's ready to be sent across the
@@ -187,22 +148,29 @@ module Oso
187
148
  #
188
149
  # @param value [Object]
189
150
  # @return [Hash<String, Object>]
190
- def to_polar_term(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
151
+ def to_polar(value) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
191
152
  value = case true # rubocop:disable Lint/LiteralAsCondition
192
153
  when value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
193
154
  { 'Boolean' => value }
194
155
  when value.instance_of?(Integer)
195
156
  { 'Number' => { 'Integer' => value } }
196
157
  when value.instance_of?(Float)
158
+ if value == Float::INFINITY
159
+ value = 'Infinity'
160
+ elsif value == -Float::INFINITY
161
+ value = '-Infinity'
162
+ elsif value.nan?
163
+ value = 'NaN'
164
+ end
197
165
  { 'Number' => { 'Float' => value } }
198
166
  when value.instance_of?(String)
199
167
  { 'String' => value }
200
168
  when value.instance_of?(Array)
201
- { 'List' => value.map { |el| to_polar_term(el) } }
169
+ { 'List' => value.map { |el| to_polar(el) } }
202
170
  when value.instance_of?(Hash)
203
- { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar_term(v) } } }
171
+ { 'Dictionary' => { 'fields' => value.transform_values { |v| to_polar(v) } } }
204
172
  when value.instance_of?(Predicate)
205
- { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar_term(el) } } }
173
+ { 'Call' => { 'name' => value.name, 'args' => value.args.map { |el| to_polar(el) } } }
206
174
  when value.instance_of?(Variable)
207
175
  # This is supported so that we can query for unbound variables
208
176
  { 'Variable' => value }
@@ -220,13 +188,28 @@ module Oso
220
188
  # @option data [Hash<String, Object>] :value
221
189
  # @return [Object]
222
190
  # @raise [UnexpectedPolarTypeError] if type cannot be converted to Ruby.
223
- def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
191
+ def to_ruby(data) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
224
192
  tag, value = data['value'].first
225
193
  case tag
226
194
  when 'String', 'Boolean'
227
195
  value
228
196
  when 'Number'
229
- value.values.first
197
+ num = value.values.first
198
+ if value.key? 'Float'
199
+ case num
200
+ when 'Infinity'
201
+ return Float::INFINITY
202
+ when '-Infinity'
203
+ return -Float::INFINITY
204
+ when 'NaN'
205
+ return Float::NAN
206
+ else
207
+ unless value['Float'].is_a? Float # rubocop:disable Metrics/BlockNesting
208
+ raise PolarRuntimeError, "Expected a floating point number, got \"#{value['Float']}\""
209
+ end
210
+ end
211
+ end
212
+ num
230
213
  when 'List'
231
214
  value.map { |el| to_ruby(el) }
232
215
  when 'Dictionary'
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'pp'
5
- require 'set'
6
- require 'digest/md5'
7
-
8
3
  # Missing Ruby type.
9
4
  module PolarBoolean; end
10
5
  # Monkey-patch Ruby true type.
@@ -12,6 +7,26 @@ class TrueClass; include PolarBoolean; end
12
7
  # Monkey-patch Ruby false type.
13
8
  class FalseClass; include PolarBoolean; end
14
9
 
10
+ # https://github.com/ruby/ruby/blob/bb9ecd026a6cadd5d0f85ac061649216806ed935/lib/bundler/vendor/thor/lib/thor/shell/color.rb#L99-L105
11
+ def supports_color
12
+ $stdout.tty? && $stderr.tty? && ENV['NO_COLOR'].nil?
13
+ end
14
+
15
+ if supports_color
16
+ RESET = "\x1b[0m"
17
+ FG_BLUE = "\x1b[34m"
18
+ FG_RED = "\x1b[31m"
19
+ else
20
+ RESET = ''
21
+ FG_BLUE = ''
22
+ FG_RED = ''
23
+ end
24
+
25
+ def print_error(error)
26
+ warn FG_RED + error.class.name.split('::').last + RESET
27
+ warn error.message
28
+ end
29
+
15
30
  module Oso
16
31
  module Polar
17
32
  # Create and manage an instance of the Polar runtime.
@@ -22,8 +37,6 @@ module Oso
22
37
  def initialize
23
38
  @ffi_polar = FFI::Polar.create
24
39
  @host = Host.new(ffi_polar)
25
- @loaded_names = {}
26
- @loaded_contents = {}
27
40
 
28
41
  # Register built-in classes.
29
42
  register_class PolarBoolean, name: 'Boolean'
@@ -34,35 +47,25 @@ module Oso
34
47
  register_class String
35
48
  end
36
49
 
37
- # Replace the current Polar instance but retain all registered classes and constructors.
38
- def clear
39
- loaded_names.clear
40
- loaded_contents.clear
41
- @ffi_polar = FFI::Polar.create
50
+ # Clear all rules and rule sources from the current Polar instance
51
+ #
52
+ # @return [self] for chaining.
53
+ def clear_rules
54
+ ffi_polar.clear_rules
55
+ self
42
56
  end
43
57
 
44
- # Enqueue a Polar policy file for loading into the KB.
58
+ # Load a Polar policy file.
45
59
  #
46
60
  # @param name [String]
47
61
  # @raise [PolarFileExtensionError] if provided filename has invalid extension.
48
62
  # @raise [PolarFileNotFoundError] if provided filename does not exist.
49
- def load_file(name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
63
+ # @return [self] for chaining.
64
+ def load_file(name)
50
65
  raise PolarFileExtensionError, name unless File.extname(name) == '.polar'
51
66
 
52
67
  file_data = File.open(name, &:read)
53
- hash = Digest::MD5.hexdigest(file_data)
54
-
55
- if loaded_names.key?(name)
56
- raise PolarFileAlreadyLoadedError, name if loaded_names[name] == hash
57
-
58
- raise PolarFileContentsChangedError, name
59
- elsif loaded_contents.key?(hash)
60
- raise PolarFileNameChangedError, name, loaded_contents[hash]
61
- else
62
- load_str(file_data, filename: name)
63
- loaded_names[name] = hash
64
- loaded_contents[hash] = name
65
- end
68
+ load_str(file_data, filename: name)
66
69
  rescue Errno::ENOENT
67
70
  raise PolarFileNotFoundError, name
68
71
  end
@@ -74,10 +77,11 @@ module Oso
74
77
  # @raise [NullByteInPolarFileError] if str includes a non-terminating null byte.
75
78
  # @raise [InlineQueryFailedError] on the first failed inline query.
76
79
  # @raise [Error] if any of the FFI calls raise one.
80
+ # @return [self] for chaining.
77
81
  def load_str(str, filename: nil) # rubocop:disable Metrics/MethodLength
78
82
  raise NullByteInPolarFileError if str.chomp("\0").include?("\0")
79
83
 
80
- ffi_polar.load_str(str, filename: filename)
84
+ ffi_polar.load(str, filename: filename)
81
85
  loop do
82
86
  next_query = ffi_polar.next_inline_query
83
87
  break if next_query.nil?
@@ -85,12 +89,13 @@ module Oso
85
89
  begin
86
90
  Query.new(next_query, host: host).results.next
87
91
  rescue StopIteration
88
- raise InlineQueryFailedError
92
+ raise InlineQueryFailedError, next_query.source
89
93
  end
90
94
  end
95
+ self
91
96
  end
92
97
 
93
- # Query for a predicate, parsing it if necessary.
98
+ # Query for a Polar predicate or string.
94
99
  #
95
100
  # @overload query(query)
96
101
  # @param query [String]
@@ -106,7 +111,7 @@ module Oso
106
111
  when String
107
112
  ffi_query = ffi_polar.new_query_from_str(query)
108
113
  when Predicate
109
- ffi_query = ffi_polar.new_query_from_term(new_host.to_polar_term(query))
114
+ ffi_query = ffi_polar.new_query_from_term(new_host.to_polar(query))
110
115
  else
111
116
  raise InvalidQueryTypeError
112
117
  end
@@ -123,72 +128,109 @@ module Oso
123
128
  query(Predicate.new(name, args: args))
124
129
  end
125
130
 
131
+ # Register a Ruby class with Polar.
132
+ #
133
+ # @param cls [Class] the class to register.
134
+ # @param name [String] the name to register the class as. Defaults to the name of the class.
135
+ # @raise [DuplicateClassAliasError] if attempting to register a class
136
+ # under a previously-registered name.
137
+ # @raise [FFI::Error] if the FFI call returns an error.
138
+ # @return [self] for chaining.
139
+ def register_class(cls, name: nil)
140
+ name = host.cache_class(cls, name: name || cls.name)
141
+ register_constant(cls, name: name)
142
+ end
143
+
144
+ # Register a Ruby object with Polar.
145
+ #
146
+ # @param value [Object] the object to register.
147
+ # @param name [String] the name to register the object as.
148
+ # @return [self] for chaining.
149
+ # @raise [FFI::Error] if the FFI call returns an error.
150
+ def register_constant(value, name:)
151
+ ffi_polar.register_constant(host.to_polar(value), name: name)
152
+ self
153
+ end
154
+
126
155
  # Start a REPL session.
127
156
  #
157
+ # @param files [Array<String>]
128
158
  # @raise [Error] if the FFI call raises one.
129
- def repl(load: false) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
130
- ARGV.map { |f| load_file(f) } if load
159
+ def repl(files = [])
160
+ files.map { |f| load_file(f) }
161
+ prompt = "#{FG_BLUE}query>#{RESET} "
162
+ # Try loading the readline module from the Ruby stdlib. If we get a
163
+ # LoadError, fall back to the standard REPL with no readline support.
164
+ require 'readline'
165
+ repl_readline(prompt)
166
+ rescue LoadError
167
+ repl_standard(prompt)
168
+ end
131
169
 
132
- loop do # rubocop:disable Metrics/BlockLength
133
- print 'query> '
134
- begin
135
- query = $stdin.readline.chomp.chomp(';')
136
- rescue EOFError
137
- return
138
- end
170
+ private
139
171
 
140
- begin
141
- ffi_query = ffi_polar.new_query_from_str(query)
142
- rescue ParseError => e
143
- puts "Parse error: #{e}"
144
- next
145
- end
172
+ # @return [FFI::Polar]
173
+ attr_reader :ffi_polar
146
174
 
147
- begin
148
- results = Query.new(ffi_query, host: host).results.to_a
149
- rescue PolarRuntimeError => e
150
- puts e
175
+ # The R and L in REPL for systems where readline is available.
176
+ def repl_readline(prompt)
177
+ while (buf = Readline.readline(prompt, true))
178
+ if /^\s*$/ =~ buf # Don't add empty entries to history.
179
+ Readline::HISTORY.pop
151
180
  next
152
181
  end
182
+ process_line(buf)
183
+ end
184
+ rescue Interrupt # rubocop:disable Lint/SuppressedException
185
+ end
153
186
 
154
- if results.empty?
155
- pp false
156
- else
157
- results.each do |result|
158
- if result.empty?
159
- pp true
160
- else
161
- pp result
162
- end
163
- end
187
+ # The R and L in REPL for systems where readline is not available.
188
+ def repl_standard(prompt)
189
+ loop do
190
+ puts prompt
191
+ begin
192
+ buf = $stdin.readline
193
+ rescue EOFError
194
+ return
164
195
  end
196
+ process_line(buf)
165
197
  end
198
+ rescue Interrupt # rubocop:disable Lint/SuppressedException
166
199
  end
167
200
 
168
- # Register a Ruby class with Polar.
201
+ # Process a line of user input in the REPL.
169
202
  #
170
- # @param cls [Class]
171
- # @param name [String]
172
- # @param from_polar [Proc]
173
- # @raise [InvalidConstructorError] if provided an invalid 'from_polar' constructor.
174
- def register_class(cls, name: nil, from_polar: nil)
175
- from_polar = Proc.new if block_given?
176
- name = host.cache_class(cls, name: name, constructor: from_polar)
177
- register_constant(name, value: cls)
178
- end
179
-
180
- def register_constant(name, value:)
181
- ffi_polar.register_constant(name, value: host.to_polar_term(value))
182
- end
203
+ # @param buf [String]
204
+ def process_line(buf) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
205
+ query = buf.chomp.chomp(';')
206
+ begin
207
+ ffi_query = ffi_polar.new_query_from_str(query)
208
+ rescue ParseError => e
209
+ print_error(e)
210
+ return
211
+ end
183
212
 
184
- private
213
+ begin
214
+ results = Query.new(ffi_query, host: host).results.to_a
215
+ rescue PolarRuntimeError => e
216
+ print_error(e)
217
+ return
218
+ end
185
219
 
186
- # @return [FFI::Polar]
187
- attr_reader :ffi_polar
188
- # @return [Hash<String, String>]
189
- attr_reader :loaded_names
190
- # @return [Hash<String, String>]
191
- attr_reader :loaded_contents
220
+ if results.empty?
221
+ puts false
222
+ else
223
+ results.each do |result|
224
+ if result.empty?
225
+ puts true
226
+ else
227
+ result.each do |variable, value|
228
+ puts "#{variable} = #{value.inspect}"
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
192
234
  end
193
235
  end
194
236
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Oso
4
6
  module Polar
5
7
  # A single Polar query.
@@ -43,12 +45,18 @@ module Oso
43
45
  # @param args [Array<Hash>]
44
46
  # @raise [InvalidCallError] if the method doesn't exist on the instance or
45
47
  # the args passed to the method are invalid.
46
- def register_call(method, call_id:, instance:, args:)
48
+ def register_call(attribute, call_id:, instance:, args:, kwargs:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
47
49
  return if calls.key?(call_id)
48
50
 
49
- args = args.map { |a| host.to_ruby(a) }
50
51
  instance = host.to_ruby(instance)
51
- result = instance.__send__(method, *args)
52
+ args = args.map { |a| host.to_ruby(a) }
53
+ kwargs = Hash[kwargs.map { |k, v| [k.to_sym, host.to_ruby(v)] }]
54
+ # The kwargs.empty? check is for Ruby < 2.7.
55
+ result = if kwargs.empty?
56
+ instance.__send__(attribute, *args)
57
+ else
58
+ instance.__send__(attribute, *args, **kwargs)
59
+ end
52
60
  result = [result].to_enum unless result.is_a? Enumerator # Call must be a generator.
53
61
  calls[call_id] = result.lazy
54
62
  rescue ArgumentError, NoMethodError
@@ -64,13 +72,13 @@ module Oso
64
72
  ffi_query.call_result(result, call_id: call_id)
65
73
  end
66
74
 
67
- # Retrieve the next result from a registered call and pass it to {#to_polar_term}.
75
+ # Retrieve the next result from a registered call and pass it to {#to_polar}.
68
76
  #
69
77
  # @param id [Integer]
70
78
  # @return [Hash]
71
79
  # @raise [StopIteration] if the call has been exhausted.
72
80
  def next_call_result(id)
73
- host.to_polar_term(calls[id].next)
81
+ host.to_polar(calls[id].next)
74
82
  end
75
83
 
76
84
  # Send application error across FFI boundary.
@@ -89,8 +97,8 @@ module Oso
89
97
  # @param call_id [Integer]
90
98
  # @param instance [Hash<String, Object>]
91
99
  # @raise [Error] if the FFI call raises one.
92
- def handle_call(method, call_id:, instance:, args:)
93
- register_call(method, call_id: call_id, instance: instance, args: args)
100
+ def handle_call(attribute, call_id:, instance:, args:, kwargs:)
101
+ register_call(attribute, call_id: call_id, instance: instance, args: args, kwargs: kwargs)
94
102
  result = JSON.dump(next_call_result(call_id))
95
103
  call_result(result, call_id: call_id)
96
104
  rescue InvalidCallError => e
@@ -100,6 +108,20 @@ module Oso
100
108
  call_result(nil, call_id: call_id)
101
109
  end
102
110
 
111
+ def handle_make_external(data) # rubocop:disable Metrics/AbcSize
112
+ id = data['instance_id']
113
+ raise DuplicateInstanceRegistrationError, id if host.instance? id
114
+
115
+ constructor = data['constructor']['value']
116
+ raise InvalidConstructorError unless constructor.key? 'Call'
117
+
118
+ cls_name = constructor['Call']['name']
119
+ args = constructor['Call']['args'].map { |arg| host.to_ruby(arg) }
120
+ kwargs = constructor['Call']['kwargs'] || {}
121
+ kwargs = Hash[kwargs.map { |k, v| [k.to_sym, host.to_ruby(v)] }]
122
+ host.make_instance(cls_name, args: args, kwargs: kwargs, id: id)
123
+ end
124
+
103
125
  # Create a generator that can be polled to advance the query loop.
104
126
  #
105
127
  # @yieldparam [Hash<String, Object>]
@@ -115,27 +137,14 @@ module Oso
115
137
  when 'Result'
116
138
  yielder << event.data['bindings'].transform_values { |v| host.to_ruby(v) }
117
139
  when 'MakeExternal'
118
- id = event.data['instance_id']
119
- raise DuplicateInstanceRegistrationError, id if host.instance? id
120
-
121
- constructor = event.data['constructor']['value']
122
- if constructor.key? 'InstanceLiteral'
123
- cls_name = constructor['InstanceLiteral']['tag']
124
- fields = constructor['InstanceLiteral']['fields']['fields']
125
- initargs = Hash[fields.map { |k, v| [k.to_sym, host.to_ruby(v)] }]
126
- elsif constructor.key? 'Call'
127
- cls_name = constructor['Call']['name']
128
- initargs = constructor['Call']['args'].map { |arg| host.to_ruby(arg) }
129
- else
130
- raise InvalidConstructorError
131
- end
132
- host.make_instance(cls_name, initargs: initargs, id: id)
140
+ handle_make_external(event.data)
133
141
  when 'ExternalCall'
134
142
  call_id = event.data['call_id']
135
143
  instance = event.data['instance']
136
- method = event.data['attribute']
137
- args = event.data['args']
138
- handle_call(method, call_id: call_id, instance: instance, args: args)
144
+ attribute = event.data['attribute']
145
+ args = event.data['args'] || []
146
+ kwargs = event.data['kwargs'] || {}
147
+ handle_call(attribute, call_id: call_id, instance: instance, args: args, kwargs: kwargs)
139
148
  when 'ExternalIsSubSpecializer'
140
149
  instance_id = event.data['instance_id']
141
150
  left_tag = event.data['left_class_tag']
@@ -160,8 +169,10 @@ module Oso
160
169
  rescue EOFError
161
170
  next
162
171
  end
163
- command = JSON.dump(host.to_polar_term(input))
172
+ command = JSON.dump(host.to_polar(input))
164
173
  ffi_query.debug_command(command)
174
+ when 'ExternalOp'
175
+ raise UnimplementedOperationError, 'comparison operators'
165
176
  else
166
177
  raise "Unhandled event: #{JSON.dump(event.inspect)}"
167
178
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Oso
4
- VERSION = '0.4.0'
4
+ VERSION = '0.7.0'
5
5
  end
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency 'pry-byebug', '~> 3.9.0'
35
35
  spec.add_development_dependency 'rake', '~> 12.0'
36
36
  spec.add_development_dependency 'rspec', '~> 3.0'
37
- spec.add_development_dependency 'rubocop', '~> 0.89.0'
38
- spec.add_development_dependency 'solargraph', '~> 0.39.8'
37
+ spec.add_development_dependency 'rubocop', '~> 0.89.1'
38
+ spec.add_development_dependency 'solargraph', '~> 0.39.14'
39
39
  spec.add_development_dependency 'yard', '~> 0.9.25'
40
40
  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.4.0
4
+ version: 0.7.0
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: 2020-08-11 00:00:00.000000000 Z
11
+ date: 2020-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -72,28 +72,28 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 0.89.0
75
+ version: 0.89.1
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 0.89.0
82
+ version: 0.89.1
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: solargraph
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 0.39.8
89
+ version: 0.39.14
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 0.39.8
96
+ version: 0.39.14
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: yard
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -137,9 +137,11 @@ files:
137
137
  - lib/oso/polar/errors.rb
138
138
  - lib/oso/polar/ffi.rb
139
139
  - lib/oso/polar/ffi/error.rb
140
+ - lib/oso/polar/ffi/message.rb
140
141
  - lib/oso/polar/ffi/polar.rb
141
142
  - lib/oso/polar/ffi/query.rb
142
143
  - lib/oso/polar/ffi/query_event.rb
144
+ - lib/oso/polar/ffi/source.rb
143
145
  - lib/oso/polar/host.rb
144
146
  - lib/oso/polar/polar.rb
145
147
  - lib/oso/polar/predicate.rb