require 'json' require 'uri' require 'faraday' require 'faraday/retry' require 'oso/helpers' module OsoCloud # @!visibility private module Core # @!visibility private class ApiResult attr_reader :message def initialize(message:) @message = message end end # @!visibility private class ApiError < StandardError def initialize(message:) super(message) end end # @!visibility private class Policy attr_reader :filename attr_reader :src def initialize(filename:, src:) @filename = filename @src = src end end # @!visibility private class GetPolicyResult attr_reader :policy def initialize(policy:) if policy.is_a? Policy @policy = policy else @policy = Policy.new(**policy) end end end # @!visibility private class Fact attr_reader :predicate attr_reader :args def initialize(predicate:, args:) @predicate = predicate @args = args.map { |v| if v.is_a? Value then v else Value.new(**v) end } end end # @!visibility private class Value attr_reader :type attr_reader :id def initialize(type:, id:) @type = type @id = id end end # @!visibility private class Bulk attr_reader :delete attr_reader :tell def initialize(delete:, tell:) @delete = delete.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } @tell = tell.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } end end # @!visibility private class AuthorizeResult attr_reader :allowed def initialize(allowed:) @allowed = allowed end end # @!visibility private class AuthorizeQuery attr_reader :actor_type attr_reader :actor_id attr_reader :action attr_reader :resource_type attr_reader :resource_id attr_reader :context_facts def initialize(actor_type:, actor_id:, action:, resource_type:, resource_id:, context_facts:) @actor_type = actor_type @actor_id = actor_id @action = action @resource_type = resource_type @resource_id = resource_id @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } end end # @!visibility private class AuthorizeResourcesResult attr_reader :results def initialize(results:) @results = results.map { |v| if v.is_a? Value then v else Value.new(**v) end } end end # @!visibility private class AuthorizeResourcesQuery attr_reader :actor_type attr_reader :actor_id attr_reader :action attr_reader :resources attr_reader :context_facts def initialize(actor_type:, actor_id:, action:, resources:, context_facts:) @actor_type = actor_type @actor_id = actor_id @action = action @resources = resources.map { |v| if v.is_a? Value then v else Value.new(**v) end } @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } end end # @!visibility private class ListResult attr_reader :results def initialize(results:) @results = results end end # @!visibility private class ListQuery attr_reader :actor_type attr_reader :actor_id attr_reader :action attr_reader :resource_type attr_reader :context_facts def initialize(actor_type:, actor_id:, action:, resource_type:, context_facts:) @actor_type = actor_type @actor_id = actor_id @action = action @resource_type = resource_type @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } end end # @!visibility private class ActionsResult attr_reader :results def initialize(results:) @results = results end end # @!visibility private class ActionsQuery attr_reader :actor_type attr_reader :actor_id attr_reader :resource_type attr_reader :resource_id attr_reader :context_facts def initialize(actor_type:, actor_id:, resource_type:, resource_id:, context_facts:) @actor_type = actor_type @actor_id = actor_id @resource_type = resource_type @resource_id = resource_id @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } end end # @!visibility private class QueryResult attr_reader :results def initialize(results:) @results = results.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } end end # @!visibility private class Query attr_reader :fact attr_reader :context_facts def initialize(fact:, context_facts:) if fact.is_a? Fact @fact = fact else @fact = Fact.new(**fact) end @context_facts = context_facts.map { |v| if v.is_a? Fact then v else Fact.new(**v) end } end end # @!visibility private class StatsResult attr_reader :num_roles attr_reader :num_relations attr_reader :num_facts def initialize(num_roles:, num_relations:, num_facts:) @num_roles = num_roles @num_relations = num_relations @num_facts = num_facts end end # @!visibility private class Api def initialize(url: 'https://cloud.osohq.com', api_key: nil, options: nil) @url = url @connection = Faraday.new(url: url) do |faraday| faraday.request :json # responses are processed in reverse order; this stack implies the # retries are attempted before an error is raised, and the json # parser is only applied if there are no errors faraday.response :json, preserve_raw: true faraday.response :raise_error faraday.request :retry, { max: (options && options[:max_retries]) || 10, interval: 0.01, interval_randomness: 0.005, max_interval: 1, backoff_factor: 2, retry_statuses: [429, 500, 502, 503, 504], # ensure authorize and related check functions are retried because # they are POST requests, which are not retried automatically retry_if: ->(env, _exc) { %w[ /api/authorize /api/authorize_resources /api/list /api/actions /api/query ].include? env.url.path }, } if (options && options[:test_adapter]) faraday.adapter :test do |stub| stub.post(options[:test_adapter][:path]) do |env| options[:test_adapter][:func].call end stub.get(options[:test_adapter][:path]) do |env| options[:test_adapter][:func].call end stub.delete(options[:test_adapter][:path]) do |env| options[:test_adapter][:func].call end end else faraday.adapter :net_http end end @api_key = api_key end def get_policy() params = {} data = nil url = "/policy" result = GET(url, params, data) GetPolicyResult.new(**result) end def post_policy(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/policy" result = POST(url, params, data) ApiResult.new(**result) end def post_facts(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/facts" result = POST(url, params, data) Fact.new(**result) end def delete_facts(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/facts" result = DELETE(url, params, data) ApiResult.new(**result) end def post_bulk_load(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/bulk_load" result = POST(url, params, data) ApiResult.new(**result) end def post_bulk_delete(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/bulk_delete" result = POST(url, params, data) ApiResult.new(**result) end def post_bulk(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/bulk" result = POST(url, params, data) ApiResult.new(**result) end def post_authorize(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/authorize" result = POST(url, params, data) AuthorizeResult.new(**result) end def post_authorize_resources(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/authorize_resources" result = POST(url, params, data) AuthorizeResourcesResult.new(**result) end def post_list(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/list" result = POST(url, params, data) ListResult.new(**result) end def post_actions(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/actions" result = POST(url, params, data) ActionsResult.new(**result) end def post_query(data) params = {} data = OsoCloud::Helpers.to_hash(data) url = "/query" result = POST(url, params, data) QueryResult.new(**result) end def get_stats() params = {} data = nil url = "/stats" result = GET(url, params, data) StatsResult.new(**result) end def clear_data() params = {} data = nil url = "/clear_data" result = POST(url, params, data) ApiResult.new(**result) end # hard-coded, not generated def get_facts(predicate, args) params = {} params["predicate"] = predicate args.each_with_index do |arg, i| arg_query = OsoCloud::Helpers.extract_arg_query(arg) if arg_query params["args.#{i}.type"] = arg_query.type params["args.#{i}.id"] = arg_query.id end end data = nil url = "/facts" result = GET(url, params, data) result.map { |v| Fact.new(**v) } end def headers() { "Authorization" => "Bearer %s" % @api_key, "User-Agent" => "Oso Cloud (ruby)", "Accept": "application/json", "Content-Type": "application/json", "X-OsoApiVersion": "0" } end def GET(path, params, body) response = @connection.get("api#{path}", params, headers ) handle_faraday_response response rescue Faraday::Error => error handle_faraday_error error end def POST(path, params, body) response = @connection.post("api#{path}", body, headers) do |req| req.params = params end handle_faraday_response response rescue Faraday::Error => error handle_faraday_error error end def DELETE(path, params, body) response = @connection.delete("api#{path}", params, headers) do |req| req.body = body end handle_faraday_response response rescue Faraday::Error => error handle_faraday_error error end def handle_faraday_response(response) # TODO:(@patrickod) refactor duplicative JSON parsing JSON.parse(response.env[:raw_body], symbolize_names: true) end def handle_faraday_error(error) err = JSON.parse(error.response[:body], symbolize_names: true) raise ApiError.new(**err) rescue JSON::ParserError => e raise ApiError.new(message: e.message) end end end end