#!/usr/bin/env ruby require 'bundler/inline' gemfile do source 'https://rubygems.org' gem "declarative_policy", "~> 1.0" gem "erb", "~> 4.0" gem "globalid", "~> 1.0" gem "google-protobuf", "~> 3.0" gem "json", "~> 2.0" gem "logger", "~> 1.0" gem "rack", "~> 3.0" gem "rackup", "~> 2.0" gem "securerandom", "~> 0.1" gem "twirp", "~> 1.0" gem "webrick", "~> 1.0" end lib_path = Pathname.new(__FILE__).parent.parent.join('lib').realpath.to_s $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path) require 'authx/rpc' $scheme = ENV.fetch("SCHEME", "http") $port = ENV.fetch("PORT", 8284).to_i $host = ENV.fetch("HOST", "localhost:#{$port}") class Entity class << self def all @items ||= [] end def create!(attributes) new({ id: SecureRandom.uuid }.merge(attributes)).tap do |item| all << item end end end def initialize(attributes = {}) @attributes = attributes end def id self[:id] end def [](attribute) @attributes.fetch(attribute) end def to_h @attributes end def to_gid ::GlobalID.create(self, app: "example") end end class Organization < Entity class << self def default @default ||= create!(id: SecureRandom.uuid) end end end class Project < Entity end module HTTPHelpers def authorized?(request, permission, resource) authorization = Rack::Auth::AbstractRequest.new(request.env) return false unless authorization.provided? response = rpc.allowed({ subject: authorization.params, permission: permission, resource: resource.to_gid.to_s, }, headers: { 'Authorization' => "Bearer #{authorization.params}"}) response.error.nil? && response.data.result end def json_not_found http_response(code: 404) end def json_ok(body) http_response(code: 200, body: JSON.pretty_generate(body)) end def json_created(body) http_response(code: 201, body: JSON.pretty_generate(body.to_h)) end def json_unauthorized(permission, resource) http_response(code: 401, body: JSON.pretty_generate({ error: { code: 401, message: "`#{permission}` is required on `#{resource.to_gid}`", } })) end def http_response(code:, headers: { 'Content-Type' => 'application/json' }, body: nil) [ code, headers.merge({ 'X-Backend-Server' => 'REST' }), [body].compact ] end end class API include HTTPHelpers attr_reader :rpc def initialize @rpc = ::Authx::Rpc::AbilityClient.new("http://idp.example.com:8080/twirp") end def call(env) request = Rack::Request.new(env) case request.request_method when Rack::GET case request.path when "/organizations", "/organizations.json" return json_ok(Organization.all.map(&:to_h)) when "/projects", "/projects.json" resource = Organization.default if authorized?(request, :read_project, resource) return json_ok(Project.all.map(&:to_h)) else return json_unauthorized(:read_project, resource) end end when Rack::POST case request.path when "/projects", "/projects.json" resource = Organization.default if authorized?(request, :create_project, resource) return json_created(Project.create!(JSON.parse(request.body.read, symbolize_names: true))) else return json_unauthorized(:create_project, resource) end end end json_not_found end private end if __FILE__ == $0 app = Rack::Builder.new do use Rack::CommonLogger use Rack::Reloader run API.new end.to_app Rackup::Server.start(app: app, Port: $port) end