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