diff --git a/app/controllers/policy_factories_controller.rb b/app/controllers/policy_factories_controller.rb new file mode 100644 index 0000000000..1d75ad7257 --- /dev/null +++ b/app/controllers/policy_factories_controller.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require 'util/multipart' + +# This controller is responsible for creating host records using +# host factory tokens for authorization. +class PolicyFactoriesController < ApplicationController + include FindResource + include AuthorizeResource + + RenderContext = Struct.new(:role, :params) do + def get_binding + binding + end + end + + def create_policy + authorize :execute + + factory = ::PolicyFactory[resource_id] + + template = Conjur::PolicyParser::YAML::Loader.load(factory.template) + + context = RenderContext.new(current_user, params) + + template = update_array(template, context) + + policy_text = template.to_yaml + + response = load_policy(factory.base_policy, policy_text, policy_context) unless dry_run? + + response = { + policy_text: policy_text, + load_to: factory.base_policy.identifier, + dry_run: dry_run?, + response: response + } + render json: response, status: :created + end + + def update_record(record, context) + fields = record.class.fields.keys + + if record.is_a?(Conjur::PolicyParser::Types::Policy) + fields << 'body' + end + + fields.each do |name| + record_value = record.send(name) + + if record_value.class < Conjur::PolicyParser::Types::Base + update_record(record_value, context) + elsif record_value.is_a?(Array) + update_array(record_value, context) + elsif record_value.is_a?(Hash) + update_hash(record_value, context) + elsif record_value.is_a?(String) + rendered_value = ERB.new(record_value).result(context.get_binding) + record.send("#{name}=", rendered_value) + end + end + + record + end + + def update_array(arr, context) + arr.map! do |item| + if item.class < Conjur::PolicyParser::Types::Base + update_record(item, context) + elsif item.is_a?(Array) + update_array(item, context) + elsif item.is_a?(Hash) + update_hash(item, context) + elsif item.is_a?(String) + ERB.new(item).result(context.get_binding) + else + item + end + end + + arr + end + + def update_hash(hsh, context) + hsh.each do |k, val| + if val.class < Conjur::PolicyParser::Types::Base + update_record(val, context) + elsif val.is_a?(Array) + update_array(val, context) + elsif val.is_a?(Hash) + update_hash(val, context) + elsif val.is_a?(String) + hsh[k] = ERB.new(val).result(context.get_binding) + end + end + end + + def get_template + authorize :read + + factory = ::PolicyFactory[resource_id] + + response = { + body: factory.template + } + + render json: response + end + + def update_template + authorize :update + + factory = ::PolicyFactory[resource_id] + + factory.template = request.body.read + factory.save + + response = { + body: factory.template + } + + render json: response, status: :accepted + end + + protected + + def policy_context + multipart_data.reject { |k,v| k == :policy } + end + + def multipart_data + return {} if request.raw_post.empty? + + @multipart_data ||= Util::Multipart.parse_multipart_data( + request.raw_post, + content_type: request.headers['CONTENT_TYPE'] + ) + end + + def dry_run? + params[:dry_run].present? + end + + def resource_kind + 'policy_factory' + end + + def load_policy(load_to, policy_text, policy_context) + policy_version = PolicyVersion.new( + role: current_user, + policy: load_to, + policy_text: policy_text, + client_ip: request.ip + ) + policy_version.delete_permitted = false + policy = policy_version.save + + policy_action = Loader::CreatePolicy.from_policy(policy, context: policy_context) + policy_action.call + + created_roles = policy_action.new_roles.select do |role| + %w(user host).member?(role.kind) + end.inject({}) do |memo, role| + credentials = Credentials[role: role] || Credentials.create(role: role) + memo[role.id] = { id: role.id, api_key: credentials.api_key } + memo + end + + { + created_roles: created_roles, + version: policy_version.version + } + end +end diff --git a/config/routes.rb b/config/routes.rb index e1f4db4a66..004c5961ab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -83,6 +83,17 @@ def matches?(request) get "/public_keys/:account/:kind/*identifier" => 'public_keys#show' post "/ca/:account/:service_id/sign" => 'certificate_authority#sign' + + # Policy Factory routes + scope '/policy_factories/:account/*identifier' do + # The `/template` routes need to be listed before create policy, so + # that `create_policy` doesn't attempt to include `/template` in the + # policy factory ID. + get '/template' => 'policy_factories#get_template' + put '/template' => 'policy_factories#update_template' + + post '/' => 'policy_factories#create_policy' + end end post "/host_factories/hosts" => 'host_factories#create_host' diff --git a/cucumber/api/features/policy_factory.feature b/cucumber/api/features/policy_factory.feature new file mode 100644 index 0000000000..edfa907323 --- /dev/null +++ b/cucumber/api/features/policy_factory.feature @@ -0,0 +1,192 @@ +Feature: Policy Factory + + Background: + Given I am the super-user + And I create a new user "alice" + And I create a new user "bob" + And I successfully PATCH "/policies/cucumber/policy/root" with body: + """ + - !policy certificates + - !policy-factory + id: certificates + base: !policy certificates + template: + - !variable + id: <%=role.identifier%> + annotations: + provision/provisioner: context + provision/context/parameter: value + + - !permit + role: !user + id: /<%=role.identifier%> + resource: !variable + id: <%=role.identifier%> + privileges: [ read, execute ] + + - !policy nested-policy + - !policy-factory + id: nested-policy + owner: !user alice + base: !policy nested-policy + template: + - !host + id: outer-<%=role.identifier%> + owner: !user /<%=role.identifier%> + annotations: + outer: <%=role.identifier%> + + - !policy + id: inner + owner: !user /<%=role.identifier%> + body: + - !host + id: inner-<%=role.identifier%> + annotations: + inner: <%=role.identifier%> + + - !policy edit-template + - !policy-factory + id: edit-template + owner: !user alice + base: !policy edit-template + template: + - !variable to-be-edited + + - !policy-factory + id: root-factory + template: + - !variable created-in-root + + - !policy annotated-variables + - !policy-factory + id: parameterized + base: !policy annotated-variables + template: + - !variable + id: <%=role.identifier%> + annotations: + description: <%=params[:description]%> + + - !permit + role: !user bob + resource: !policy-factory parameterized + privileges: [ read ] + + - !permit + role: !user alice + resource: !policy-factory certificates + privileges: [ read, execute ] + + - !permit + role: !user alice + resource: !policy-factory parameterized + privileges: [ read, execute ] + """ + + Scenario: Dry run loading policy using a factory + Given I login as "alice" + + When I POST "/policy_factories/cucumber/certificates?dry_run=true" + Then the JSON should be: + """ + { + "policy_text": "---\n- !variable\n id: alice\n annotations:\n provision/provisioner: context\n provision/context/parameter: value\n- !permit\n privilege:\n - read\n - execute\n role: !user\n id: \"/alice\"\n resource: !variable\n id: alice\n", + "load_to": "certificates", + "dry_run": true, + "response": null + } + """ + + Scenario: Nested policy within factory template + Given I login as "alice" + When I successfully POST "/policy_factories/cucumber/nested-policy" + Then I successfully GET "/resources/cucumber/host/nested-policy/outer-alice" + Then I successfully GET "/resources/cucumber/host/nested-policy/inner/inner-alice" + + Scenario: Load policy using a factory + Given I login as "alice" + And I set the "Content-Type" header to "multipart/form-data; boundary=demo" + When I successfully POST "/policy_factories/cucumber/certificates" with body from file "policy-factory-context.txt" + Then the JSON should be: + """ + { + "policy_text": "---\n- !variable\n id: alice\n annotations:\n provision/provisioner: context\n provision/context/parameter: value\n- !permit\n privilege:\n - read\n - execute\n role: !user\n id: \"/alice\"\n resource: !variable\n id: alice\n", + "load_to": "certificates", + "dry_run": false, + "response": { + "created_roles": { + }, + "version": 1 + } + } + """ + And I successfully GET "/secrets/cucumber/variable/certificates/alice" + Then the JSON should be: + """ + "test value" + """ + + Scenario: Load parameterized policy using a factory + Given I login as "alice" + + When I POST "/policy_factories/cucumber/parameterized?description=first%20description" + Then the JSON should be: + """ + { + "policy_text": "---\n- !variable\n id: alice\n annotations:\n description: first description\n", + "load_to": "annotated-variables", + "dry_run": false, + "response": { + "created_roles": { + }, + "version": 1 + } + } + """ + + Scenario: Get a 404 response without read permission + Given I login as "bob" + When I POST "/policy_factories/cucumber/certificates" + Then the HTTP response status code is 404 + + Scenario: Get a 403 response without execute permission + Given I login as "bob" + When I POST "/policy_factories/cucumber/parameterized" + Then the HTTP response status code is 403 + + Scenario: A policy factory without a base loads into the root policy + Given I POST "/policy_factories/cucumber/root-factory" + And the HTTP response status code is 201 + Then I successfully GET "/resources/cucumber/variable/created-in-root" + + Scenario: I retrieve the policy factory template through the API + Given I login as "alice" + When I GET "/policy_factories/cucumber/edit-template/template" + Then the HTTP response status code is 200 + And the JSON response should be: + """ + { + "body": "---\n- !variable\n id: to-be-edited\n" + } + """ + + Scenario: I update the policy factory template through the API + Given I login as "alice" + When I PUT "/policy_factories/cucumber/edit-template/template" with body: + """ + ---\n- !variable replaced + """ + Then the HTTP response status code is 202 + When I GET "/policy_factories/cucumber/edit-template/template" + Then the JSON response should be: + """ + { + "body": "---\\n- !variable replaced" + } + """ + + Scenario: I don't have permission to retrieve the policy factory template + Given I login as "bob" + When I GET "/policy_factories/cucumber/edit-template/template" + Then the HTTP response status code is 404 diff --git a/cucumber/api/features/support/policy-factory-context.txt b/cucumber/api/features/support/policy-factory-context.txt new file mode 100644 index 0000000000..9defffb2e7 --- /dev/null +++ b/cucumber/api/features/support/policy-factory-context.txt @@ -0,0 +1,5 @@ +--demo +Content-Disposition: form-data; name="value" + +test value +--demo--