/
acl.rb
239 lines (224 loc) · 8.98 KB
/
acl.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# frozen_string_literal: true
module PuppetX
# Module to capture all untility extensions for the Rundeck puppet module
module Rundeck
# Module to check ACL validity
module ACL
# RundeckValidator class
class RundeckValidator
def raise_err(msg)
raise(Puppet::ParseError, "The policy is invalid - #{msg}")
end
def validate_description(description)
raise_err('description is not a String') unless description.is_a? String
end
def validate_context(context)
raise_err('context is not a Hash') unless context.is_a? Hash
raise_err('context is empty') if context.empty?
raise_err('context can only contain project or application') unless context.keys.length == 1
type = context.keys[0]
raise_err("context:#{type} is not a String") unless context[type].is_a? String
raise_err('context can only be project or application') unless %w[application project].include? type
end
def validate_rule_action(type, type_section, scope)
action_found = false
actions = []
property = ''
value = ''
raise_err("for:#{type} is empty") if type_section.empty?
type_section.each do |e|
raise_err("for:#{type} entry is not a Hash") unless e.is_a? Hash
end
type_section.each do |e| # rubocop:disable Style/CombinableLoops
e.each do |k, v|
if k.eql?('allow') || k.eql?('deny')
action_found = true
actions = v
elsif %w[match equals contains subset].include?(k)
case type
when 'resource'
property = v['kind']
when 'job'
if v['name']
property = 'name'
value = v['name']
elsif v['group']
property = 'group'
value = v['group']
else
property = v.keys[0]
end
when 'project'
if v['name']
property = 'name'
value = v['name']
else
property = v.keys[0]
end
when 'storage'
if v['name']
property = 'name'
value = v['name']
elsif v['path']
property = 'path'
value = v['path']
else
property = v.keys[0]
end
end
end
end
raise_err("for:#{type} does not contain a rule action of [allow,deny]") unless action_found
case scope
when 'project'
validate_proj_actions(type, actions, property, value) if property.to_s != '' || type.eql?('adhoc') || type.eql?('node')
when 'application'
validate_app_actions(type, actions, property, value)
end
end
end
def validate_proj_actions(type, actions, property, value = '')
project_actions = {
'resource' => {
'job' => %w[create delete],
'node' => %w[read create update refresh],
'event' => %w[read create]
},
'adhoc' => %w[read run runAs kill killAs],
'job' => {
'name' => %w[read update delete run runAs kill killAs create],
'group' => %w[read update delete run runAs kill killAs create]
},
'node' => %w[read run]
}
case type
when 'resource'
case property
when 'job', 'node', 'event'
actions.each do |action|
raise_err("for:resource kind:#{property} can only contain actions #{project_actions[type][property]}") unless project_actions[type][property].include?(action)
end
end
when 'adhoc', 'node'
actions.each do |action|
raise_err("for:#{type} can only contain actions #{project_actions[type]}") unless project_actions[type].include?(action)
end
when 'job'
case property
when 'name', 'group'
actions.each do |action|
raise_err("for:job #{property}:#{value} can only contain actions #{project_actions[type][property]}") unless project_actions[type][property].include?(action)
end
else
raise_err("#{property} is not a valid property for the job scope")
end
end
end
def validate_app_actions(type, actions, property, _value = '')
app_actions = {
'resource' => {
'project' => ['create'],
'system' => ['read'],
'user' => ['admin'],
'job' => ['admin']
},
'project' => { 'name' => %w[read configure delete import export delete_execution admin] },
'storage' => {
'name' => %w[create update read delete],
'path' => %w[create update read delete]
}
}
case type
when 'resource'
case property
when 'project', 'system', 'user', 'job'
actions.each do |action|
raise_err("for:resource kind:#{property} can only contain actions #{app_actions[type][property]}") unless app_actions[type][property].include? action
end
end
when 'project'
if property.eql?('name')
actions.each do |action|
raise_err("for:project #{property} can only contain actions #{app_actions[type][property]}") unless app_actions[type][property].include? action
end
end
when 'storage'
case property
when 'name', 'path'
actions.each do |action|
raise_err("for:storage #{property} can only contain actions #{app_actions[type][property]}") unless app_actions[type][property].include? action
end
end
end
end
def validate_matching(type, type_section)
matching_found = false
raise_err("for:#{type} is empty") if type_section.empty?
type_section.each do |e|
if e.is_a? Hash
e.each do |k, _v|
matching_found = true if k.eql?('match') || k.eql?('equals') || k.eql?('contains')
end
else
raise_err("for:#{type} entry is not a Hash")
end
end
raise_err("for:#{type} does not contain a matching statement of [match,equals,contains]") unless matching_found
end
def validate_for(for_section, context)
if !for_section.is_a? Hash
raise_err('for is not a Hash')
elsif for_section.empty?
raise_err('for is empty')
else
scope = context.keys[0]
case scope
when 'project'
resource_types = %w[job node adhoc project resource]
when 'application'
resource_types = %w[resource project storage]
else
raise_err("unknown scope: #{scope}")
end
for_section.each do |k, _v|
raise_err("for section must only contain #{resource_types.inspect.tr!('"', "'")}") unless resource_types.include?(k)
end
resource_types.each do |type|
next unless for_section.key?(type)
if !for_section[type].is_a? Array
raise_err("for:#{type} is not an Array")
elsif for_section[type].empty?
raise_err("for:#{type} is empty")
else
validate_rule_action(type, for_section[type], scope)
validate_matching(type, for_section[type]) unless for_section[type].eql?('adhoc')
end
end
end
end
def validate_by(by_section)
raise_err('by is not an Array') unless by_section.is_a? Array
raise_err('by is empty') if by_section.empty?
by_section.each do |item|
raise_err("by:#{item} is not a Hash") unless item.is_a? Hash
raise_err('by is empty') if item.empty?
item.each do |k, _v|
raise_err('by section must only contain [username,group]') unless %w[username group].include?(k)
end
%w[username group].each do |type|
raise_err("by:#{type} is not a String or an Array") if item.key?(type) && !item[type].is_a?(String) && !item[type].is_a?(Array)
end
end
end
end
def validate_acl(hash)
rv = RundeckValidator.new
rv.validate_description(hash['description'])
rv.validate_context(hash['context'])
rv.validate_for(hash['for'], hash['context'])
rv.validate_by(hash['by'])
end
module_function :validate_acl
end
end
end