"""
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))