Skip to content

Commit 33d4bae

Browse files
committed
Add policy factory route and controller
1 parent 2c8d642 commit 33d4bae

File tree

4 files changed

+382
-0
lines changed

4 files changed

+382
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# frozen_string_literal: true
2+
3+
require 'util/multipart'
4+
5+
# This controller is responsible for creating host records using
6+
# host factory tokens for authorization.
7+
class PolicyFactoriesController < ApplicationController
8+
include FindResource
9+
include AuthorizeResource
10+
11+
RenderContext = Struct.new(:role, :params) do
12+
def get_binding
13+
binding
14+
end
15+
end
16+
17+
def create_policy
18+
authorize :execute
19+
20+
factory = ::PolicyFactory[resource_id]
21+
22+
template = Conjur::PolicyParser::YAML::Loader.load(factory.template)
23+
24+
context = RenderContext.new(current_user, params)
25+
26+
template = update_array(template, context)
27+
28+
policy_text = template.to_yaml
29+
30+
response = load_policy(factory.base_policy, policy_text, policy_context) unless dry_run?
31+
32+
response = {
33+
policy_text: policy_text,
34+
load_to: factory.base_policy.identifier,
35+
dry_run: dry_run?,
36+
response: response
37+
}
38+
render json: response, status: :created
39+
end
40+
41+
def update_record(record, context)
42+
fields = record.class.fields.keys
43+
44+
if record.is_a?(Conjur::PolicyParser::Types::Policy)
45+
fields << 'body'
46+
end
47+
48+
fields.each do |name|
49+
record_value = record.send(name)
50+
51+
if record_value.class < Conjur::PolicyParser::Types::Base
52+
update_record(record_value, context)
53+
elsif record_value.is_a?(Array)
54+
update_array(record_value, context)
55+
elsif record_value.is_a?(Hash)
56+
update_hash(record_value, context)
57+
elsif record_value.is_a?(String)
58+
rendered_value = ERB.new(record_value).result(context.get_binding)
59+
record.send("#{name}=", rendered_value)
60+
end
61+
end
62+
63+
record
64+
end
65+
66+
def update_array(arr, context)
67+
arr.map! do |item|
68+
if item.class < Conjur::PolicyParser::Types::Base
69+
update_record(item, context)
70+
elsif item.is_a?(Array)
71+
update_array(item, context)
72+
elsif item.is_a?(Hash)
73+
update_hash(item, context)
74+
elsif item.is_a?(String)
75+
ERB.new(item).result(context.get_binding)
76+
else
77+
item
78+
end
79+
end
80+
81+
arr
82+
end
83+
84+
def update_hash(hsh, context)
85+
hsh.each do |k, val|
86+
if val.class < Conjur::PolicyParser::Types::Base
87+
update_record(val, context)
88+
elsif val.is_a?(Array)
89+
update_array(val, context)
90+
elsif val.is_a?(Hash)
91+
update_hash(val, context)
92+
elsif val.is_a?(String)
93+
hsh[k] = ERB.new(val).result(context.get_binding)
94+
end
95+
end
96+
end
97+
98+
def get_template
99+
authorize :read
100+
101+
factory = ::PolicyFactory[resource_id]
102+
103+
response = {
104+
body: factory.template
105+
}
106+
107+
render json: response
108+
end
109+
110+
def update_template
111+
authorize :update
112+
113+
factory = ::PolicyFactory[resource_id]
114+
115+
factory.template = request.body.read
116+
factory.save
117+
118+
response = {
119+
body: factory.template
120+
}
121+
122+
render json: response, status: :accepted
123+
end
124+
125+
protected
126+
127+
def policy_context
128+
multipart_data.reject { |k,v| k == :policy }
129+
end
130+
131+
def multipart_data
132+
return {} if request.raw_post.empty?
133+
134+
@multipart_data ||= Util::Multipart.parse_multipart_data(
135+
request.raw_post,
136+
content_type: request.headers['CONTENT_TYPE']
137+
)
138+
end
139+
140+
def dry_run?
141+
params[:dry_run].present?
142+
end
143+
144+
def resource_kind
145+
'policy_factory'
146+
end
147+
148+
def load_policy(load_to, policy_text, policy_context)
149+
policy_version = PolicyVersion.new(
150+
role: current_user,
151+
policy: load_to,
152+
policy_text: policy_text,
153+
client_ip: request.ip
154+
)
155+
policy_version.delete_permitted = false
156+
policy = policy_version.save
157+
158+
policy_action = Loader::CreatePolicy.from_policy(policy, context: policy_context)
159+
policy_action.call
160+
161+
created_roles = policy_action.new_roles.select do |role|
162+
%w(user host).member?(role.kind)
163+
end.inject({}) do |memo, role|
164+
credentials = Credentials[role: role] || Credentials.create(role: role)
165+
memo[role.id] = { id: role.id, api_key: credentials.api_key }
166+
memo
167+
end
168+
169+
{
170+
created_roles: created_roles,
171+
version: policy_version.version
172+
}
173+
end
174+
end

config/routes.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ def matches?(request)
8383
get "/public_keys/:account/:kind/*identifier" => 'public_keys#show'
8484

8585
post "/ca/:account/:service_id/sign" => 'certificate_authority#sign'
86+
87+
# Policy Factory routes
88+
scope '/policy_factories/:account/*identifier' do
89+
# The `/template` routes need to be listed before create policy, so
90+
# that `create_policy` doesn't attempt to include `/template` in the
91+
# policy factory ID.
92+
get '/template' => 'policy_factories#get_template'
93+
put '/template' => 'policy_factories#update_template'
94+
95+
post '/' => 'policy_factories#create_policy'
96+
end
8697
end
8798

8899
post "/host_factories/hosts" => 'host_factories#create_host'
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
Feature: Policy Factory
2+
3+
Background:
4+
Given I am the super-user
5+
And I create a new user "alice"
6+
And I create a new user "bob"
7+
And I successfully PATCH "/policies/cucumber/policy/root" with body:
8+
"""
9+
- !policy certificates
10+
- !policy-factory
11+
id: certificates
12+
base: !policy certificates
13+
template:
14+
- !variable
15+
id: <%=role.identifier%>
16+
annotations:
17+
provision/provisioner: context
18+
provision/context/parameter: value
19+
20+
- !permit
21+
role: !user
22+
id: /<%=role.identifier%>
23+
resource: !variable
24+
id: <%=role.identifier%>
25+
privileges: [ read, execute ]
26+
27+
- !policy nested-policy
28+
- !policy-factory
29+
id: nested-policy
30+
owner: !user alice
31+
base: !policy nested-policy
32+
template:
33+
- !host
34+
id: outer-<%=role.identifier%>
35+
owner: !user /<%=role.identifier%>
36+
annotations:
37+
outer: <%=role.identifier%>
38+
39+
- !policy
40+
id: inner
41+
owner: !user /<%=role.identifier%>
42+
body:
43+
- !host
44+
id: inner-<%=role.identifier%>
45+
annotations:
46+
inner: <%=role.identifier%>
47+
48+
- !policy edit-template
49+
- !policy-factory
50+
id: edit-template
51+
owner: !user alice
52+
base: !policy edit-template
53+
template:
54+
- !variable to-be-edited
55+
56+
- !policy-factory
57+
id: root-factory
58+
template:
59+
- !variable created-in-root
60+
61+
- !policy annotated-variables
62+
- !policy-factory
63+
id: parameterized
64+
base: !policy annotated-variables
65+
template:
66+
- !variable
67+
id: <%=role.identifier%>
68+
annotations:
69+
description: <%=params[:description]%>
70+
71+
- !permit
72+
role: !user bob
73+
resource: !policy-factory parameterized
74+
privileges: [ read ]
75+
76+
- !permit
77+
role: !user alice
78+
resource: !policy-factory certificates
79+
privileges: [ read, execute ]
80+
81+
- !permit
82+
role: !user alice
83+
resource: !policy-factory parameterized
84+
privileges: [ read, execute ]
85+
"""
86+
87+
Scenario: Dry run loading policy using a factory
88+
Given I login as "alice"
89+
90+
When I POST "/policy_factories/cucumber/certificates?dry_run=true"
91+
Then the JSON should be:
92+
"""
93+
{
94+
"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",
95+
"load_to": "certificates",
96+
"dry_run": true,
97+
"response": null
98+
}
99+
"""
100+
101+
Scenario: Nested policy within factory template
102+
Given I login as "alice"
103+
When I successfully POST "/policy_factories/cucumber/nested-policy"
104+
Then I successfully GET "/resources/cucumber/host/nested-policy/outer-alice"
105+
Then I successfully GET "/resources/cucumber/host/nested-policy/inner/inner-alice"
106+
107+
Scenario: Load policy using a factory
108+
Given I login as "alice"
109+
And I set the "Content-Type" header to "multipart/form-data; boundary=demo"
110+
When I successfully POST "/policy_factories/cucumber/certificates" with body from file "policy-factory-context.txt"
111+
Then the JSON should be:
112+
"""
113+
{
114+
"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",
115+
"load_to": "certificates",
116+
"dry_run": false,
117+
"response": {
118+
"created_roles": {
119+
},
120+
"version": 1
121+
}
122+
}
123+
"""
124+
And I successfully GET "/secrets/cucumber/variable/certificates/alice"
125+
Then the JSON should be:
126+
"""
127+
"test value"
128+
"""
129+
130+
Scenario: Load parameterized policy using a factory
131+
Given I login as "alice"
132+
133+
When I POST "/policy_factories/cucumber/parameterized?description=first%20description"
134+
Then the JSON should be:
135+
"""
136+
{
137+
"policy_text": "---\n- !variable\n id: alice\n annotations:\n description: first description\n",
138+
"load_to": "annotated-variables",
139+
"dry_run": false,
140+
"response": {
141+
"created_roles": {
142+
},
143+
"version": 1
144+
}
145+
}
146+
"""
147+
148+
Scenario: Get a 404 response without read permission
149+
Given I login as "bob"
150+
When I POST "/policy_factories/cucumber/certificates"
151+
Then the HTTP response status code is 404
152+
153+
Scenario: Get a 403 response without execute permission
154+
Given I login as "bob"
155+
When I POST "/policy_factories/cucumber/parameterized"
156+
Then the HTTP response status code is 403
157+
158+
Scenario: A policy factory without a base loads into the root policy
159+
Given I POST "/policy_factories/cucumber/root-factory"
160+
And the HTTP response status code is 201
161+
Then I successfully GET "/resources/cucumber/variable/created-in-root"
162+
163+
Scenario: I retrieve the policy factory template through the API
164+
Given I login as "alice"
165+
When I GET "/policy_factories/cucumber/edit-template/template"
166+
Then the HTTP response status code is 200
167+
And the JSON response should be:
168+
"""
169+
{
170+
"body": "---\n- !variable\n id: to-be-edited\n"
171+
}
172+
"""
173+
174+
Scenario: I update the policy factory template through the API
175+
Given I login as "alice"
176+
When I PUT "/policy_factories/cucumber/edit-template/template" with body:
177+
"""
178+
---\n- !variable replaced
179+
"""
180+
Then the HTTP response status code is 202
181+
When I GET "/policy_factories/cucumber/edit-template/template"
182+
Then the JSON response should be:
183+
"""
184+
{
185+
"body": "---\\n- !variable replaced"
186+
}
187+
"""
188+
189+
Scenario: I don't have permission to retrieve the policy factory template
190+
Given I login as "bob"
191+
When I GET "/policy_factories/cucumber/edit-template/template"
192+
Then the HTTP response status code is 404
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
--demo
2+
Content-Disposition: form-data; name="value"
3+
4+
test value
5+
--demo--

0 commit comments

Comments
 (0)