1module MCollective 2 module Util 3 class ActionPolicy 4 attr_accessor :config, :allow_unconfigured, :configdir, :agent, :caller_id 5 attr_accessor :action, :groups, :enable_default, :default_name 6 7 def self.authorize(request) 8 ActionPolicy.new(request).authorize_request 9 end 10 11 def initialize(request) 12 @config = Config.instance 13 @agent = request.agent 14 @caller_id = request.caller 15 @action = request.action 16 @allow_unconfigured = !!(config.pluginconf.fetch('actionpolicy.allow_unconfigured', 'n') =~ /^1|y/i) 17 @enable_default = !!(config.pluginconf.fetch('actionpolicy.enable_default', 'n') =~ /^1|y/i) 18 @default_name = config.pluginconf.fetch('actionpolicy.default_name', 'default') 19 @configdir = @config.configdir 20 @groups = {} 21 end 22 23 # Performs request authorization 24 # 25 # @return [Boolean] 26 def authorize_request 27 # Lookup the policy file. If none exists and @allow_unconfigured 28 # is false the request gets denied. 29 policy_file = lookup_policy_file 30 31 # No policy file exists and allow_unconfigured is false 32 if !policy_file && !allow_unconfigured 33 deny('Could not load any valid policy files. Denying based on allow_unconfigured: %s' % allow_unconfigured) 34 # No policy exists but allow_unconfigured is true 35 elsif !(policy_file) && allow_unconfigured 36 Log.debug('Could not load any valid policy files. Allowing based on allow_unconfigured: %s' % allow_unconfigured) 37 return true 38 end 39 40 parse_group_file(lookup_groups_file) 41 42 # A policy file exists 43 parse_policy_file(policy_file) 44 end 45 46 # Parse and validate the policy file 47 # 48 # @param policy_file [String] path to the policy file 49 # @return [Boolean] 50 # @raize [RPCAborted] when the request does not pass the policy 51 def parse_policy_file(policy_file) 52 Log.debug('Parsing policyfile for %s: %s' % [agent, policy_file]) 53 allow = allow_unconfigured 54 55 File.read(policy_file).each_line do |line| 56 next if line =~ /^(#.*|\s*)$/ 57 58 if line =~ /^policy\s+default\s+(\w+)/ 59 if $1 == 'allow' 60 allow = true 61 else 62 allow = false 63 end 64 elsif line =~ /^(allow|deny)\t+(.+?)\t+(.+?)\t+(.+?)(\t+(.+?))*$/ 65 if check_policy($2, $3, $4, $6) 66 if $1 == 'allow' 67 return true 68 else 69 deny("Denying based on explicit 'deny' policy rule in policyfile: %s" % File.basename(policy_file)) 70 end 71 end 72 else 73 Log.warn("Cannot parse policy %s line: %s" % [policy_file, line]) 74 end 75 end 76 77 allow || deny("Denying based on default policy in %s" % File.basename(policy_file)) 78 end 79 80 # Parses the group file into the `@groups` memory structure 81 # 82 # @param group_file [String] path to the groups file 83 # @return [Hash] parsed groups 84 def parse_group_file(group_file) 85 return unless group_file 86 return unless File.exist?(group_file) 87 88 unless File.readable?(group_file) 89 Log.warn("The group file %s exist but it is not readable" % group_file) 90 return 91 end 92 93 Log.debug("Parsing groups file %s" % group_file) 94 95 File.read(group_file).each_line do |line| 96 next if line =~ /^(#.*|\s*)$/ 97 98 parts = line.chomp.split 99 100 if parts[0] =~ /^([\w\.\-]+)$/ 101 next if parts[1..-1].empty? 102 103 groups[ parts[0] ] = parts[1..-1] 104 else 105 Log.warn("Group file line '%s' is not in the expected format of 'group_name caller_id caller_id caller_id'" % line.chomp) 106 end 107 end 108 109 groups 110 end 111 112 # Determines if any of the groups have the caller in them 113 # 114 # @param groups [String,nil] space seperated list of groups 115 # @return [Boolean] 116 def caller_in_groups?(group_names) 117 return false unless group_names 118 119 group_names.to_s.split.select do |group| 120 next unless group =~ /^([\w\.\-]+)$/ 121 122 groups.fetch(group, []).include?(caller_id) 123 end.any? 124 end 125 126 # Determine if the caller is any of the callerids 127 # 128 # @param caller_ids [String] space seperated list of caller ids 129 # @return [Boolean] 130 def caller_in_callerids?(caller_ids) 131 return false unless caller_ids 132 133 caller_ids.to_s.include?(caller_id) 134 end 135 136 def action_in_actions?(actions) 137 actions.split.include?(action) 138 end 139 140 # Check if a request made by a caller matches the state defined in the policy 141 # 142 # @param rpccaller [String] the rpccaller as per the policy line 143 # @param action [String] the actions as per the policy line 144 # @param facts [String] the facts as per the policy line 145 # @param classes [String] the facts as per the policy line 146 # @return [Boolean] 147 def check_policy(rpccaller, actions, facts, classes) 148 # If we have a wildcard caller or the caller matches our policy line 149 # then continue else skip this policy line 150 return false unless rpccaller == '*' || caller_in_callerids?(rpccaller) || caller_in_groups?(rpccaller) 151 152 # If we have a wildcard actions list or the request action is in the list 153 # of actions in the policy line continue, else skip this policy line 154 return false unless actions == '*' || action_in_actions?(actions) 155 156 return parse_facts(facts) && parse_classes(classes) if classes 157 158 parse_compound(facts) 159 end 160 161 # Parses and validates the facts from a policy line 162 # 163 # @param facts [String] facts as per the policy line 164 # @return [Boolean] 165 def parse_facts(facts) 166 return true if facts == '*' 167 168 if is_compound?(facts) 169 return parse_compound(facts) 170 else 171 facts.split.each do |fact| 172 return false unless lookup_fact(fact) 173 end 174 end 175 176 true 177 end 178 179 # Parses and validates the classes from the policy line 180 # 181 # @param classes [String] classes as per the policy line 182 # @return [Boolean] 183 def parse_classes(classes) 184 return true if classes == '*' 185 186 if is_compound?(classes) 187 return parse_compound(classes) 188 else 189 classes.split.each do |klass| 190 return false unless lookup_class(klass) 191 end 192 end 193 194 true 195 end 196 197 # Parses and validates a fact from the policy line 198 # 199 # @param fact [String] a standard fact filter format fact 200 # @return [Boolean] 201 def lookup_fact(fact) 202 if fact =~ /(.+)(<|>|=|<=|>=)(.+)/ 203 lv = $1 204 sym = $2 205 rv = $3 206 207 sym = '==' if sym == '=' 208 return eval("'#{Util.get_fact(lv)}'#{sym}'#{rv}'") 209 else 210 Log.warn("Class found where fact was expected") 211 return false 212 end 213 end 214 215 # Parses a class expression and validates it 216 # 217 # @param klass [String] class name to lookup and validate 218 # @return [Boolean] 219 def lookup_class(klass) 220 if klass =~ /(.+)(<|>|=|<=|>=)(.+)/ 221 Log.warn("Fact found where class was expected") 222 return false 223 else 224 return Util.has_cf_class?(klass) 225 end 226 end 227 228 # Looks up and validates either a class or a fact 229 # 230 # @param token [String] either a fact in fact filter format or a class 231 # @return [Boolean] 232 def lookup(token) 233 if token =~ /(.+)(<|>|=|<=|>=)(.+)/ 234 return lookup_fact(token) 235 else 236 return lookup_class(token) 237 end 238 end 239 240 # Determines full path to the policy file 241 # 242 # Here we lookup the full path of the policy file. If the policyfile 243 # does not exist, we check to see if a default file was set and 244 # determine its full path. If no default file exists, or default was 245 # not specified, we return false. 246 # 247 # @return [String,Boolean] full file path else false 248 def lookup_policy_file 249 policy_file = File.join(@configdir, "policies", "#{agent}.policy") 250 251 Log.debug("Looking for policy in #{policy_file}") 252 253 return policy_file if File.exist?(policy_file) 254 255 if enable_default 256 default_file = File.join(configdir, "policies", "#{default_name}.policy") 257 258 Log.debug("Initial lookup failed: looking for policy in #{default_file}") 259 260 return default_file if File.exist?(default_file) 261 end 262 263 Log.debug('Could not find any policy files.') 264 265 false 266 end 267 268 269 # Determines full path to the groups file 270 # 271 # @return [String] path to the file 272 def lookup_groups_file 273 File.join(configdir, "policies", "groups") 274 end 275 276 # Evalute a compound statement and return its truth value 277 # 278 # @param statement [String] a standard compound filter string 279 # @return [Boolean] 280 def eval_statement(statement) 281 token_type = statement.keys.first 282 token_value = statement.values.first 283 284 return token_value if (token_type != 'statement' && token_type != 'fstatement') 285 286 if token_type == 'statement' 287 return lookup(token_value) 288 elsif token_type == 'fstatement' 289 begin 290 return Matcher.eval_compound_fstatement(token_value) 291 rescue => e 292 Log.warn("Could not call Data function in policy file: #{e}") 293 return false 294 end 295 end 296 end 297 298 # Determines if a string is a compound filter 299 # 300 # @param list [String] a standard compound filter string 301 # @return [Boolean] 302 def is_compound?(list) 303 list.split.each do |token| 304 if token =~ /^!|^not$|^or$|^and$|\(.+\)/ 305 return true 306 end 307 end 308 309 false 310 end 311 312 # Parse and evaluate a compound filter string 313 # 314 # @param list [String] compound filter 315 # @return [Boolean] 316 def parse_compound(list) 317 stack = Matcher.create_compound_callstack(list) 318 319 begin 320 stack.map!{ |item| eval_statement(item) } 321 rescue => e 322 Log.debug(e.to_s) 323 return false 324 end 325 326 eval(stack.join(' ')) 327 end 328 329 # Log and raise an appropriate error on deny 330 # 331 # @param logline [String] line to log in the log file 332 # @raise [RPCAborted] standard non specific failure error 333 def deny(logline) 334 Log.debug(logline) 335 336 raise(RPCAborted, 'You are not authorized to call this agent or action.') 337 end 338 end 339 end 340end 341