Skip to content

Commit a173698

Browse files
committed
Migrates Policy Factory to use Response objects
1 parent 2143815 commit a173698

File tree

10 files changed

+738
-130
lines changed

10 files changed

+738
-130
lines changed

app/controllers/policy_factories_controller.rb

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,27 @@
22

33
require 'base64'
44
require 'json'
5+
require './app/domain/responses'
56

67
class PolicyFactoriesController < RestController
7-
include AuthorizeResource
8-
98
before_action :current_user
109

1110
def create
12-
factory_resource = load_factory(kind: params[:kind], id: params[:id], account: params[:account])
13-
authorize(:execute, factory_resource)
14-
15-
response = Factory::CreateFromPolicyFactory.new.call(
16-
account: params[:account],
17-
factory_template: JSON.parse(Base64.decode64(factory_resource.secret.value)),
18-
request_body: JSON.parse(request.body.read),
19-
authorization: request.headers["Authorization"]
20-
)
11+
response = load_factory(kind: params[:kind], id: params[:id], account: params[:account])
12+
.bind do |factory|
13+
Factory::CreateFromPolicyFactory.new.call(
14+
account: params[:account],
15+
factory_template: JSON.parse(Base64.decode64(factory)),
16+
request_body: request.body.read,
17+
authorization: request.headers["Authorization"]
18+
)
19+
end
2120

22-
render(json: JSON.parse(response.body), status: :created) if response.code == 201
21+
if response.success?
22+
render(json: JSON.parse(response.result), status: :created)
23+
else
24+
render(json: error_response(response), status: response.status)
25+
end
2326
end
2427

2528
def info
@@ -32,8 +35,43 @@ def info
3235

3336
private
3437

35-
def load_factory(kind:, id:, account:)
36-
Resource["#{account}:variable:conjur/factories/#{kind}/#{id}"]
38+
def error_response(response)
39+
rtn = {
40+
status: response.status,
41+
body: { errors: [] }
42+
}
43+
if response.message.is_a?(Array)
44+
rtn[:body][:errors] == response.message
45+
else
46+
rtn[:body][:errors] << {
47+
message: response.message
48+
}
49+
end
3750
end
3851

52+
def load_factory(kind:, id:, account:)
53+
factory_resource = Resource["#{account}:variable:conjur/factories/#{kind}/#{id}"]
54+
if factory_resource.blank?
55+
return ::FailureResponse.new(
56+
"Policy Factory '#{kind}/#{id}' does not exist in account '#{account}'",
57+
status: :not_found
58+
)
59+
end
60+
61+
if current_user.allowed_to?(:execute, factory_resource)
62+
if factory_resource.secret.present?
63+
::SuccessResponse.new(factory_resource.secret.value)
64+
else
65+
::FailureResponse.new(
66+
"Policy Factory '#{kind}/#{id}' in account '#{account}' has not been initialized",
67+
status: :bad_request
68+
)
69+
end
70+
else
71+
::FailureResponse.new(
72+
"Role '#{current_user}' does not have access to Policy Factory '#{kind}/#{id}' does not exist in account '#{account}'",
73+
status: :forbidden
74+
)
75+
end
76+
end
3977
end

app/domain/factory/Readme.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Policy Factory
2+
3+
## Policy Factory Creation Requests
4+
5+
```plantuml
6+
@startuml
7+
start
8+
:Identify Factory\nvariable based\non request params;
9+
if (Can role load\nfactory variable?) then
10+
:Load Factory;
11+
:Extract Schema from Factory;
12+
if (Parse JSON body?) then
13+
if (Required keys missing?) then
14+
#pink:(400) - missing keys;
15+
kill
16+
else
17+
if (required values empty?) then
18+
#pink:(400) - missing values;
19+
kill
20+
else
21+
if (Policy rendered?) then
22+
if (Policy namespace path rendered?) then
23+
if (Policy successfully applied) then
24+
if (Factory has variables?) then
25+
if (Variable successfully set?) then
26+
:(201) Return policy response;
27+
end
28+
else
29+
#pink:(401) - setting variables not permitted;
30+
kill
31+
endif
32+
else
33+
:(201) Return policy response;
34+
end
35+
endif
36+
else
37+
#pink:(401) - policy creation not permitted;
38+
kill
39+
endif
40+
else
41+
#pink:(400) - policy namespace variable(s) missing;
42+
kill
43+
endif
44+
else
45+
#pink:(400) - policy variable(s) missing;
46+
kill
47+
endif
48+
endif
49+
endif
50+
else
51+
#pink:(400) - malformed JSON;
52+
kill
53+
endif
54+
else
55+
#pink:(404) - factory not available;
56+
kill
57+
endif
58+
@enduml
59+
```

app/domain/factory/create_from_policy_factory.rb

Lines changed: 178 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,67 +3,200 @@
33
require 'base64'
44
require 'rest_client'
55
require 'json_schemer'
6-
require 'factory/render_policy'
6+
require 'factory/renderer'
77

88
module Factory
99
class CreateFromPolicyFactory
10-
def initialize(base64: Base64, renderer: Factory::RenderPolicy.new, http: RestClient, schema_validator: JSONSchemer)
11-
@base64 = base64
10+
def initialize(renderer: Factory::Renderer.new, http: RestClient, schema_validator: JSONSchemer, success: SuccessResponse, failure: FailureResponse)
1211
@renderer = renderer
1312
@http = http
1413
@schema_validator = schema_validator
14+
@success = success
15+
@failure = failure
16+
17+
# JSON, URI, and Base64 are defined here for visibility. They
18+
# are not currently mocked in testing, thus, we're not setting
19+
# them in the initializer.
20+
@json = JSON
21+
@base64 = Base64
22+
@uri = URI
1523
end
1624

17-
def validate!(schema:, params:)
25+
def validate_and_transform_request(schema:, params:)
26+
return @failure.new("Request body must be JSON", status: :bad_request) if params.blank?
27+
28+
begin
29+
params = @json.parse(params)
30+
rescue
31+
return @failure.new("Request body must be valid JSON", status: :bad_request)
32+
end
33+
34+
# Strip keys without values
35+
params = params.select{|_, v| v.present? }
36+
1837
validator = @schema_validator.schema(schema)
19-
return if validator.valid?(params)
20-
21-
error = validator.validate(params).first
22-
case error['type']
23-
when 'required'
24-
missing_attributes = error['details']['missing_keys'].map{|key| [ error['data_pointer'], key].reject{|item| item.empty?}.join('/') }.join("', '")
25-
raise "The following JSON attributes are missing: '#{missing_attributes}'"
26-
else
27-
raise "Generic JSON Schema validation error: type => '#{error['type']}', details => '#{error['type'].inspect}'"
38+
return @success.new(params) if validator.valid?(params)
39+
40+
errors = validator.validate(params).map do |error|
41+
case error['type']
42+
when 'required'
43+
missing_attributes = error['details']['missing_keys'].map{|key| [ error['data_pointer'], key].reject(&:empty?).join('/') } #.join("', '")
44+
missing_attributes.map do |attribute|
45+
{
46+
message: "Missing JSON key or value for: '#{attribute}'",
47+
key: attribute
48+
}
49+
end
50+
else
51+
{
52+
message: "Generic JSON Schema validation error: type => '#{error['type']}', details => '#{error['type'].inspect}'"
53+
}
54+
end
55+
end
56+
@failure.new(errors.flatten, status: :bad_request)
57+
end
58+
59+
def render_and_apply_policy(policy_load_path:, policy_template:, variables:, account:, authorization:)
60+
@renderer.render(
61+
template: policy_template,
62+
variables: variables
63+
).bind do |rendered_policy|
64+
response = @http.post(
65+
"http://localhost:3000/policies/#{account}/policy/#{policy_load_path}",
66+
rendered_policy,
67+
'Authorization' => authorization
68+
)
69+
if response.code == 201
70+
@success.new(response.body)
71+
else
72+
case response.code
73+
when 400
74+
@failure.new("Failed to apply generated Policy to '#{policy_load_path}'", status: :bad_request)
75+
when 401
76+
@failure.new("Unauthorized to apply generated policy to '#{policy_load_path}'", status: :unauthorized)
77+
when 403
78+
@failure.new("Forbidden to apply generated policy to '#{policy_load_path}'", status: :forbidden)
79+
when 404
80+
@failure.new("Unable to apply generated policy to '#{policy_load_path}'", status: :not_found)
81+
else
82+
@failure.new(
83+
"Failed to apply generated policy to '#{policy_load_path}'. Status Code: '#{response.code}, Response: '#{response.body}''",
84+
status: :bad_request
85+
)
86+
end
87+
end
88+
end
89+
end
90+
91+
def set_factory_variables(schema_variables:, factory_variables:, variable_path:, authorization:, account:)
92+
schema_variables.each_key do |factory_variable|
93+
next unless factory_variables.key?(factory_variable)
94+
95+
variable_id = @uri.encode_www_form_component("#{variable_path}/#{factory_variable}")
96+
secret_path = "secrets/#{account}/variable/#{variable_id}"
97+
98+
response = @http.post(
99+
"http://localhost:3000/#{secret_path}",
100+
factory_variables[factory_variable].to_s,
101+
{ 'Authorization' => authorization }
102+
)
103+
next if response.code == 201
104+
105+
case response.code
106+
when 401
107+
return @failure.new("Role is unauthorized to set variable: '#{secret_path}'", status: :unauthorized)
108+
when 403
109+
return @failure.new("Role lacks the privilege to set variable: '#{secret_path}'", status: :forbidden)
110+
else
111+
return @failure.new(
112+
"Failed to set variable: '#{secret_path}'. Status Code: '#{response.code}', Response: '#{response.body}'",
113+
status: :bad_request
114+
)
115+
end
28116
end
117+
@success.new('Variables successfully set')
29118
end
30119

31120
def call(factory_template:, request_body:, account:, authorization:)
32-
request_body = request_body.select{|_, v| v.present? }
33-
validate!(
121+
validate_and_transform_request(
34122
schema: factory_template['schema'],
35123
params: request_body
36-
)
37-
38-
# Convert `dashed` keys to `underscored`. This only occurs for top-level parameters.
39-
# Conjur variables should be use dashes rather than underscores.
40-
template_variables = request_body.transform_keys { |key| key.to_s.underscore }
41-
42-
# Render the policy from the template and provided values
43-
policy_template = @base64.decode64(factory_template['policy'])
44-
45-
# Push rendered policy to the desired policy branch
46-
policy_load_path = @renderer.render(template: factory_template['policy_namespace'], variables: template_variables)
47-
response = @http.post(
48-
"http://localhost:3000/policies/#{account}/policy/#{policy_load_path}",
49-
@renderer.render(template: policy_template, variables: template_variables),
50-
'Authorization' => authorization
51-
)
52-
53-
if factory_template['schema']['properties'].key?('variables')
54-
variable_path = @renderer.render(template: "#{factory_template['policy_namespace']}/<%= id %>", variables: template_variables)
55-
factory_template['schema']['properties']['variables']['properties'].each_key do |factory_variable|
56-
variable_id = URI.encode_www_form_component("#{variable_path}/#{factory_variable}")
57-
58-
@http.post(
59-
"http://localhost:3000/secrets/#{account}/variable/#{variable_id}",
60-
# All values must be sent to Conjur as strings
61-
template_variables['variables'][factory_variable].to_s,
62-
{ 'Authorization' => authorization }
63-
)
124+
).bind do |body_variables|
125+
# Convert `dashed` keys to `underscored`. This only occurs for top-level parameters.
126+
# Conjur variables should be use dashes rather than underscores.
127+
template_variables = body_variables.transform_keys { |key| key.to_s.underscore }
128+
129+
# Render the policy from the template and provided values
130+
policy_template = @base64.decode64(factory_template['policy'])
131+
132+
# Push rendered policy to the desired policy branch
133+
@renderer.render(template: factory_template['policy_namespace'], variables: template_variables)
134+
.bind do |policy_load_path|
135+
valid_variables = factory_template['schema']['properties'].keys - ['variables']
136+
render_and_apply_policy(
137+
policy_load_path: policy_load_path,
138+
policy_template: policy_template,
139+
variables: template_variables.select { |k,_| valid_variables.include?(k) },
140+
account: account,
141+
authorization: authorization
142+
).bind do |result|
143+
return @success.new(result) unless factory_template['schema']['properties'].key?('variables')
144+
145+
# Set Policy Factory variables
146+
@renderer.render(template: "#{factory_template['policy_namespace']}/<%= id %>", variables: template_variables)
147+
.bind { |variable_path|
148+
set_factory_variables(
149+
schema_variables: factory_template['schema']['properties']['variables']['properties'],
150+
factory_variables: template_variables['variables'],
151+
variable_path: variable_path,
152+
authorization: authorization,
153+
account: account
154+
)
155+
}.bind {
156+
# If variables were added successfully, return the result so that
157+
# we send the policy load response back to the client.
158+
@success.new(result)
159+
}
160+
end
161+
end
64162
end
65-
end
66-
response
163+
164+
165+
# # Push rendered policy to the desired policy branch
166+
# policy_post = @renderer.render(template: factory_template['policy_namespace'], variables: template_variables)
167+
# .bind do |policy_load_path|
168+
# render_and_apply_policy(
169+
# policy_load_path: policy_load_path,
170+
# policy_template: policy_template,
171+
# # TODO: restrict the scope to the first level (exclude variables) if present
172+
# variables: @json.parse(params)
173+
# )
174+
# end
175+
# .bind do |result|
176+
# return result unless factory_template['schema']['properties'].key?('variables')
177+
178+
# apply_variables(
179+
# schema_variables: factory_template['schema']['properties']['variables']['properties'],
180+
# variable_path: @renderer.render(template: "#{factory_template['policy_namespace']}/<%= id %>", variables: template_variables)
181+
# )
182+
# end
183+
184+
# return policy_post unless policy_post.success?
185+
186+
# if factory_template['schema']['properties'].key?('variables')
187+
# variable_path = @renderer.render(template: "#{factory_template['policy_namespace']}/<%= id %>", variables: template_variables)
188+
# factory_template['schema']['properties']['variables']['properties'].each_key do |factory_variable|
189+
# variable_id = URI.encode_www_form_component("#{variable_path}/#{factory_variable}")
190+
191+
# @http.post(
192+
# "http://localhost:3000/secrets/#{account}/variable/#{variable_id}",
193+
# # All values must be sent to Conjur as strings
194+
# template_variables['variables'][factory_variable].to_s,
195+
# { 'Authorization' => authorization }
196+
# )
197+
# end
198+
# end
199+
# policy_post
67200
end
68201
end
69202
end

0 commit comments

Comments
 (0)