oso-cloud 0.6.0 → 1.0.0

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: f76c57bb36720bbfbd88a403db0e80304c1d29edd4ee24ffaf4a3e48ff34760f
4
- data.tar.gz: bb077dcb1f83e4b376d302ecf91067aa0b06c466e9e6c4a39cfeea0851b553a5
3
+ metadata.gz: 460a79201e11b1f8e81db2eedea8bfd1c6651301336e1d31652a54a1ed537344
4
+ data.tar.gz: bd3692528348f8e9e45a36c09a3df41cef74fc47258db81471b456e0f1297f32
5
5
  SHA512:
6
- metadata.gz: b0c91e6514431e826ac0729c15e008c5aed0a85a9eed6d4d60e59fb1b75919eac6a3199e5d30bc2a3e806a291e5c9421010ae6323dc37adfa56bcc0a82a38ab4
7
- data.tar.gz: 6130fd8d01cc30f858dd8494d2fe73ff95c451796589728175dd25a29dced4b6f7ef21d889c870a260897c389b2c071dd12511d385c4093c80aaebbbed193e87
6
+ metadata.gz: 360042f1cb0f076651160473ae092215f36773832c70aa042e26a58a2ebbd9aacf034ec7e0227136075fbe1bc000148c627629a6ddee3ca0ba91cc422b1f2818
7
+ data.tar.gz: 79eeab9786a8358d12c3010648ab03f3fff454e69ae217e15122fbc5ebd5e2f4619111da3727e2bb60af14bd3f11e66989c8c74594243852ab131de0e8d1396d
data/Gemfile.lock CHANGED
@@ -1,13 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- oso-cloud (0.6.0)
4
+ oso-cloud (1.0.0)
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.0)
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,416 @@
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
22
+ attr_reader :message
23
+
24
+ def initialize(message:)
25
+ @message = message
26
+ end
27
+ end
28
+
29
+ # @!visibility private
30
+ class Policy
31
+ attr_reader :filename
32
+ attr_reader :src
33
+
34
+ def initialize(filename:, src:)
35
+ @filename = filename
36
+ @src = src
37
+ end
38
+ end
39
+
40
+ # @!visibility private
41
+ class GetPolicyResult
42
+ attr_reader :policy
43
+
44
+ def initialize(policy:)
45
+ if policy.is_a? Policy
46
+ @policy = policy
47
+ else
48
+ @policy = Policy.new(**policy)
49
+ end
50
+ end
51
+ end
52
+
53
+ # @!visibility private
54
+ class Fact
55
+ attr_reader :predicate
56
+ attr_reader :args
57
+
58
+ def initialize(predicate:, args:)
59
+ @predicate = predicate
60
+ @args = args.map { |v| if v.is_a? Value then v else Value.new(**v) end }
61
+ end
62
+ end
63
+
64
+ # @!visibility private
65
+ class Value
66
+ attr_reader :type
67
+ attr_reader :id
68
+
69
+ def initialize(type:, id:)
70
+ @type = type
71
+ @id = id
72
+ end
73
+ end
74
+
75
+ # @!visibility private
76
+ class Bulk
77
+ attr_reader :delete
78
+ attr_reader :tell
79
+
80
+ def initialize(delete:, tell:)
81
+ @delete = delete.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
82
+ @tell = tell.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
83
+ end
84
+ end
85
+
86
+ # @!visibility private
87
+ class AuthorizeResult
88
+ attr_reader :allowed
89
+
90
+ def initialize(allowed:)
91
+ @allowed = allowed
92
+ end
93
+ end
94
+
95
+ # @!visibility private
96
+ class AuthorizeQuery
97
+ attr_reader :actor_type
98
+ attr_reader :actor_id
99
+ attr_reader :action
100
+ attr_reader :resource_type
101
+ attr_reader :resource_id
102
+ attr_reader :context_facts
103
+
104
+ def initialize(actor_type:, actor_id:, action:, resource_type:, resource_id:, context_facts:)
105
+ @actor_type = actor_type
106
+ @actor_id = actor_id
107
+ @action = action
108
+ @resource_type = resource_type
109
+ @resource_id = resource_id
110
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
111
+ end
112
+ end
113
+
114
+ # @!visibility private
115
+ class AuthorizeResourcesResult
116
+ attr_reader :results
117
+
118
+ def initialize(results:)
119
+ @results = results.map { |v| if v.is_a? Value then v else Value.new(**v) end }
120
+ end
121
+ end
122
+
123
+ # @!visibility private
124
+ class AuthorizeResourcesQuery
125
+ attr_reader :actor_type
126
+ attr_reader :actor_id
127
+ attr_reader :action
128
+ attr_reader :resources
129
+ attr_reader :context_facts
130
+
131
+ def initialize(actor_type:, actor_id:, action:, resources:, context_facts:)
132
+ @actor_type = actor_type
133
+ @actor_id = actor_id
134
+ @action = action
135
+ @resources = resources.map { |v| if v.is_a? Value then v else Value.new(**v) end }
136
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
137
+ end
138
+ end
139
+
140
+ # @!visibility private
141
+ class ListResult
142
+ attr_reader :results
143
+
144
+ def initialize(results:)
145
+ @results = results
146
+ end
147
+ end
148
+
149
+ # @!visibility private
150
+ class ListQuery
151
+ attr_reader :actor_type
152
+ attr_reader :actor_id
153
+ attr_reader :action
154
+ attr_reader :resource_type
155
+ attr_reader :context_facts
156
+
157
+ def initialize(actor_type:, actor_id:, action:, resource_type:, context_facts:)
158
+ @actor_type = actor_type
159
+ @actor_id = actor_id
160
+ @action = action
161
+ @resource_type = resource_type
162
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
163
+ end
164
+ end
165
+
166
+ # @!visibility private
167
+ class ActionsResult
168
+ attr_reader :results
169
+
170
+ def initialize(results:)
171
+ @results = results
172
+ end
173
+ end
174
+
175
+ # @!visibility private
176
+ class ActionsQuery
177
+ attr_reader :actor_type
178
+ attr_reader :actor_id
179
+ attr_reader :resource_type
180
+ attr_reader :resource_id
181
+ attr_reader :context_facts
182
+
183
+ def initialize(actor_type:, actor_id:, resource_type:, resource_id:, context_facts:)
184
+ @actor_type = actor_type
185
+ @actor_id = actor_id
186
+ @resource_type = resource_type
187
+ @resource_id = resource_id
188
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
189
+ end
190
+ end
191
+
192
+ # @!visibility private
193
+ class QueryResult
194
+ attr_reader :results
195
+
196
+ def initialize(results:)
197
+ @results = results.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
198
+ end
199
+ end
200
+
201
+ # @!visibility private
202
+ class Query
203
+ attr_reader :fact
204
+ attr_reader :context_facts
205
+
206
+ def initialize(fact:, context_facts:)
207
+ if fact.is_a? Fact
208
+ @fact = fact
209
+ else
210
+ @fact = Fact.new(**fact)
211
+ end
212
+ @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end }
213
+ end
214
+ end
215
+
216
+ # @!visibility private
217
+ class StatsResult
218
+ attr_reader :num_roles
219
+ attr_reader :num_relations
220
+ attr_reader :num_facts
221
+
222
+ def initialize(num_roles:, num_relations:, num_facts:)
223
+ @num_roles = num_roles
224
+ @num_relations = num_relations
225
+ @num_facts = num_facts
226
+ end
227
+ end
228
+
229
+
230
+ # @!visibility private
231
+ class Api
232
+ def initialize(url: 'https://cloud.osohq.com', api_key: nil)
233
+ @url = url
234
+ @connection = Faraday.new(url: url) do |faraday|
235
+ faraday.request :retry, {
236
+ max: 15,
237
+ interval: 0.05,
238
+ interval_randomness: 0.5,
239
+ backoff_factor: 2,
240
+ retry_statuses: [429, 500, 502, 503, 504],
241
+ }
242
+
243
+ faraday.request :json
244
+ faraday.response :json, preserve_raw: true
245
+ faraday.adapter :net_http
246
+ end
247
+ @api_key = api_key
248
+ end
249
+
250
+ def get_policy()
251
+ params = {}
252
+ data = nil
253
+ url = "/policy"
254
+ result = GET(url, params, data)
255
+ GetPolicyResult.new(**result)
256
+ end
257
+
258
+ def post_policy(data)
259
+ params = {}
260
+ data = OsoCloud::Helpers.to_hash(data)
261
+ url = "/policy"
262
+ result = POST(url, params, data)
263
+ ApiResult.new(**result)
264
+ end
265
+
266
+ def post_facts(data)
267
+ params = {}
268
+ data = OsoCloud::Helpers.to_hash(data)
269
+ url = "/facts"
270
+ result = POST(url, params, data)
271
+ Fact.new(**result)
272
+ end
273
+
274
+ def delete_facts(data)
275
+ params = {}
276
+ data = OsoCloud::Helpers.to_hash(data)
277
+ url = "/facts"
278
+ result = DELETE(url, params, data)
279
+ ApiResult.new(**result)
280
+ end
281
+
282
+ def post_bulk_load(data)
283
+ params = {}
284
+ data = OsoCloud::Helpers.to_hash(data)
285
+ url = "/bulk_load"
286
+ result = POST(url, params, data)
287
+ ApiResult.new(**result)
288
+ end
289
+
290
+ def post_bulk_delete(data)
291
+ params = {}
292
+ data = OsoCloud::Helpers.to_hash(data)
293
+ url = "/bulk_delete"
294
+ result = POST(url, params, data)
295
+ ApiResult.new(**result)
296
+ end
297
+
298
+ def post_bulk(data)
299
+ params = {}
300
+ data = OsoCloud::Helpers.to_hash(data)
301
+ url = "/bulk"
302
+ result = POST(url, params, data)
303
+ ApiResult.new(**result)
304
+ end
305
+
306
+ def post_authorize(data)
307
+ params = {}
308
+ data = OsoCloud::Helpers.to_hash(data)
309
+ url = "/authorize"
310
+ result = POST(url, params, data)
311
+ AuthorizeResult.new(**result)
312
+ end
313
+
314
+ def post_authorize_resources(data)
315
+ params = {}
316
+ data = OsoCloud::Helpers.to_hash(data)
317
+ url = "/authorize_resources"
318
+ result = POST(url, params, data)
319
+ AuthorizeResourcesResult.new(**result)
320
+ end
321
+
322
+ def post_list(data)
323
+ params = {}
324
+ data = OsoCloud::Helpers.to_hash(data)
325
+ url = "/list"
326
+ result = POST(url, params, data)
327
+ ListResult.new(**result)
328
+ end
329
+
330
+ def post_actions(data)
331
+ params = {}
332
+ data = OsoCloud::Helpers.to_hash(data)
333
+ url = "/actions"
334
+ result = POST(url, params, data)
335
+ ActionsResult.new(**result)
336
+ end
337
+
338
+ def post_query(data)
339
+ params = {}
340
+ data = OsoCloud::Helpers.to_hash(data)
341
+ url = "/query"
342
+ result = POST(url, params, data)
343
+ QueryResult.new(**result)
344
+ end
345
+
346
+ def get_stats()
347
+ params = {}
348
+ data = nil
349
+ url = "/stats"
350
+ result = GET(url, params, data)
351
+ StatsResult.new(**result)
352
+ end
353
+
354
+ def clear_data()
355
+ params = {}
356
+ data = nil
357
+ url = "/clear_data"
358
+ result = POST(url, params, data)
359
+ ApiResult.new(**result)
360
+ end
361
+
362
+
363
+ # hard-coded, not generated
364
+ def get_facts(predicate, args)
365
+ params = {}
366
+ params["predicate"] = predicate
367
+ args.each_with_index do |arg, i|
368
+ arg_query = OsoCloud::Helpers.extract_arg_query(arg)
369
+ if arg_query
370
+ params["args.#{i}.type"] = arg_query.type
371
+ params["args.#{i}.id"] = arg_query.id
372
+ end
373
+ end
374
+ data = nil
375
+ url = "/facts"
376
+ result = GET(url, params, data)
377
+ result.map { |v| Fact.new(**v) }
378
+ end
379
+
380
+ def headers()
381
+ {
382
+ "Authorization" => "Bearer %s" % @api_key,
383
+ "User-Agent" => "Oso Cloud (ruby)",
384
+ "Accept": "application/json",
385
+ "Content-Type": "application/json",
386
+ "X-OsoApiVersion": "0"
387
+ }
388
+ end
389
+
390
+ def GET(path, params, body)
391
+ response = @connection.get("api#{path}", params, headers )
392
+ handle_faraday_response response
393
+ end
394
+
395
+ def POST(path, params, body)
396
+ response = @connection.post("api#{path}", body, headers) do |req|
397
+ req.params = params
398
+ end
399
+ handle_faraday_response response
400
+ end
401
+
402
+ def DELETE(path, params, body)
403
+ response = @connection.delete("api#{path}", params, headers) do |req|
404
+ req.body = body
405
+ end
406
+ handle_faraday_response response
407
+ end
408
+
409
+ def handle_faraday_response(response)
410
+ # TODO:(@patrickod) refactor duplicative JSON parsing
411
+ JSON.parse(response.env[:raw_body], symbolize_names: true)
412
+ end
413
+ end
414
+
415
+ end
416
+ 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.6.0'.freeze
1
+ module OsoCloud
2
+ VERSION = '1.0.0'.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.6.0
4
+ version: 1.0.0
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-23 00:00:00.000000000 Z
11
+ date: 2022-09-27 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