Skip to content

Commit

Permalink
Add policy factory route and controller
Browse files Browse the repository at this point in the history
  • Loading branch information
micahlee committed Jul 21, 2023
1 parent c6971f2 commit b196a45
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 0 deletions.
159 changes: 159 additions & 0 deletions app/controllers/policy_factories_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# frozen_string_literal: true

# 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 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
11 changes: 11 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
192 changes: 192 additions & 0 deletions cucumber/api/features/policy_factory.feature
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions cucumber/api/features/support/policy-factory-context.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
--demo
Content-Disposition: form-data; name="value"

test value
--demo--

0 comments on commit b196a45

Please sign in to comment.