|
3 | 3 | require 'base64'
|
4 | 4 | require 'rest_client'
|
5 | 5 | require 'json_schemer'
|
6 |
| -require 'factory/render_policy' |
| 6 | +require 'factory/renderer' |
7 | 7 |
|
8 | 8 | module Factory
|
9 | 9 | 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) |
12 | 11 | @renderer = renderer
|
13 | 12 | @http = http
|
14 | 13 | @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 |
15 | 23 | end
|
16 | 24 |
|
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 | + |
18 | 37 | 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 |
28 | 116 | end
|
| 117 | + @success.new('Variables successfully set') |
29 | 118 | end
|
30 | 119 |
|
31 | 120 | def call(factory_template:, request_body:, account:, authorization:)
|
32 |
| - request_body = request_body.select{|_, v| v.present? } |
33 |
| - validate!( |
| 121 | + validate_and_transform_request( |
34 | 122 | schema: factory_template['schema'],
|
35 | 123 | 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 |
64 | 162 | 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 |
67 | 200 | end
|
68 | 201 | end
|
69 | 202 | end
|
0 commit comments