main
  1#!/usr/bin/env ruby
  2
  3require 'bundler/inline'
  4
  5gemfile do
  6  source 'https://rubygems.org'
  7
  8  gem "csv", "~> 3.0"
  9  gem "declarative_policy", "~> 1.0"
 10  gem "erb", "~> 4.0"
 11  gem "globalid", "~> 1.0"
 12  gem "google-protobuf", "~> 3.0"
 13  gem "json", "~> 2.0"
 14  gem "logger", "~> 1.0"
 15  gem "rack", "~> 3.0"
 16  gem "rackup", "~> 2.0"
 17  gem "securerandom", "~> 0.1"
 18  gem "twirp", "~> 1.0"
 19  gem "webrick", "~> 1.0"
 20end
 21
 22lib_path = Pathname.new(__FILE__).parent.parent.join('lib').realpath.to_s
 23$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
 24
 25require 'authx/rpc'
 26
 27$scheme = ENV.fetch("SCHEME", "http")
 28$port = ENV.fetch("PORT", 8284).to_i
 29$host = ENV.fetch("HOST", "localhost:#{$port}")
 30
 31class Entity
 32  class << self
 33    def all
 34      @items ||= ::CSV.read(File.join(__dir__, "../db/api/#{self.name.downcase}s.csv"), headers: true).map do |row|
 35        new(row.to_h.transform_keys(&:to_sym))
 36      end
 37    end
 38
 39    def create!(attributes)
 40      new({ id: SecureRandom.uuid }.merge(attributes)).tap do |item|
 41        all << item
 42      end
 43    end
 44  end
 45
 46  def initialize(attributes = {})
 47    @attributes = attributes
 48  end
 49
 50  def id
 51    self[:id]
 52  end
 53
 54  def [](attribute)
 55    @attributes.fetch(attribute.to_sym)
 56  end
 57
 58  def to_h
 59    @attributes
 60  end
 61
 62  def to_gid
 63    ::GlobalID.create(self, app: "example")
 64  end
 65end
 66
 67class Organization < Entity
 68  class << self
 69    def default
 70      @default ||= all.find { |organization| organization[:name] == "default" }
 71    end
 72  end
 73end
 74
 75class Group < Entity
 76end
 77
 78class Project < Entity
 79end
 80
 81module HTTPHelpers
 82  def authorized?(request, permission, resource)
 83    raise [permission, resource].inspect if resource.nil?
 84    authorization = Rack::Auth::AbstractRequest.new(request.env)
 85    return false unless authorization.provided?
 86
 87    response = rpc.allowed({
 88      subject: authorization.params,
 89      permission: permission,
 90      resource: resource.to_gid.to_s,
 91    }, headers: { 'Authorization' => "Bearer #{authorization.params}"})
 92    puts [response&.data&.result, permission, resource.to_gid.to_s].inspect
 93    response.error.nil? && response.data.result
 94  end
 95
 96  def json_not_found
 97    http_response(code: 404)
 98  end
 99
100  def json_ok(body)
101    http_response(code: 200, body: JSON.pretty_generate(body))
102  end
103
104  def json_created(body)
105    http_response(code: 201, body: JSON.pretty_generate(body.to_h))
106  end
107
108  def json_unauthorized(permission, resource)
109    http_response(code: 401, body: JSON.pretty_generate({
110      error: {
111        code: 401,
112        message: "`#{permission}` is required on `#{resource.to_gid}`",
113      }
114    }))
115  end
116
117  def http_response(code:, headers: { 'Content-Type' => 'application/json' }, body: nil)
118    [
119      code,
120      headers.merge({ 'X-Backend-Server' => 'REST' }),
121      [body].compact
122    ]
123  end
124end
125
126class API
127  include HTTPHelpers
128
129  attr_reader :rpc
130
131  def initialize
132    @rpc = ::Authx::Rpc::AbilityClient.new("http://idp.example.com:8080/twirp")
133  end
134
135  def call(env)
136    request = Rack::Request.new(env)
137    case request.request_method
138    when Rack::GET
139      case request.path
140      when "/organizations", "/organizations.json"
141        return json_ok(Organization.all.map(&:to_h))
142      when "/groups", "/groups.json"
143        resource = Organization.default
144        if authorized?(request, :read_group, resource)
145          return json_ok(Group.all.map(&:to_h))
146        else
147          return json_unauthorized(:read_group, resource)
148        end
149      when "/projects", "/projects.json"
150        resource = Organization.default
151        if authorized?(request, :read_project, resource)
152          return json_ok(Project.all.map(&:to_h))
153        else
154          return json_unauthorized(:read_project, resource)
155        end
156      end
157    when Rack::POST
158      case request.path
159      when "/projects", "/projects.json"
160        resource = Organization.default
161        if authorized?(request, :create_project, resource)
162          return json_created(Project.create!(JSON.parse(request.body.read, symbolize_names: true)))
163        else
164          return json_unauthorized(:create_project, resource)
165        end
166      end
167    end
168    json_not_found
169  end
170
171  private
172end
173
174if __FILE__ == $0
175  app = Rack::Builder.new do
176    use Rack::CommonLogger
177    use Rack::Reloader
178    run API.new
179  end.to_app
180
181  Rackup::Server.start(app: app, Port: $port)
182end