Skip to content

Commit b196a45

Browse files
committed
Add policy factory route and controller
1 parent c6971f2 commit b196a45

File tree

4 files changed

+367
-0
lines changed

4 files changed

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