Source code for insights.parsers.pam

"""
Pluggable Authentication Module configuration
=============================================

This module provides parsing for PAM configuration files.
``PamConf`` is a parser for ``/etc/pam.conf`` files. Sample input is provided
in the examples.

PamConf - file ``/etc/pam.conf``
--------------------------------

Sample file data::

    #%PAM-1.0
    vsftpd      auth        required    pam_securetty.so
    vsftpd      auth        requisite   pam_unix.so nullok
    vsftpd      auth        sufficient  pam_nologin.so
    vsftpd      account     optional    pam_unix.so
    other       password    include     pam_cracklib.so retry=3 logging=verbose
    other       password    required    pam_unix.so shadow nullok use_authtok
    other       session     required    pam_unix.so


Examples:
    >>> type(pam_conf)
    <class 'insights.parsers.pam.PamConf'>
    >>> len(pam_conf)
    7
    >>> pam_conf[0].service
    'vsftpd'
    >>> pam_conf[0].interface
    'auth'
    >>> pam_conf[0].control_flags
    [ControlFlag(flag='required', value=None)]
    >>> pam_conf[0].module_name
    'pam_securetty.so'
    >>> pam_conf[0].module_args is None
    True
    >>> pam_conf.file_path
    '/etc/pam.conf'

PamDConf - used for specific PAM configuration files
----------------------------------------------------

``PamDConf`` is a base class for the creation of parsers for ``/etc/pam.d``
service specific configuration files.

Sample file from ``/etc/pam.d/sshd``::

    #%PAM-1.0
    auth       required     pam_sepermit.so
    auth       substack     password-auth
    auth       include      postlogin
    # Used with polkit to reauthorize users in remote sessions
    -auth      optional     pam_reauthorize.so prepare
    account    required     pam_nologin.so
    account    include      password-auth
    password   include      password-auth
    # pam_selinux.so close should be the first session rule
    session    required     pam_selinux.so close
    session    required     pam_loginuid.so
    # pam_selinux.so open should only be followed by sessions to be executed in the user context
    session    required     pam_selinux.so open env_params
    session    required     pam_namespace.so
    session    optional     pam_keyinit.so force revoke
    session    include      password-auth
    session    include      postlogin
    # Used with polkit to reauthorize users in remote sessions
    -session   optional     pam_reauthorize.so prepare

Examples:

    >>> type(pamd_conf)
    <class 'insights.parsers.pam.PamDConf'>
    >>> len(pamd_conf)
    15
    >>> pamd_conf[0]._errors == [] # No errors in parsing
    True
    >>> pamd_conf[0].service
    'sshd'
    >>> pamd_conf[0].interface
    'auth'
    >>> pamd_conf[0].control_flags
    [ControlFlag(flag='required', value=None)]
    >>> pamd_conf[0].module_name
    'pam_sepermit.so'
    >>> pamd_conf[0].module_args is None
    True
    >>> pamd_conf.file_path
    '/etc/pam.d/sshd'
    >>> pamd_conf[3].module_name
    'pam_reauthorize.so'
    >>> pamd_conf[3].ignored_if_module_not_found
    True

Normal use of the ``PamDConf`` class is to subclass it for a parser.  In
``insights/specs/default.py``::

    pam_sshd = simple_file("etc/pam.d/sshd")

In the parser module (e.g. ``insights/parsers/pam_sshd.py``)::

    from insights import parser
    from insights.parsers.pam import PamDConf
    from insights.specs import Specs

    @parser(Specs.pam_sshd)
    class PamSSHD(PamDConf):
        pass

References:
    http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_SAG.html
"""

from collections import namedtuple
from .. import Parser, get_active_lines, parser
from ..parsers import unsplit_lines, keyword_search, optlist_to_dict

import re
from insights.specs import Specs


[docs] class PamConfEntry(object): """Contains information from one PAM configuration line. Parses a single line of either a ``/etc/pam.conf`` file or a service specific ``/etc/pam.d`` conf file. The difference is that for ``/etc/pam.conf``, the service name is the first column of the input line. If a service specific conf file then the service name is not present in the line and must be provided as the ``service`` parameter as well as setting the ``pamd_conf`` to True. Parameters: line (str): One line of the pam conf info. pamd_config (boolean): If this is set to False then ``line`` will be parsed as a line from the ``etc/pam.conf`` file, if ``True`` then the line will be parsed as a line from a service specific ``etc/pam.d/`` conf file. Default is ``True``. service (str): If ``pamd_conf`` is ``True`` then the name of the service file must be provided since it is not present in `line`. Attributes: service (str): The service name (taken from the line or from the file name if not parsing ``pam.conf``) interface (str): The *type* clause - should be one of ``'account'``, ``'auth'``, ``'password'`` or ``'session'. If the line was invalid this is set to ``None``. ignored_if_module_not_found (bool): If the *type* clause is preceded by ``'-'``, then this is set to True and it indicates that PAM would skip this line rather than reporting an error if the given module is not found. control_flags (list): A list of ControlFlag named tuples. If the control flag was one of ``'required'``, ``'requisite'``, ``'sufficient'``, ``'optional'``, ``'include'``, or ``'substack'``, then this is the only flag in the list and its value is set to ``True``. If the control flag started with ``[``, then the list inside the square brackets is interpreted as a list of key=value tuples. _control_raw (str): the raw control flag string before parsing, for reference. module_name (str): the PAM module name (including the '.so') module_args (str): the PAM module arguments, if any. This is not parsed. _full_line (str): The original line in the PAM configuration. _errors (list): A list of parsing errors detected in this line. Examples: >>> pam_conf_line = 'vsftpd auth requisite pam_unix.so nullok' >>> entry = PamConfEntry(pam_conf_line) >>> entry.service 'vsftpd' >>> entry.control_flags[0].flag 'requisite' >>> entry.module_args 'nullok' >>> pamd_conf_line = ''' ... auth [success=2 default=ok] pam_debug.so auth=perm_denied cred=success ... '''.strip() >>> entry = PamConfEntry(pamd_conf_line, pamd_conf=True, service='vsftpd') >>> entry.service 'vsftpd' >>> entry.control_flags [ControlFlag(flag='success', value='2'), ControlFlag(flag='default', value='ok')] >>> entry.module_args 'auth=perm_denied cred=success' Raises: ValueError: If `pamd_conf` is True and `service` name is not provided, or if the line doesn't contain any module information. .. describe:: ControlFlag A named tuple with the 'flag' and 'value' properties, used to store information about the control flags in a PAM configuration line. """ ControlFlag = namedtuple('ControlFlag', 'flag, value') type_re = r'(?P<type>-?(?:account|auth|password|session))' control_re = r'(?P<control>required|requisite|sufficient|optional|' +\ r'include|substack|\[\w+=\w+(?:\s+\w+=\w+)*\])' mod_path_re = r'(?P<module>[\w.-]+)' mod_args_re = r'(?P<mod_args>\S.*)' line_re = r'\s+'.join([type_re, control_re, mod_path_re]) + \ r'(?:\s+' + mod_args_re + r')?' line_rex = re.compile(line_re) def __init__(self, line, pamd_conf=False, service=None): # If not pam.d file then service is first column in # line for pam.conf file. Otherwise service must be parameter. # print "PamConfEntry(line='{l}', pamd_conf={p}, service={s})".format(l=line, p=pamd_conf, s=service) self._full_line = line self._errors = [] self.service = service self.interface = None self.ignored_if_module_not_found = None self._control_raw = '' self.control_flags = [] self.module_name = None self.module_args = None if not pamd_conf: # Because this is called using get_active_lines() and # line_unsplit(), we cannot get a line that does not at least have # one token on it. self.service, line = line.split(None, 1) elif service is None: # The only valid situation to raise an error - the implementation # has called us with pamd_conf = true so we expect a service to # be given as a parameter. raise ValueError('Service name must be provided for pam.d conf file') match = self.line_rex.search(line) if match: # Type can have a '-' in front, if so line is ignored if module # cannot be found. self._type_raw = match.group('type') self.ignored_if_module_not_found = self._type_raw[0] == '-' self.interface = self._type_raw[1:] if self.ignored_if_module_not_found else self._type_raw # Parse control token, either regular word or [key=val...] self._control_raw = match.group('control') if self._control_raw.startswith('['): self.control_flags = [] # Regex assures that it ends with ] for group in self._control_raw[1:-1].split(None): # We could do a match: val is constrained to a list and # action is 'ignore', 'bad', 'die', 'ok', done', 'reset' # or N where N is an unsigned integer. But keep it simple # for now. # Regex makes sure that each group contains an '=' though. val, action = group.split('=', 1) self.control_flags.append(self.ControlFlag(val, action)) else: self.control_flags = [self.ControlFlag(self._control_raw, None)] self.module_name = match.group('module') self.module_args = match.group('mod_args') if 'mod_args' in match.groupdict() else None self.module_args_dict = ( optlist_to_dict(self.module_args, opt_sep=' ') if self.module_args is not None else {} ) else: # Line not valid - report error self._errors.append("Cannot parse line '{l}' as a valid pam.d entry".format(l=self._full_line)) def __repr__(self): return "<PamConfEntry for {svc}: {typ} {ctl} {name}{args}>".format( svc=self.service, typ=self._type_raw, ctl=self._control_raw, name=self.module_name, args=( ' ' + self.module_args if self.module_args else '' ) )
[docs] class PamDConf(Parser): """Base class for parsing files in ``/etc/pam.d`` Derive from this class for parsers of files in the ``/etc/pam.d`` directory. Parses each line of the conf file into a list of PamConfEntry. Configuration file format is:: module_interface control_flag module_name module_arguments Sample input:: >>> pam_sshd = ''' ... auth required pam_securetty.so ... auth requisite pam_unix.so nullok ... auth sufficient pam_nologin.so ... auth [success=2 default=ok] pam_debug.so auth=perm_denied cred=success ... account optional pam_unix.so ... password include pam_cracklib.so retry=3 logging=verbose ... password required pam_unix.so shadow nullok use_authtok ... ''' >>> from insights.tests import context_wrap >>> class YourPamDConf(PamDConf): # A trivial example ... pass >>> conf = YourPamDConf(context_wrap(pam_sshd, path='/etc/pam.d/sshd')) The `service` property of each PamConfEntry is set to the complete path name of the PAM config file. Attributes: data (list): List containing a PamConfEntry object for each line of the conf file in the same order as lines appear in the file. Examples: >>> conf[0].module_name # Can be used like a list of objects 'pam_securetty.so' >>> account_rows = list(conf.search(interface='account')) >>> len(account_rows) 1 >>> account_rows[0].interface 'account' >>> account_rows[0].module_name 'pam_unix.so' >>> account_rows[0].control_flags [ControlFlag(flag='optional', value=None)] """
[docs] def parse_content(self, content): self.data = [] # Need unsplit_lines for handling \ line continuations for line in get_active_lines(unsplit_lines(content)): self.data.append(PamConfEntry(line, pamd_conf=True, service=self.file_name))
def __iter__(self): """(iterable): Iterate through the list of rules in the file""" for entry in self.data: yield entry def __getitem__(self, ndx): """(PamConfEntry): Return the entry data for the given line number""" return self.data[ndx] def __len__(self): """(int): Return the number of entries read from the file""" return len(self.data)
[docs] def search(self, **kwargs): """ Search the pam.d configuration file by keyword. This is provided by the :func:`insights.parsers.keyword_search` function - see its documentation for more information. Searching on the list of PAM configuration entries is exactly like they were dictionaries instead of objects with properties. In addition, the 'control_flags' property becomes a dictionary of keywords and values, so that 'control_flags__contains' allows searching for a particular control flag. Returns: (list): A list of PamConfEntry objects that match the given search criteria. """ # Because keyword_search takes dicts, and we have objects with complex # properties, we convert them to a list of dicts. # First, store the things we're going to convert. prop_keys = ('service', 'interface', 'module_name', 'module_args') search = [] # Can't find a neat way to do this as a comprehension, so it's back # to loops. for obj in self.data: row = {'entry_obj': obj} for key in prop_keys: row[key] = getattr(obj, key) # For hysterical reasons, module_args can contain None if not # defined, but that's non-iterable: replace with '' if row['module_args'] is None: row['module_args'] = '' # Convert control_flags down to key/value dictionary flags = {} for cf in obj.control_flags: flags[cf.flag] = cf.value row['control_flags'] = flags search.append(row) # Now get the result, and just return the entry objects found = [] for r in keyword_search(search, **kwargs): found.append(r['entry_obj']) return found
[docs] @parser(Specs.pam_conf) class PamConf(PamDConf): """Base class for parsing pam config file ``/etc/pam.conf``. Based on the PamDConf parser class, but the service must be given as the first element of the line, rather than assumed from the file name. """
[docs] def parse_content(self, content): self.data = [] for line in get_active_lines(content): self.data.append(PamConfEntry(line))