oso-cloud 0.7.0 → 1.0.1

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
  SHA256:
3
- metadata.gz: 69149698f1269f5bd98bf3072671992b364170e0ff5264132fde524f10d997b2
4
- data.tar.gz: ccd52aa2087dfadffc11dce6343aaad7abf10ec3cb2099a8b01df8f19f3e88d4
3
+ metadata.gz: 339079e696596a482f6f1fe6bc875d9f88dc5415b0eaca4b4e7a04304280cc7a
4
+ data.tar.gz: '099c6b0532405a7fc0cf5933b09ea03991fe43f3e2da6b726d5e33e932706b55'
5
5
  SHA512:
6
- metadata.gz: 2cc987575b2ef6ff3b7bdff48b575c23f1a6739ba5553a44f00731700e03b89a854d71bac3fd60b41bf76854d9b38fb9b380ee2eb7b4abdc7a0338abcb48b3ed
7
- data.tar.gz: afa3922a254311adf4616372c15b3c51c834339d89c5cc3942c86d261e12121d919b8879dd9494e28cb71b7f318f060924e1852cae28e36e7fcf385c70591cb1
6
+ metadata.gz: 441e11c7fdb4b201cf22d84195d7f0cc5454a64186c0078086ae085138a31a8ee10f667f5723b94b467aa74ee89780e1ccd853c435d044f84616b46f78a44527
7
+ data.tar.gz: 56c4cb7d88820805bbd9238624f8220253c48aa19fdffe71298af26ff9369e3bf5692d49be70f439a582e4f76ff9d4b7458f45a303fa32c02296c5c2324f9f08
data/Gemfile.lock CHANGED
@@ -1,13 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-cloud (0.7.0)
4
+ oso-cloud (1.0.1)
5
+ faraday (~> 2.5.2)
6
+ faraday-retry (~> 2.0.0)
5
7
 
6
8
  GEM
7
9
  remote: https://rubygems.org/
8
10
  specs:
11
+ faraday (2.5.2)
12
+ faraday-net_http (>= 2.0, < 3.1)
13
+ ruby2_keywords (>= 0.0.4)
14
+ faraday-net_http (3.0.2)
15
+ faraday-retry (2.0.0)
16
+ faraday (~> 2.0)
9
17
  minitest (5.15.0)
10
18
  rake (12.3.3)
19
+ ruby2_keywords (0.0.5)
11
20
 
12
21
  PLATFORMS
13
22
  ruby
data/README.md CHANGED
@@ -1,25 +1,53 @@
1
- # Oso::Client
1
+ # Oso Cloud Client for Ruby
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/oso/client`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ [![Slack][badge-slack]][badge-slack-link]
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ The Oso Cloud client for Ruby provides a convenient wrapper around the Oso
6
+ Cloud HTTP API for applications and services written in Ruby.
6
7
 
7
- ## Installation
8
+ ## What is Oso Cloud?
9
+ Oso Cloud is authorization-as-a-service. It provides abstractions for building
10
+ and iterating on authorization in your application – based on years of work
11
+ with hundreds of engineering teams.
8
12
 
9
- Add this line to your application's Gemfile:
13
+ - Model: Build your authorization model using primitives for common patterns
14
+ like multi-tenancy and RBAC. Express custom rules using Polar, a
15
+ declarative policy language for authorization.
10
16
 
11
- ```ruby
12
- gem 'oso-cloud'
13
- ```
17
+ - Store: Store your authorization data using a best-practices data model and
18
+ use it for access decisions across all of your services.
14
19
 
15
- And then execute:
20
+ - Enforce & Query: Add enforcement calls to your application to perform
21
+ yes/no permission checks, filter resources by permissions, list a user's
22
+ roles, and show/hide pieces of your UI.
16
23
 
17
- $ bundle install
24
+ - Test & Watch: Write tests over your authorization policies before you push
25
+ them live. See logs of authorization decisions in real time.
18
26
 
19
- Or install it yourself as:
27
+ For more information on how Oso Cloud works and how it fits into your
28
+ architecture, check out the
29
+ [introduction](https://www.osohq.com/docs/get-started/what-is-oso-cloud).
20
30
 
21
- $ gem install oso-cloud
31
+ ## Documentation
32
+ - To get up and running with Oso Cloud, try the
33
+ [Quickstart guide](https://www.osohq.com/docs/get-started/quickstart).
34
+ - For method-level documentation, see the
35
+ [Ruby Client API documentation](https://www.osohq.com/docs/reference/client-apis/ruby).
36
+ - Full documentation is available at
37
+ [osohq.com/docs](https://www.osohq.com/docs).
38
+ - To learn about authorization best practices (not specific to Oso), read the
39
+ [Authorization Academy](https://www.osohq.com/developers/authorization-academy)
40
+ guides.
22
41
 
23
- ## Usage
42
+ ## Community & Support
43
+
44
+ If you have any questions on Oso Cloud or authorization more generally, you can
45
+ join our engineering team & hundreds of other developers using Oso in our
46
+ community Slack:
47
+
48
+ [![Button][join-slack-link]][badge-slack-link]
49
+
50
+ [join-slack-link]: https://user-images.githubusercontent.com/282595/128394344-1bd9e5b2-e83d-4666-b446-2e4f431ffcea.png
51
+ [badge-slack]: https://img.shields.io/badge/slack-oso--oss-orange
52
+ [badge-slack-link]: https://join-slack.osohq.com/
24
53
 
25
- TODO: Write usage instructions here
data/lib/oso/api.rb ADDED
@@ -0,0 +1,458 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'faraday'
4
+ require 'faraday/retry'
5
+
6
+ require 'oso/helpers'
7
+
8
+ module OsoCloud
9
+ # @!visibility private
10
+ module Core
11
+ # @!visibility private
12
+ class ApiResult
13
+ attr_reader :message
14
+
15
+ def initialize(message:)
16
+ @message = message
17
+ end
18
+ end
19
+
20
+ # @!visibility private
21
+ class ApiError < StandardError
22
+ def initialize(message:)
23
+ super(message)
24
+ end
25
+ end
26
+
27
+ # @!visibility private
28
+ class Policy
29
+ attr_reader :filename
30
+ attr_reader :src
31
+
32
+ def initialize(filename:, src:)
33
+ @filename = filename
34
+ @src = src
35
+ end
36
+ end
37
+
38
+ # @!visibility private
39
+ class GetPolicyResult
40
+ attr_reader :policy
41
+
42
+ def initialize(policy:)
43
+ if policy.is_a? Policy
44
+ @policy = policy
45
+ else
46
+ @policy = Policy.new(**policy)
47
+ end
48
+ end
49
+ end
50
+
51
+ # @!visibility private
52
+ class Fact
53
+ attr_reader :predicate
54
+ attr_reader :args
55
+
56
+ def initialize(predicate:, args:)
57
+ @predicate = predicate
58
+ @args = args.map { |v| if v.is_a? Value then v else Value.new(**v) end }
59
+ end
60
+ end
61
+
62
+ # @!visibility private
63
+ class Value
64
+ attr_reader :type
65
+ attr_reader :id
66
+
67
+ def initialize(type:, id:)
68
+ @type = type
69
+ @id = id
70
+ end
71
+ end
72
+
73
+ # @!visibility private
74
+ class Bulk
75
+ attr_reader :delete
76
+ attr_reader :tell
77
+
78
+ def initialize(delete:, tell:)
79
+ @delete = delete.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
80
+ @tell = tell.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
81
+ end
82
+ end
83
+
84
+ # @!visibility private
85
+ class AuthorizeResult
86
+ attr_reader :allowed
87
+
88
+ def initialize(allowed:)
89
+ @allowed = allowed
90
+ end
91
+ end
92
+
93
+ # @!visibility private
94
+ class AuthorizeQuery
95
+ attr_reader :actor_type
96
+ attr_reader :actor_id
97
+ attr_reader :action
98
+ attr_reader :resource_type
99
+ attr_reader :resource_id
100
+ attr_reader :context_facts
101
+
102
+ def initialize(actor_type:, actor_id:, action:, resource_type:, resource_id:, context_facts:)
103
+ @actor_type = actor_type
104
+ @actor_id = actor_id
105
+ @action = action
106
+ @resource_type = resource_type
107
+ @resource_id = resource_id
108
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
109
+ end
110
+ end
111
+
112
+ # @!visibility private
113
+ class AuthorizeResourcesResult
114
+ attr_reader :results
115
+
116
+ def initialize(results:)
117
+ @results = results.map { |v| if v.is_a? Value then v else Value.new(**v) end }
118
+ end
119
+ end
120
+
121
+ # @!visibility private
122
+ class AuthorizeResourcesQuery
123
+ attr_reader :actor_type
124
+ attr_reader :actor_id
125
+ attr_reader :action
126
+ attr_reader :resources
127
+ attr_reader :context_facts
128
+
129
+ def initialize(actor_type:, actor_id:, action:, resources:, context_facts:)
130
+ @actor_type = actor_type
131
+ @actor_id = actor_id
132
+ @action = action
133
+ @resources = resources.map { |v| if v.is_a? Value then v else Value.new(**v) end }
134
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
135
+ end
136
+ end
137
+
138
+ # @!visibility private
139
+ class ListResult
140
+ attr_reader :results
141
+
142
+ def initialize(results:)
143
+ @results = results
144
+ end
145
+ end
146
+
147
+ # @!visibility private
148
+ class ListQuery
149
+ attr_reader :actor_type
150
+ attr_reader :actor_id
151
+ attr_reader :action
152
+ attr_reader :resource_type
153
+ attr_reader :context_facts
154
+
155
+ def initialize(actor_type:, actor_id:, action:, resource_type:, context_facts:)
156
+ @actor_type = actor_type
157
+ @actor_id = actor_id
158
+ @action = action
159
+ @resource_type = resource_type
160
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
161
+ end
162
+ end
163
+
164
+ # @!visibility private
165
+ class ActionsResult
166
+ attr_reader :results
167
+
168
+ def initialize(results:)
169
+ @results = results
170
+ end
171
+ end
172
+
173
+ # @!visibility private
174
+ class ActionsQuery
175
+ attr_reader :actor_type
176
+ attr_reader :actor_id
177
+ attr_reader :resource_type
178
+ attr_reader :resource_id
179
+ attr_reader :context_facts
180
+
181
+ def initialize(actor_type:, actor_id:, resource_type:, resource_id:, context_facts:)
182
+ @actor_type = actor_type
183
+ @actor_id = actor_id
184
+ @resource_type = resource_type
185
+ @resource_id = resource_id
186
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
187
+ end
188
+ end
189
+
190
+ # @!visibility private
191
+ class QueryResult
192
+ attr_reader :results
193
+
194
+ def initialize(results:)
195
+ @results = results.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
196
+ end
197
+ end
198
+
199
+ # @!visibility private
200
+ class Query
201
+ attr_reader :fact
202
+ attr_reader :context_facts
203
+
204
+ def initialize(fact:, context_facts:)
205
+ if fact.is_a? Fact
206
+ @fact = fact
207
+ else
208
+ @fact = Fact.new(**fact)
209
+ end
210
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
211
+ end
212
+ end
213
+
214
+ # @!visibility private
215
+ class StatsResult
216
+ attr_reader :num_roles
217
+ attr_reader :num_relations
218
+ attr_reader :num_facts
219
+
220
+ def initialize(num_roles:, num_relations:, num_facts:)
221
+ @num_roles = num_roles
222
+ @num_relations = num_relations
223
+ @num_facts = num_facts
224
+ end
225
+ end
226
+
227
+
228
+ # @!visibility private
229
+ class Api
230
+ def initialize(url: 'https://cloud.osohq.com', api_key: nil, options: nil)
231
+ @url = url
232
+ @connection = Faraday.new(url: url) do |faraday|
233
+ faraday.request :json
234
+
235
+ # responses are processed in reverse order; this stack implies the
236
+ # retries are attempted before an error is raised, and the json
237
+ # parser is only applied if there are no errors
238
+ faraday.response :json, preserve_raw: true
239
+ faraday.response :raise_error
240
+ faraday.request :retry, {
241
+ max: (options && options[:max_retries]) || 10,
242
+ interval: 0.01,
243
+ interval_randomness: 0.005,
244
+ max_interval: 1,
245
+ backoff_factor: 2,
246
+ retry_statuses: [429, 500, 502, 503, 504],
247
+ # ensure authorize and related check functions are retried because
248
+ # they are POST requests, which are not retried automatically
249
+ retry_if: ->(env, _exc) {
250
+ %w[
251
+ /api/authorize
252
+ /api/authorize_resources
253
+ /api/list
254
+ /api/actions
255
+ /api/query
256
+ ].include? env.url.path
257
+ },
258
+ }
259
+
260
+ if (options && options[:test_adapter])
261
+ faraday.adapter :test do |stub|
262
+ stub.post(options[:test_adapter][:path]) do |env|
263
+ options[:test_adapter][:func].call
264
+ end
265
+ stub.get(options[:test_adapter][:path]) do |env|
266
+ options[:test_adapter][:func].call
267
+ end
268
+ stub.delete(options[:test_adapter][:path]) do |env|
269
+ options[:test_adapter][:func].call
270
+ end
271
+ end
272
+ else
273
+ faraday.adapter :net_http
274
+ end
275
+ end
276
+ @api_key = api_key
277
+ end
278
+
279
+ def get_policy()
280
+ params = {}
281
+ data = nil
282
+ url = "/policy"
283
+ result = GET(url, params, data)
284
+ GetPolicyResult.new(**result)
285
+ end
286
+
287
+ def post_policy(data)
288
+ params = {}
289
+ data = OsoCloud::Helpers.to_hash(data)
290
+ url = "/policy"
291
+ result = POST(url, params, data)
292
+ ApiResult.new(**result)
293
+ end
294
+
295
+ def post_facts(data)
296
+ params = {}
297
+ data = OsoCloud::Helpers.to_hash(data)
298
+ url = "/facts"
299
+ result = POST(url, params, data)
300
+ Fact.new(**result)
301
+ end
302
+
303
+ def delete_facts(data)
304
+ params = {}
305
+ data = OsoCloud::Helpers.to_hash(data)
306
+ url = "/facts"
307
+ result = DELETE(url, params, data)
308
+ ApiResult.new(**result)
309
+ end
310
+
311
+ def post_bulk_load(data)
312
+ params = {}
313
+ data = OsoCloud::Helpers.to_hash(data)
314
+ url = "/bulk_load"
315
+ result = POST(url, params, data)
316
+ ApiResult.new(**result)
317
+ end
318
+
319
+ def post_bulk_delete(data)
320
+ params = {}
321
+ data = OsoCloud::Helpers.to_hash(data)
322
+ url = "/bulk_delete"
323
+ result = POST(url, params, data)
324
+ ApiResult.new(**result)
325
+ end
326
+
327
+ def post_bulk(data)
328
+ params = {}
329
+ data = OsoCloud::Helpers.to_hash(data)
330
+ url = "/bulk"
331
+ result = POST(url, params, data)
332
+ ApiResult.new(**result)
333
+ end
334
+
335
+ def post_authorize(data)
336
+ params = {}
337
+ data = OsoCloud::Helpers.to_hash(data)
338
+ url = "/authorize"
339
+ result = POST(url, params, data)
340
+ AuthorizeResult.new(**result)
341
+ end
342
+
343
+ def post_authorize_resources(data)
344
+ params = {}
345
+ data = OsoCloud::Helpers.to_hash(data)
346
+ url = "/authorize_resources"
347
+ result = POST(url, params, data)
348
+ AuthorizeResourcesResult.new(**result)
349
+ end
350
+
351
+ def post_list(data)
352
+ params = {}
353
+ data = OsoCloud::Helpers.to_hash(data)
354
+ url = "/list"
355
+ result = POST(url, params, data)
356
+ ListResult.new(**result)
357
+ end
358
+
359
+ def post_actions(data)
360
+ params = {}
361
+ data = OsoCloud::Helpers.to_hash(data)
362
+ url = "/actions"
363
+ result = POST(url, params, data)
364
+ ActionsResult.new(**result)
365
+ end
366
+
367
+ def post_query(data)
368
+ params = {}
369
+ data = OsoCloud::Helpers.to_hash(data)
370
+ url = "/query"
371
+ result = POST(url, params, data)
372
+ QueryResult.new(**result)
373
+ end
374
+
375
+ def get_stats()
376
+ params = {}
377
+ data = nil
378
+ url = "/stats"
379
+ result = GET(url, params, data)
380
+ StatsResult.new(**result)
381
+ end
382
+
383
+ def clear_data()
384
+ params = {}
385
+ data = nil
386
+ url = "/clear_data"
387
+ result = POST(url, params, data)
388
+ ApiResult.new(**result)
389
+ end
390
+
391
+
392
+ # hard-coded, not generated
393
+ def get_facts(predicate, args)
394
+ params = {}
395
+ params["predicate"] = predicate
396
+ args.each_with_index do |arg, i|
397
+ arg_query = OsoCloud::Helpers.extract_arg_query(arg)
398
+ if arg_query
399
+ params["args.#{i}.type"] = arg_query.type
400
+ params["args.#{i}.id"] = arg_query.id
401
+ end
402
+ end
403
+ data = nil
404
+ url = "/facts"
405
+ result = GET(url, params, data)
406
+ result.map { |v| Fact.new(**v) }
407
+ end
408
+
409
+ def headers()
410
+ {
411
+ "Authorization" => "Bearer %s" % @api_key,
412
+ "User-Agent" => "Oso Cloud (ruby)",
413
+ "Accept": "application/json",
414
+ "Content-Type": "application/json",
415
+ "X-OsoApiVersion": "0"
416
+ }
417
+ end
418
+
419
+ def GET(path, params, body)
420
+ response = @connection.get("api#{path}", params, headers )
421
+ handle_faraday_response response
422
+ rescue Faraday::Error => error
423
+ handle_faraday_error error
424
+ end
425
+
426
+ def POST(path, params, body)
427
+ response = @connection.post("api#{path}", body, headers) do |req|
428
+ req.params = params
429
+ end
430
+ handle_faraday_response response
431
+ rescue Faraday::Error => error
432
+ handle_faraday_error error
433
+ end
434
+
435
+ def DELETE(path, params, body)
436
+ response = @connection.delete("api#{path}", params, headers) do |req|
437
+ req.body = body
438
+ end
439
+ handle_faraday_response response
440
+ rescue Faraday::Error => error
441
+ handle_faraday_error error
442
+ end
443
+
444
+ def handle_faraday_response(response)
445
+ # TODO:(@patrickod) refactor duplicative JSON parsing
446
+ JSON.parse(response.env[:raw_body], symbolize_names: true)
447
+ end
448
+
449
+ def handle_faraday_error(error)
450
+ err = JSON.parse(error.response[:body], symbolize_names: true)
451
+ raise ApiError.new(**err)
452
+ rescue JSON::ParserError => e
453
+ raise ApiError.new(message: e.message)
454
+ end
455
+ end
456
+
457
+ end
458
+ end
@@ -0,0 +1,59 @@
1
+ module OsoCloud
2
+ # @!visibility private
3
+ module Helpers
4
+ # @!visibility private
5
+ def self.extract_value(x)
6
+ return OsoCloud::Core::Value.new(type: "String", id: x) if x.is_a? String
7
+
8
+ return nil if x.nil?
9
+
10
+ type = (x.type.nil? ? nil : x.type.to_s)
11
+ id = (x.id.nil? ? nil : x.id.to_s)
12
+ OsoCloud::Core::Value.new(type: type, id: id)
13
+ end
14
+
15
+ # @!visibility private
16
+ def self.extract_arg_query(x)
17
+ self.extract_value(x)
18
+ end
19
+
20
+ # @!visibility private
21
+ def self.param_to_fact(predicate, args)
22
+ OsoCloud::Core::Fact.new(predicate: predicate, args: args.map { |a| self.extract_value(a) })
23
+ end
24
+
25
+ # @!visibility private
26
+ def self.params_to_facts(facts)
27
+ facts.map { |predicate, *args| self.param_to_fact(predicate, args) }
28
+ end
29
+
30
+ def self.from_value(value)
31
+ if value.id.nil?
32
+ if value.type.nil?
33
+ nil
34
+ else
35
+ { type: value.type }
36
+ end
37
+ else
38
+ if value.type == "String"
39
+ value.id
40
+ else
41
+ { id: value.id, type: value.type }
42
+ end
43
+ end
44
+ end
45
+
46
+ # @!visibility private
47
+ def self.to_hash(o)
48
+ return o.map { |v| self.to_hash(v) } if o.is_a? Array
49
+ return o if o.instance_variables.empty?
50
+ hash = {}
51
+ o.instance_variables.each { |var|
52
+ v = var.to_s.delete("@")
53
+ value = o.send(v)
54
+ hash[v] = self.to_hash(value)
55
+ }
56
+ hash
57
+ end
58
+ end
59
+ end
data/lib/oso/oso.rb ADDED
@@ -0,0 +1,255 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ require 'oso/version'
6
+ require 'oso/api'
7
+ require 'oso/helpers'
8
+
9
+ ##
10
+ # For more detailed documentation, see
11
+ # https://www.osohq.com/docs/reference/client-apis/ruby
12
+ module OsoCloud
13
+
14
+ # Represents an object in your application, with a type and id.
15
+ # Both "type" and "id" should be strings.
16
+ Value = Struct::new(:type, :id, keyword_init: true) do
17
+
18
+ def to_api_value
19
+ OsoCloud::Helpers.extract_value(self)
20
+ end
21
+ end
22
+
23
+ # Oso Cloud client for Ruby
24
+ #
25
+ # About facts:
26
+ #
27
+ # Some of these methods accept and return "fact"s.
28
+ # A "fact" is an array with at least one element.
29
+ # The first element must be a string, representing the fact's name.
30
+ # Any other elements in the array, which together represent the fact's arguments,
31
+ # can be "OsoCloud::Value" objects or strings.
32
+ class Oso
33
+ def initialize(url: 'https://cloud.osohq.com', api_key: nil)
34
+ @api = OsoCloud::Core::Api.new(url: url, api_key: api_key)
35
+ end
36
+
37
+ ##
38
+ # Update the active policy
39
+ #
40
+ # Updates the active policy in Oso Cloud, The string passed into
41
+ # this method should be written in Polar.
42
+ #
43
+ # @param policy [String]
44
+ # @return [nil]
45
+ def policy(policy)
46
+ @api.post_policy(OsoCloud::Core::Policy.new(src: policy, filename: ""))
47
+ nil
48
+ end
49
+
50
+ ##
51
+ # Check a permission
52
+ #
53
+ # Returns true if the actor can perform the action on the resource;
54
+ # otherwise false.
55
+ #
56
+ # @param actor [OsoCloud::Value]
57
+ # @param action [String]
58
+ # @param resource [OsoCloud::Value]
59
+ # @param context_facts [Array<fact>]
60
+ # @return [Boolean]
61
+ # @see Oso more information about facts
62
+ def authorize(actor, action, resource, context_facts = [])
63
+ actor_typed_id = actor.to_api_value
64
+ resource_typed_id = resource.to_api_value
65
+ result = @api.post_authorize(OsoCloud::Core::AuthorizeQuery.new(
66
+ actor_type: actor_typed_id.type,
67
+ actor_id: actor_typed_id.id,
68
+ action: action,
69
+ resource_type: resource_typed_id.type,
70
+ resource_id: resource_typed_id.id,
71
+ context_facts: OsoCloud::Helpers.params_to_facts(context_facts)
72
+ ))
73
+ result.allowed
74
+ end
75
+
76
+ ##
77
+ # Check authorized resources
78
+ #
79
+ # Returns a subset of the resource which an actor can perform
80
+ # a particular action. Ordering and duplicates, if any exist, are preserved.
81
+ #
82
+ # @param actor [OsoCloud::Value]
83
+ # @param action [String]
84
+ # @param resources [Array<OsoCloud::Value>]
85
+ # @param context_facts [Array<fact>]
86
+ # @return [Array<OsoCloud::Value>]
87
+ # @see Oso more information about facts
88
+ def authorize_resources(actor, action, resources, context_facts = [])
89
+ return [] if resources.nil?
90
+ return [] if resources.empty?
91
+
92
+ key = lambda do |type, id|
93
+ "#{type}:#{id}"
94
+ end
95
+
96
+ resources_extracted = resources.map(&:to_api_value)
97
+ actor_typed_id = actor.to_api_value
98
+ data = OsoCloud::Core::AuthorizeResourcesQuery.new(
99
+ actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
100
+ action: action,
101
+ resources: resources_extracted,
102
+ context_facts: OsoCloud::Helpers::params_to_facts(context_facts)
103
+ )
104
+ result = @api.post_authorize_resources(data)
105
+
106
+ return [] if result.results.empty?
107
+
108
+ results_lookup = Hash.new
109
+ result.results.each do |r|
110
+ k = key.call(r.type, r.id)
111
+ if results_lookup[k] == nil
112
+ results_lookup[k] = true
113
+ end
114
+ end
115
+
116
+ results = resources.select do |r|
117
+ e = r.to_api_value
118
+ exists = results_lookup[key.call(e.type, e.id)]
119
+ exists
120
+ end
121
+ results
122
+ end
123
+
124
+ ##
125
+ # List authorized resources
126
+ #
127
+ # Fetches a list of resource ids on which an actor can perform a
128
+ # particular action.
129
+ #
130
+ # @param actor [OsoCloud::Value]
131
+ # @param action [String]
132
+ # @param resource_type [String]
133
+ # @param context_facts [Array<fact>]
134
+ # @return [Array<String>]
135
+ # @see Oso more information about facts
136
+ def list(actor, action, resource_type, context_facts = [])
137
+ actor_typed_id = actor.to_api_value
138
+ result = @api.post_list(OsoCloud::Core::ListQuery.new(
139
+ actor_type: actor_typed_id.type,
140
+ actor_id: actor_typed_id.id,
141
+ action: action,
142
+ resource_type: resource_type,
143
+ context_facts: OsoCloud::Helpers.params_to_facts(context_facts)
144
+ ))
145
+ result.results
146
+ end
147
+
148
+ ##
149
+ # List authorized actions
150
+ #
151
+ # Fetches a list of actions which an actor can perform on a particular resource.
152
+ #
153
+ # @param actor [OsoCloud::Value]
154
+ # @param resource [OsoCloud::Value]
155
+ # @param context_facts [Array<fact>]
156
+ # @return [Array<String>]
157
+ # @see Oso more information about facts
158
+ def actions(actor, resource, context_facts = [])
159
+ actor_typed_id = actor.to_api_value
160
+ resource_typed_id = resource.to_api_value
161
+ result = @api.post_actions(OsoCloud::Core::ActionsQuery.new(
162
+ actor_type: actor_typed_id.type,
163
+ actor_id: actor_typed_id.id,
164
+ resource_type: resource_typed_id.type,
165
+ resource_id: resource_typed_id.id,
166
+ context_facts: OsoCloud::Helpers.params_to_facts(context_facts)
167
+ ))
168
+ result.results
169
+ end
170
+
171
+ ##
172
+ # Add a fact
173
+ #
174
+ # Adds a fact with the given name and arguments.
175
+ #
176
+ # @param name [String]
177
+ # @param args [*[String, OsoCloud::Value]]
178
+ # @return [nil]
179
+ def tell(name, *args)
180
+ typed_args = args.map { |a| OsoCloud::Helpers.extract_value(a)}
181
+ @api.post_facts(OsoCloud::Core::Fact.new(predicate: name, args: typed_args))
182
+ nil
183
+ end
184
+
185
+ ##
186
+ # Add many facts
187
+ #
188
+ # Adds many facts at once.
189
+ #
190
+ # @param facts [Array<fact>]
191
+ # @return [nil]
192
+ # @see Oso more information about facts
193
+ def bulk_tell(facts)
194
+ @api.post_bulk_load(OsoCloud::Helpers.params_to_facts(facts))
195
+ nil
196
+ end
197
+
198
+ ##
199
+ # Delete fact
200
+ #
201
+ # Deletes a fact. Does not throw an error if the fact is not found.
202
+ #
203
+ # @param name [String]
204
+ # @param args [*[String, OsoCloud::Value]]
205
+ # @return [nil]
206
+ def delete(name, *args)
207
+ typed_args = args.map { |a| OsoCloud::Helpers.extract_value(a) }
208
+ @api.delete_facts(OsoCloud::Core::Fact.new(predicate: name, args: typed_args))
209
+ nil
210
+ end
211
+
212
+ ##
213
+ # Delete many facts
214
+ #
215
+ # Deletes many facts at once. Does not throw an error when some of
216
+ # the facts are not found.
217
+ #
218
+ # @param facts [Array<fact>]
219
+ # @return [nil]
220
+ # @see Oso more information about facts
221
+ def bulk_delete(facts)
222
+ @api.post_bulk_delete(OsoCloud::Helpers.params_to_facts(facts))
223
+ nil
224
+ end
225
+
226
+ ##
227
+ # List facts
228
+ #
229
+ # Lists facts that are stored in Oso Cloud. Can be used to check the existence
230
+ # of a particular fact, or used to fetch all facts that have a particular
231
+ # argument. nil arguments operate as wildcards.
232
+ #
233
+ # @param name [String]
234
+ # @param args [*[String, OsoCloud::Value, nil]]
235
+ # @return [Array<fact>]
236
+ # @see Oso more information about facts
237
+ def get(name, *args)
238
+ @api.get_facts(name, args).map do |f|
239
+ name = f.predicate
240
+ args = f.args.map do |a|
241
+ v = OsoCloud::Helpers.from_value(a)
242
+ if v.is_a? Hash
243
+ OsoCloud::Value.new(type: v[:type], id: v[:id])
244
+ else
245
+ v
246
+ end
247
+ end
248
+ [name, *args]
249
+ end
250
+ end
251
+
252
+
253
+ # TODO query, bulk
254
+ end
255
+ end
data/lib/oso/version.rb CHANGED
@@ -1,3 +1,3 @@
1
- module Oso
2
- VERSION = '0.7.0'.freeze
1
+ module OsoCloud
2
+ VERSION = '1.0.1'.freeze
3
3
  end
data/lib/oso-cloud.rb ADDED
@@ -0,0 +1 @@
1
+ require 'oso/oso'
data/oso-cloud.gemspec CHANGED
@@ -2,13 +2,14 @@ require_relative 'lib/oso/version'
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'oso-cloud'
5
- spec.version = Oso::VERSION
5
+ spec.version = OsoCloud::VERSION
6
6
  spec.authors = ['Oso Security, Inc.']
7
7
  spec.email = ['support@osohq.com']
8
- spec.summary = 'Oso authorization library.'
8
+ spec.summary = 'Oso Cloud Ruby client'
9
9
  spec.homepage = 'https://www.osohq.com/'
10
+ spec.license = 'Apache-2.0'
10
11
 
11
- spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
12
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
12
13
 
13
14
  # Specify which files should be added to the gem when it is released.
14
15
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -19,5 +20,7 @@ Gem::Specification.new do |spec|
19
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
21
  spec.require_paths = ['lib']
21
22
 
23
+ spec.add_dependency 'faraday', '~> 2.5.2'
24
+ spec.add_dependency 'faraday-retry', '~> 2.0.0'
22
25
  spec.add_development_dependency 'minitest', '~> 5.15'
23
26
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oso-cloud
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oso Security, Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-24 00:00:00.000000000 Z
11
+ date: 2023-03-29 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.5.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.5.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-retry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.0
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: minitest
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,11 +66,15 @@ files:
38
66
  - Rakefile
39
67
  - bin/console
40
68
  - bin/setup
41
- - lib/oso/client.rb
69
+ - lib/oso-cloud.rb
70
+ - lib/oso/api.rb
71
+ - lib/oso/helpers.rb
72
+ - lib/oso/oso.rb
42
73
  - lib/oso/version.rb
43
74
  - oso-cloud.gemspec
44
75
  homepage: https://www.osohq.com/
45
- licenses: []
76
+ licenses:
77
+ - Apache-2.0
46
78
  metadata: {}
47
79
  post_install_message:
48
80
  rdoc_options: []
@@ -52,15 +84,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
52
84
  requirements:
53
85
  - - ">="
54
86
  - !ruby/object:Gem::Version
55
- version: 2.7.0
87
+ version: 3.0.0
56
88
  required_rubygems_version: !ruby/object:Gem::Requirement
57
89
  requirements:
58
90
  - - ">="
59
91
  - !ruby/object:Gem::Version
60
92
  version: '0'
61
93
  requirements: []
62
- rubygems_version: 3.1.6
94
+ rubygems_version: 3.2.33
63
95
  signing_key:
64
96
  specification_version: 4
65
- summary: Oso authorization library.
97
+ summary: Oso Cloud Ruby client
66
98
  test_files: []
data/lib/oso/client.rb DELETED
@@ -1,198 +0,0 @@
1
- require 'json'
2
- require 'net/http'
3
- require 'uri'
4
-
5
- require 'oso/version'
6
-
7
- module Oso
8
- class Client
9
- def initialize(url: 'https://cloud.osohq.com', api_key: nil)
10
- @url = url
11
- @api_key = api_key
12
- end
13
-
14
- def policy(policy)
15
- POST('policy', { src: policy })
16
- end
17
-
18
- def authorize(actor, action, resource, context_facts = [])
19
- actor_typed_id = extract_typed_id actor
20
- resource_typed_id = extract_typed_id resource
21
- result = POST('authorize', {
22
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
23
- action: action,
24
- resource_type: resource_typed_id.type, resource_id: resource_typed_id.id,
25
- context_facts: facts_to_params(context_facts)
26
- })
27
- allowed = result['allowed']
28
- allowed
29
- end
30
-
31
- def authorize_resources(actor, action, resources, context_facts = [])
32
- return [] if resources.nil?
33
- return [] if resources.empty?
34
-
35
- key = lambda do |type, id|
36
- "#{type}:#{id}"
37
- end
38
-
39
- resources_extracted = resources.map { |r| extract_typed_id(r) }
40
- actor_typed_id = extract_typed_id actor
41
- result = POST('authorize_resources', {
42
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
43
- action: action,
44
- resources: resources_extracted,
45
- context_facts: facts_to_params(context_facts)
46
- })
47
-
48
- return [] if result['results'].empty?
49
-
50
- results_lookup = Hash.new
51
- result['results'].each do |r|
52
- k = key.call(r['type'], r['id'])
53
- if results_lookup[k] == nil
54
- results_lookup[k] = true
55
- end
56
- end
57
-
58
- results = resources.select do |r|
59
- e = extract_typed_id(r)
60
- exists = results_lookup[key.call(e.type, e.id)]
61
- exists
62
- end
63
- results
64
- end
65
-
66
- def list(actor, action, resource_type, context_facts = [])
67
- actor_typed_id = extract_typed_id actor
68
- result = POST('list', {
69
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
70
- action: action,
71
- resource_type: resource_type,
72
- context_facts: facts_to_params(context_facts)
73
- })
74
- results = result['results']
75
- results
76
- end
77
-
78
- def actions(actor, resource, context_facts = [])
79
- actor_typed_id = extract_typed_id actor
80
- resource_typed_id = extract_typed_id resource
81
- result = POST('actions', {
82
- actor_type: actor_typed_id.type, actor_id: actor_typed_id.id,
83
- resource_type: resource_typed_id.type, resource_id: resource_typed_id.id,
84
- context_facts: facts_to_params(context_facts)
85
- })
86
- results = result['results']
87
- results
88
- end
89
-
90
- def tell(predicate, *args)
91
- typed_args = args.map { |a| extract_typed_id a}
92
- POST('facts', { predicate: predicate, args: typed_args })
93
- end
94
-
95
- def bulk_tell(facts)
96
- POST('bulk_load', facts_to_params(facts))
97
- end
98
-
99
- def delete(predicate, *args)
100
- typed_args = args.map { |a| extract_typed_id a}
101
- DELETE('facts', { predicate: predicate, args: typed_args })
102
- end
103
-
104
- def bulk_delete(facts)
105
- POST('bulk_delete', facts_to_params(facts))
106
- end
107
-
108
- def get(predicate, *args)
109
- params = {predicate: predicate}
110
- args.each_with_index do |arg, i|
111
- typed_id = extract_arg_query(arg)
112
- if typed_id
113
- params["args.#{i}.type"] = typed_id.type
114
- params["args.#{i}.id"] = typed_id.id
115
- end
116
- end
117
-
118
- GET('facts', params)
119
- end
120
-
121
- private
122
-
123
- def headers()
124
- {
125
- "Authorization" => "Basic %s" % @api_key,
126
- "User-Agent" => "Oso Cloud (ruby)",
127
- "Accept": "application/json",
128
- "Content-Type": "application/json"
129
- }
130
- end
131
-
132
-
133
- def GET(path, params)
134
- uri = URI("#{@url}/api/#{path}")
135
- uri.query = URI::encode_www_form(params)
136
- use_ssl = (uri.scheme == 'https')
137
-
138
- result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl ) { |http|
139
- http.request(Net::HTTP::Get.new(uri, headers)) {|r|
140
- r.read_body
141
- }
142
- }
143
- handle_result result
144
-
145
- end
146
-
147
- def POST(path, params)
148
- result = Net::HTTP.post(URI("#{@url}/api/#{path}"), params.to_json, headers)
149
- handle_result result
150
- end
151
-
152
- def DELETE(path, params)
153
- uri = URI("#{@url}/api/#{path}")
154
- use_ssl = (uri.scheme == 'https')
155
- result = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl ) { |http|
156
- http.request(Net::HTTP::Delete.new(uri, headers), params.to_json) {|r|
157
- r.read_body
158
- }
159
- }
160
- handle_result result
161
- end
162
-
163
- def handle_result(result)
164
- unless result.is_a?(Net::HTTPSuccess)
165
- raise "Got an unexpected error from Oso Service: #{result.code}\n#{result.body}"
166
- end
167
-
168
- JSON.parse(result.body)
169
- end
170
-
171
- def extract_typed_id(x)
172
- return TypedId.new(type: "String", id: x) if x.is_a? String
173
-
174
- raise "#{x} does not have an 'id' field" unless x.respond_to? :id
175
- raise "Invalid 'id' field on #{x}: #{x.id}" if x.id.nil?
176
-
177
- TypedId.new(type: x.class.name, id: x.id.to_s)
178
- end
179
-
180
- def extract_arg_query(x)
181
- return nil if x.nil?
182
- extract_typed_id(x)
183
- end
184
-
185
- def facts_to_params(facts)
186
- facts.map { |predicate, *args|
187
- typed_args = args.map { |a| extract_typed_id a}
188
- { predicate: predicate, args: typed_args }
189
- }
190
- end
191
-
192
- TypedId = Struct.new(:type, :id, keyword_init: true) do
193
- def to_json(*args)
194
- to_h.to_json(*args)
195
- end
196
- end
197
- end
198
- end