diff --git a/lib/ansible/modules/system/pamd.py b/lib/ansible/modules/system/pamd.py new file mode 100644 index 00000000000..252bca6a30e --- /dev/null +++ b/lib/ansible/modules/system/pamd.py @@ -0,0 +1,549 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Kenneth D. Evensen +# +# This file is part of Ansible (sort of) +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception + +DOCUMENTATION = """ +module: pamd +author: + - "Kenneth D. Evensen (@kevensen)" +short_description: Manage PAM Modules +description: + - Edit PAM service's type, control, module path and module arguments. + In order for a PAM rule to be modified, the type, control and + module_path must match an existing rule. See man(5) pam.d for details. +version_added: "2.3" +options: + name: + required: true + description: + - The name generally refers to the PAM service file to + change, for example system-auth. + type: + required: true + description: + - The type of the PAM rule being modified. The type, control + and module_path all must match a rule to be modified. + control: + required: true + description: + - The control of the PAM rule being modified. This may be a + complicated control with brackets. If this is the case, be + sure to put "[bracketed controls]" in quotes. The type, + control and module_path all must match a rule to be modified. + module_path: + required: true + description: + - The module path of the PAM rule being modified. The type, + control and module_path all must match a rule to be modified. + new_type: + required: false + description: + - The type to assign to the new rule. + new_control: + required: false + description: + - The control to assign to the new rule. + new_module_path: + required: false + description: + - The control to assign to the new rule. + module_arguments: + required: false + description: + - When state is 'updated', the module_arguments will replace existing + module_arguments. When state is 'args_absent' args matching those + listed in module_arguments will be removed. When state is + 'args_present' any args listed in module_arguments are added if + missing from the existing rule. Furthermore, if the module argument + takes a value denoted by '=', the value will be changed to that specified + in module_arguments. + insert: + required: false + default: none + choices: + - updated + - before + - after + - args_present + - args_absent + description: + - The default of 'updated' will modify an existing rule if type, + control and module_path all match an existing rule. With 'before', + the new rule will be inserted before a rule matching type, control + and module_path. Similarly, with 'after', the new rule will be inserted + after an existing rule matching type, control and module_path. With + either 'before' or 'after' new_type, new_control, and new_module_path + must all be specified. If state is 'args_absent' or 'args_present', + new_type, new_control, and new_module_path will be ignored. + path: + required: false + default: /etc/pam.d/ + description: + - This is the path to the PAM service files +""" + +EXAMPLES = """ +- name: Update pamd rule's control in /etc/pam.d/system-auth + pamd: + name: system-auth + type: auth + control: required + module_path: pam_faillock.so + new_control: sufficient + +- name: Update pamd rule's complex control in /etc/pam.d/system-auth + pamd: + name: system-auth + type: session + control: '[success=1 default=ignore]' + module_path: pam_succeed_if.so + new_control: '[success=2 default=ignore]' + +- name: Insert a new rule before an existing rule + pamd: + name: system-auth + type: auth + control: required + module_path: pam_faillock.so + new_type: auth + new_control: sufficient + new_module_path: pam_faillock.so + state: before + +- name: Insert a new rule after an existing rule + pamd: + name: system-auth + type: auth + control: required + module_path: pam_faillock.so + new_type: auth new_control=sufficient + new_module_path: pam_faillock.so + state: after + +- name: Remove module arguments from an existing rule + pamd: + name: system-auth + type: auth + control: required + module_path: pam_faillock.so + module_arguments: '' + state: updated + +- name: Replace all module arguments in an existing rule + pamd: + name: system-auth + type: auth + control: required + module_path: pam_faillock.so + module_arguments: 'preauth + silent + deny=3 + unlock_time=604800 + fail_interval=900' + state: updated + +- name: Remove specific arguments from a rule + pamd: + name: system-auth + type: session control='[success=1 default=ignore]' + module_path: pam_succeed_if.so + module_arguments: 'crond quiet' + state: args_absent + +- name: Ensure specific arguments are present in a rule + pamd: + name: system-auth + type: session + control: '[success=1 default=ignore]' + module_path: pam_succeed_if.so + module_arguments: 'crond quiet' + state: args_present + +- name: Update specific argument value in a rule + pamd: + name: system-auth + type: auth + control: required + module_path: pam_faillock.so + module_arguments: 'fail_interval=300' + state: args_present +""" + +RETURN = ''' +dest: + description: path to pam.d service that was changed + returned: success + type: string + sample: "/etc/pam.d/system-auth" +... +''' + + +# The PamdRule class encapsulates a rule in a pam.d service +class PamdRule(object): + + def __init__(self, rule_type, + rule_control, rule_module_path, + rule_module_args=None): + + self.rule_type = rule_type + self.rule_control = rule_control + self.rule_module_path = rule_module_path + + try: + if (rule_module_args is not None and + type(rule_module_args) is list): + self.rule_module_args = rule_module_args + elif (rule_module_args is not None and + type(rule_module_args) is str): + self.rule_module_args = rule_module_args.split() + except AttributeError: + self.rule_module_args = [] + + @classmethod + def rulefromstring(cls, stringline): + split_line = stringline.split() + + rule_type = split_line[0] + rule_control = split_line[1] + + if rule_control.startswith('['): + rule_control = stringline[stringline.index('['): + stringline.index(']')+1] + + if "]" in split_line[2]: + rule_module_path = split_line[3] + rule_module_args = split_line[4:] + else: + rule_module_path = split_line[2] + rule_module_args = split_line[3:] + + return cls(rule_type, rule_control, rule_module_path, rule_module_args) + + def get_module_args_as_string(self): + try: + if self.rule_module_args is not None: + return ' '.join(self.rule_module_args) + except AttributeError: + pass + return '' + + def __str__(self): + return "%-10s\t%s\t%s %s" % (self.rule_type, + self.rule_control, + self.rule_module_path, + self.get_module_args_as_string()) + + +# PamdService encapsulates an entire service and contains one or more rules +class PamdService(object): + + def __init__(self, path, name, ansible): + self.path = path + self.name = name + self.check = ansible.check_mode + self.ansible = ansible + self.fname = self.path + "/" + self.name + self.preamble = [] + self.rules = [] + + try: + for line in open(self.fname, 'r'): + if line.startswith('#') and not line.isspace(): + self.preamble.append(line.rstrip()) + elif not line.startswith('#') and not line.isspace(): + self.rules.append(PamdRule.rulefromstring + (stringline=line.rstrip())) + except Exception: + e = get_exception() + self.ansible.fail_json(msg='Unable to open/read PAM module file ' + + '%s with error %s' % (self.fname, str(e))) + + def __str__(self): + return self.fname + + +def update_rule(service, old_rule, new_rule): + + changed = False + change_count = 0 + result = {'action': 'update_rule'} + + for rule in service.rules: + if (old_rule.rule_type == rule.rule_type and + old_rule.rule_control == rule.rule_control and + old_rule.rule_module_path == rule.rule_module_path): + + if (new_rule.rule_type is not None and + new_rule.rule_type != rule.rule_type): + rule.rule_type = new_rule.rule_type + changed = True + if (new_rule.rule_control is not None and + new_rule.rule_control != rule.rule_control): + rule.rule_control = new_rule.rule_control + changed = True + if (new_rule.rule_module_path is not None and + new_rule.rule_module_path != rule.rule_module_path): + rule.rule_module_path = new_rule.rule_module_path + changed = True + try: + if (new_rule.rule_module_args is not None and + new_rule.rule_module_args != + rule.rule_module_args): + rule.rule_module_args = new_rule.rule_module_args + changed = True + except AttributeError: + pass + if changed: + result['updated_rule_'+str(change_count)] = str(rule) + result['new_rule'] = str(new_rule) + + change_count += 1 + + result['change_count'] = change_count + return changed, result + + +def insert_before_rule(service, old_rule, new_rule): + index = 0 + change_count = 0 + result = {'action': + 'insert_before_rule'} + changed = False + for rule in service.rules: + if (old_rule.rule_type == rule.rule_type and + old_rule.rule_control == rule.rule_control and + old_rule.rule_module_path == rule.rule_module_path): + if index == 0: + service.rules.insert(0, new_rule) + changed = True + elif (new_rule.rule_type != service.rules[index-1].rule_type or + new_rule.rule_control != + service.rules[index-1].rule_control or + new_rule.rule_module_path != + service.rules[index-1].rule_module_path): + service.rules.insert(index, new_rule) + changed = True + if changed: + result['new_rule'] = str(new_rule) + result['before_rule_'+str(change_count)] = str(rule) + change_count += 1 + index += 1 + result['change_count'] = change_count + return changed, result + + +def insert_after_rule(service, old_rule, new_rule): + index = 0 + change_count = 0 + result = {'action': 'insert_after_rule'} + changed = False + for rule in service.rules: + if (old_rule.rule_type == rule.rule_type and + old_rule.rule_control == rule.rule_control and + old_rule.rule_module_path == rule.rule_module_path): + if (new_rule.rule_type != service.rules[index+1].rule_type or + new_rule.rule_control != + service.rules[index+1].rule_control or + new_rule.rule_module_path != + service.rules[index+1].rule_module_path): + service.rules.insert(index+1, new_rule) + changed = True + if changed: + result['new_rule'] = str(new_rule) + result['after_rule_'+str(change_count)] = str(rule) + change_count += 1 + index += 1 + + result['change_count'] = change_count + return changed, result + + +def remove_module_arguments(service, old_rule, module_args): + result = {'action': 'args_absent'} + changed = False + change_count = 0 + for rule in service.rules: + if (old_rule.rule_type == rule.rule_type and + old_rule.rule_control == rule.rule_control and + old_rule.rule_module_path == rule.rule_module_path): + for arg_to_remove in module_args.split(): + for arg in rule.rule_module_args: + if arg == arg_to_remove: + rule.rule_module_args.remove(arg) + changed = True + result['removed_arg_'+str(change_count)] = arg + result['from_rule_'+str(change_count)] = str(rule) + change_count += 1 + + result['change_count'] = change_count + return changed, result + + +def add_module_arguments(service, old_rule, module_args): + result = {'action': 'args_present'} + changed = False + change_count = 0 + for rule in service.rules: + if (old_rule.rule_type == rule.rule_type and + old_rule.rule_control == rule.rule_control and + old_rule.rule_module_path == rule.rule_module_path): + for arg_to_add in module_args.split(' '): + if "=" in arg_to_add: + pre_string = arg_to_add[:arg_to_add.index('=')+1] + indicies = [i for i, arg + in enumerate(rule.rule_module_args) + if arg.startswith(pre_string)] + for i in indicies: + if rule.rule_module_args[i] != arg_to_add: + rule.rule_module_args[i] = arg_to_add + changed = True + result['updated_arg_' + + str(change_count)] = arg_to_add + result['in_rule_' + + str(change_count)] = str(rule) + change_count += 1 + elif arg_to_add not in rule.rule_module_args: + rule.rule_module_args.append(arg_to_add) + changed = True + result['added_arg_'+str(change_count)] = arg_to_add + result['to_rule_'+str(change_count)] = str(rule) + change_count += 1 + result['change_count'] = change_count + return changed, result + + +def write_rules(service): + previous_rule = None + + f = open(service.fname, 'w') + for amble in service.preamble: + f.write(amble+'\n') + + for rule in service.rules: + if (previous_rule is not None and + previous_rule.rule_type != rule.rule_type): + f.write('\n') + f.write(str(rule)+'\n') + previous_rule = rule + f.close() + + +def main(): + + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + type=dict(required=True, + choices=['account', 'auth', + 'password', 'session']), + control=dict(required=True), + module_path=dict(required=True), + new_type=dict(required=False, + choices=['account', 'auth', + 'password', 'session']), + new_control=dict(required=False), + new_module_path=dict(required=False), + module_arguments=dict(required=False), + state=dict(required=False, default="updated", + choices=['before', 'after', 'updated', + 'args_absent', 'args_present']), + path=dict(required=False, default='/etc/pam.d') + ), + supports_check_mode=True + ) + + service = module.params['name'] + old_type = module.params['type'] + old_control = module.params['control'] + old_module_path = module.params['module_path'] + + new_type = module.params['new_type'] + new_control = module.params['new_control'] + new_module_path = module.params['new_module_path'] + + module_arguments = module.params['module_arguments'] + state = module.params['state'] + + path = module.params['path'] + + pamd = PamdService(path, service, module) + + old_rule = PamdRule(old_type, + old_control, + old_module_path) + new_rule = PamdRule(new_type, + new_control, + new_module_path, + module_arguments) + + try: + if state == 'updated': + change, result = update_rule(pamd, + old_rule, + new_rule) + elif state == 'before': + if (new_rule.rule_control is None or + new_rule.rule_type is None or + new_rule.rule_module_path is None): + + module.fail_json(msg='When inserting a new rule before ' + + 'or after an existing rule, new_type, ' + + 'new_control and new_module_path must ' + + 'all be set.') + change, result = insert_before_rule(pamd, + old_rule, + new_rule) + elif state == 'after': + if (new_rule.rule_control is None or + new_rule.rule_type is None or + new_rule.rule_module_path is None): + + module.fail_json(msg='When inserting a new rule before' + + 'or after an existing rule, new_type,' + + ' new_control and new_module_path must' + + ' all be set.') + change, result = insert_after_rule(pamd, + old_rule, + new_rule) + elif state == 'args_absent': + change, result = remove_module_arguments(pamd, + old_rule, + module_arguments) + elif state == 'args_present': + change, result = add_module_arguments(pamd, + old_rule, + module_arguments) + + if not module.check_mode: + write_rules(pamd) + + except Exception: + e = get_exception() + module.fail_json(msg='error running changing pamd: %s' % str(e)) + facts = {} + facts['pamd'] = {'changed': change, 'result': result} + + module.params['dest'] = pamd.fname + + module.exit_json(changed=change, ansible_facts=facts) + +if __name__ == '__main__': + main()