require 'json' require 'uri' require 'faraday' require 'faraday/retry' require 'faraday/net_http_persistent' require 'oso/helpers' require 'oso/version' 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, :src def initialize(filename:, src:) @filename = filename @src = src end end # @!visibility private class GetPolicyResult attr_reader :policy def initialize(policy:) @policy = if policy.is_a? Policy policy else Policy.new(**policy) end end end class PolicyMetadata attr_reader :resources def initialize(resources:) @resources = resources.map do |k, v| if v.is_a? ResourceMetadata [k, v] else [k, ResourceMetadata.new(**v)] end end.to_h end end class ResourceMetadata attr_reader :roles, :permissions, :relations def initialize(roles:, permissions:, relations:) @roles = roles @permissions = permissions @relations = relations end end # @!visibility private class GetPolicyMetadataResult attr_reader :metadata def initialize(metadata:) @metadata = if metadata.is_a? PolicyMetadata metadata else PolicyMetadata.new(**metadata) end end end # @!visibility private class Fact attr_reader :predicate, :args def initialize(predicate:, args:) @predicate = predicate @args = args.map { |v| (v.is_a? Value) ? v : Value.new(**v) } end end # @!visibility private class Value attr_reader :type, :id def initialize(type:, id:) @type = type @id = id end end # @!visibility private class Bulk attr_reader :delete, :tell def initialize(delete:, tell:) @delete = delete.map { |v| (v.is_a? Fact) ? v : Fact.new(**v) } @tell = tell.map { |v| (v.is_a? Fact) ? v : Fact.new(**v) } end end # @!visibility private class AuthorizeResult attr_reader :allowed def initialize(allowed:) @allowed = allowed end end # @!visibility private class AuthorizeQuery attr_reader :actor_type, :actor_id, :action, :resource_type, :resource_id, :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| (v.is_a? Fact) ? v : Fact.new(**v) } end end # @!visibility private class AuthorizeResourcesResult attr_reader :results def initialize(results:) @results = results.map { |v| (v.is_a? Value) ? v : Value.new(**v) } end end # @!visibility private class AuthorizeResourcesQuery attr_reader :actor_type, :actor_id, :action, :resources, :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| (v.is_a? Value) ? v : Value.new(**v) } @context_facts = context_facts.map { |v| (v.is_a? Fact) ? v : Fact.new(**v) } end end # @!visibility private class ListResult attr_reader :results def initialize(results:) @results = results end end # @!visibility private class ListQuery attr_reader :actor_type, :actor_id, :action, :resource_type, :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| (v.is_a? Fact) ? v : Fact.new(**v) } end end # @!visibility private class ActionsResult attr_reader :results def initialize(results:) @results = results end end # @!visibility private class ActionsQuery attr_reader :actor_type, :actor_id, :resource_type, :resource_id, :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| (v.is_a? Fact) ? v : Fact.new(**v) } end end # @!visibility private class QueryResult attr_reader :results def initialize(results:) @results = results.map { |v| (v.is_a? Fact) ? v : Fact.new(**v) } end end # @!visibility private class Query attr_reader :fact, :context_facts def initialize(fact:, context_facts:) @fact = if fact.is_a? Fact fact else Fact.new(**fact) end @context_facts = context_facts.map { |v| (v.is_a? Fact) ? v : Fact.new(**v) } end end # @!visibility private class StatsResult attr_reader :num_roles, :num_relations, :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 LocalAuthQuery attr_reader :query, :data_bindings def initialize(query:, data_bindings:) @query = query @data_bindings = data_bindings end end # @!visibility private class LocalListQuery attr_reader :query, :column, :data_bindings def initialize(query:, column:, data_bindings:) @query = query @column = column @data_bindings = data_bindings end end # @!visibility private class LocalQueryResult attr_reader :sql def initialize(sql:) @sql = sql end end # @!visibility private class Api def initialize(url: 'https://api.osohq.com', api_key: nil, data_bindings: 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 :raise_error faraday.response :json, parser_options: { symbolize_names: true } 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], # This is the default set of methods plus POST. # ref: https://github.com/lostisland/faraday-retry#specify-which-methods-will-be-retried methods: %i[delete get head options post put], } 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_persistent, pool_size: 10 end end if options && options[:fallback_url] @fallback_connection = Faraday.new(url: options[:fallback_url]) do |faraday| faraday.request :json faraday.response :json, parser_options: { symbolize_names: true } faraday.response :raise_error faraday.adapter :net_http end end @api_key = api_key @user_agent = "Oso Cloud (ruby #{RUBY_VERSION}p#{RUBY_PATCHLEVEL}; rv:#{VERSION})" @last_offset = nil @data_bindings = IO.read(data_bindings) unless data_bindings.nil? end def fallback_eligible(path) !@fallback_connection.nil? && ['/authorize', '/authorize_resources', '/list', '/actions', '/query'].include?(path) end def get_policy url = '/policy' result = GET(url, nil) GetPolicyResult.new(**result) end def get_policy_metadata url = '/policy_metadata' result = GET(url, nil) GetPolicyMetadataResult.new(**result) end def post_policy(data) url = '/policy' result = POST(url, nil, data, true) ApiResult.new(**result) end def post_facts(data) url = '/facts' result = POST(url, nil, data, true) Fact.new(**result) end def delete_facts(data) url = '/facts' result = DELETE(url, data) ApiResult.new(**result) end def post_bulk_load(data) url = '/bulk_load' result = POST(url, nil, data, true) ApiResult.new(**result) end def post_bulk_delete(data) url = '/bulk_delete' result = POST(url, nil, data, true) ApiResult.new(**result) end def post_bulk(data) url = '/bulk' result = POST(url, nil, data, true) ApiResult.new(**result) end def post_authorize(data) url = '/authorize' result = POST(url, nil, data, false) AuthorizeResult.new(**result) end def post_authorize_resources(data) url = '/authorize_resources' result = POST(url, nil, data, false) AuthorizeResourcesResult.new(**result) end def post_list(data) url = '/list' result = POST(url, nil, data, false) ListResult.new(**result) end def post_actions(data) url = '/actions' result = POST(url, nil, data, false) ActionsResult.new(**result) end def post_bulk_actions(data) url = '/bulk_actions' results = POST(url, nil, data, false) results.map { |result| ActionsResult.new(**result) } end def post_query(data) url = '/query' result = POST(url, nil, data, false) QueryResult.new(**result) end def get_stats url = '/stats' result = GET(url, {}) StatsResult.new(**result) end def post_authorize_query(query) url = '/authorize_query' data = LocalAuthQuery.new(query: query, data_bindings: @data_bindings) result = POST(url, nil, data, false) LocalQueryResult.new(**result) end def post_list_query(query:, column:) url = '/list_query' data = LocalListQuery.new(query: query, column: column, data_bindings: @data_bindings) result = POST(url, nil, data, false) LocalQueryResult.new(**result) end def clear_data url = '/clear_data' result = POST(url, nil, nil, true) ApiResult.new(**result) end # hard-coded, not generated def get_facts(predicate, args) params = {} params['predicate'] = predicate args.each_with_index do |arg, i| next if arg.nil? 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 url = '/facts' result = GET(url, params) result.map { |v| Fact.new(**v) } end def headers default_headers = { 'Authorization' => format('Bearer %s', @api_key), 'User-Agent' => @user_agent, Accept: 'application/json', 'Content-Type': 'application/json', 'X-OsoApiVersion': '0', } # set OsoOffset is last_offset is not nil default_headers[:OsoOffset] = @last_offset unless @last_offset.nil? default_headers end def GET(path, params) begin response = @connection.get("api#{path}") do |req| req.params = params unless params.nil? req.headers = headers end response.body rescue Faraday::ServerError, Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e raise e unless fallback_eligible(path) response = @fallback_connection.get("api#{path}") do |req| req.params = params unless params.nil? req.headers = headers end response.body end rescue Faraday::Error => e handle_faraday_error e end def POST(path, params, body, isMutation) begin response = @connection.post("api#{path}") do |req| req.params = params unless params.nil? req.body = OsoCloud::Helpers.to_hash(body) unless body.nil? req.headers = headers end if isMutation @last_offset = response.headers[:OsoOffset] end response.body # only attempt fallback on 5xx, and connection failure conditions rescue Faraday::ServerError, Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e raise e unless fallback_eligible(path) response = @fallback_connection.post("api#{path}") do |req| req.params = params unless params.nil? req.body = OsoCloud::Helpers.to_hash(body) unless body.nil? req.headers = headers end response.body end rescue Faraday::Error => e handle_faraday_error e end def DELETE(path, body) response = @connection.delete("api#{path}") do |req| req.headers = headers req.body = OsoCloud::Helpers.to_hash(body) unless body.nil? end response.body rescue Faraday::Error => e handle_faraday_error e end def handle_faraday_error(error) resp = error.response err = if resp.nil? || resp[:body].nil? || resp[:body][:message].nil? error.message else resp[:body][:message] end raise ApiError.new(message: err) end end end end