diff --git a/src/middlewared/middlewared/etc_files/pam.d/common-account.mako b/src/middlewared/middlewared/etc_files/pam.d/common-account.mako index bc77319314e7c..19e80582cc491 100644 --- a/src/middlewared/middlewared/etc_files/pam.d/common-account.mako +++ b/src/middlewared/middlewared/etc_files/pam.d/common-account.mako @@ -6,12 +6,27 @@ # the central access policy for use on the system. The default is to # only deny service to users whose accounts are expired in /etc/shadow. # -<%namespace name="pam" file="pam.inc.mako" />\ <% - dsp = pam.getDirectoryServicePam(middleware=middleware, render_ctx=render_ctx).pam_account() + from middlewared.utils.directoryservices.constants import DSType + from middlewared.utils.pam import STANDALONE_ACCOUNT, AD_ACCOUNT, SSS_ACCOUNT + + match (dstype := render_ctx['directoryservices.status']['type']): + # dstype of None means standalone server + case None: + conf = STANDALONE_ACCOUNT + case DSType.AD.value: + conf = AD_ACCOUNT + case DSType.LDAP.value | DSType.IPA.value: + conf = SSS_ACCOUNT + case _: + raise TypeError(f'{dstype}: unknown DSType') %>\ -${'\n'.join(dsp['primary'])} +% if conf.primary: +${'\n'.join(line.as_conf() for line in conf.primary)} +% endif account requisite pam_deny.so account required pam_permit.so -${'\n'.join(dsp['additional'])} +% if conf.secondary: +${'\n'.join(line.as_conf() for line in conf.secondary)} +% endif diff --git a/src/middlewared/middlewared/etc_files/pam.d/common-auth.mako b/src/middlewared/middlewared/etc_files/pam.d/common-auth.mako index 0abf0ed61a64c..30a82c2e5b0d1 100644 --- a/src/middlewared/middlewared/etc_files/pam.d/common-auth.mako +++ b/src/middlewared/middlewared/etc_files/pam.d/common-auth.mako @@ -7,11 +7,26 @@ # (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the # traditional Unix authentication mechanisms. -<%namespace name="pam" file="pam.inc.mako" />\ <% - dsp = pam.getDirectoryServicePam(middleware=middleware, render_ctx=render_ctx).pam_auth() + from middlewared.utils.directoryservices.constants import DSType + from middlewared.utils.pam import STANDALONE_AUTH, AD_AUTH, SSS_AUTH + + match (dstype := render_ctx['directoryservices.status']['type']): + # dstype of None means standalone server + case None: + conf = STANDALONE_AUTH + case DSType.AD.value: + conf = AD_AUTH + case DSType.LDAP.value | DSType.IPA.value: + conf = SSS_AUTH + case _: + raise TypeError(f'{dstype}: unknown DSType') %>\ -${'\n'.join(dsp['primary'])} +% if conf.primary: +${'\n'.join(line.as_conf() for line in conf.primary)} +% endif @include common-auth-unix -${'\n'.join(dsp['additional'])} +% if conf.secondary: +${'\n'.join(line.as_conf() for line in conf.secondary)} +% endif diff --git a/src/middlewared/middlewared/etc_files/pam.d/common-password.mako b/src/middlewared/middlewared/etc_files/pam.d/common-password.mako index 9eccca8b1c6cb..a93504460edf5 100644 --- a/src/middlewared/middlewared/etc_files/pam.d/common-password.mako +++ b/src/middlewared/middlewared/etc_files/pam.d/common-password.mako @@ -4,23 +4,9 @@ # This file is included from other service-specific PAM config files, # and should contain a list of modules that define the services to be # used to change user passwords. The default is pam_unix. - -# Explanation of pam_unix options: -# -# The "sha512" option enables salted SHA512 passwords. Without this option, -# the default is Unix crypt. Prior releases used the option "md5". # -# The "obscure" option replaces the old `OBSCURE_CHECKS_ENAB' option in -# login.defs. -# -# See the pam_unix manpage for other options. - -<%namespace name="pam" file="pam.inc.mako" />\ -<% - dsp = pam.getDirectoryServicePam(middleware=middleware, render_ctx=render_ctx).pam_password() -%>\ +# The ability to change password via PAM is disabled on TrueNAS. Any account +# password changes should be made through the TrueNAS middleware -${'\n'.join(dsp['primary'])} password requisite pam_deny.so password required pam_permit.so -${'\n'.join(dsp['additional'])} diff --git a/src/middlewared/middlewared/etc_files/pam.d/common-session-noninteractive.mako b/src/middlewared/middlewared/etc_files/pam.d/common-session-noninteractive.mako index 97549ed9fa0f3..75cbadd1f23a7 100644 --- a/src/middlewared/middlewared/etc_files/pam.d/common-session-noninteractive.mako +++ b/src/middlewared/middlewared/etc_files/pam.d/common-session-noninteractive.mako @@ -6,13 +6,29 @@ # and should contain a list of modules that define tasks to be performed # at the start and end of all non-interactive sessions. # -<%namespace name="pam" file="pam.inc.mako" />\ <% - dsp = pam.getDirectoryServicePam(middleware=middleware, render_ctx=render_ctx).pam_session() + from middlewared.utils.directoryservices.constants import DSType + from middlewared.utils.pam import STANDALONE_SESSION, AD_SESSION, SSS_SESSION + + match (dstype := render_ctx['directoryservices.status']['type']): + # dstype of None means standalone server + case None: + conf = STANDALONE_SESSION + case DSType.AD.value: + conf = AD_SESSION + case DSType.LDAP.value | DSType.IPA.value: + conf = SSS_SESSION + case _: + raise TypeError(f'{dstype}: unknown DSType') %>\ -${'\n'.join(dsp['primary'])} +% if conf.primary: +${'\n'.join(line.as_conf() for line in conf.primary)} +% endif session [default=1] pam_permit.so session requisite pam_deny.so session required pam_permit.so -${'\n'.join(dsp['additional'])} +% if conf.secondary: +${'\n'.join(line.as_conf() for line in conf.secondary)} +% endif + diff --git a/src/middlewared/middlewared/etc_files/pam.d/common-session.mako b/src/middlewared/middlewared/etc_files/pam.d/common-session.mako index 1169df0e7d2ea..d49685c4e82af 100644 --- a/src/middlewared/middlewared/etc_files/pam.d/common-session.mako +++ b/src/middlewared/middlewared/etc_files/pam.d/common-session.mako @@ -6,14 +6,36 @@ # at the start and end of sessions of *any* kind (both interactive and # non-interactive). -<%namespace name="pam" file="pam.inc.mako" />\ <% - dsp = pam.getDirectoryServicePam(middleware=middleware, render_ctx=render_ctx).pam_session() -%>\ + from middlewared.utils.directoryservices.constants import DSType + from middlewared.utils.pam import TTY_AUDIT_LINE, STANDALONE_SESSION, AD_SESSION, SSS_SESSION + + tty_audit_line = None -${'\n'.join(dsp['primary'])} + match (dstype := render_ctx['directoryservices.status']['type']): + # dstype of None means standalone server + case None: + conf = STANDALONE_SESSION + case DSType.AD.value: + conf = AD_SESSION + case DSType.LDAP.value | DSType.IPA.value: + conf = SSS_SESSION + case _: + raise TypeError(f'{dstype}: unknown DSType') + + if render_ctx['system.security.config']['enable_gpos_stig']: + tty_audit_line = TTY_AUDIT_LINE +%>\ +% if conf.tty_audit_line: +${TTY_AUDIT_LINE.as_conf()} +% endif +% if conf.primary: +${'\n'.join(line.as_conf() for line in conf.primary)} +% endif session [default=1] pam_permit.so session requisite pam_deny.so session required pam_permit.so session optional pam_systemd.so -${'\n'.join(dsp['additional'])} +% if conf.secondary: +${'\n'.join(line.as_conf() for line in conf.secondary)} +% endif diff --git a/src/middlewared/middlewared/etc_files/pam.d/middleware-api-key.mako b/src/middlewared/middlewared/etc_files/pam.d/middleware-api-key.mako index aa950f7fd8b80..8a1b44cb99f2b 100644 --- a/src/middlewared/middlewared/etc_files/pam.d/middleware-api-key.mako +++ b/src/middlewared/middlewared/etc_files/pam.d/middleware-api-key.mako @@ -1,6 +1,7 @@ <% from middlewared.utils import filter_list from middlewared.utils.auth import LEGACY_API_KEY_USERNAME + from middlewared.utils.pam import STANDALONE_AUTH ds_auth = render_ctx['datastore.config']['stg_ds_auth'] truenas_admin_string = '' @@ -18,11 +19,7 @@ auth [success=1 default=die] pam_tdb.so ${truenas_admin_string} %if ds_auth: @include common-account %else: -<%namespace name="pam" file="pam.inc.mako" />\ -<% - dsp = pam.getNoDirectoryServicePam().pam_account() -%>\ -${'\n'.join(dsp['primary'])} +${'\n'.join(line.as_conf() for line in STANDALONE_AUTH.primary)} @include common-account-unix %endif password required pam_deny.so diff --git a/src/middlewared/middlewared/etc_files/pam.d/middleware.mako b/src/middlewared/middlewared/etc_files/pam.d/middleware.mako index 6d78f56151397..b8c239084abbb 100644 --- a/src/middlewared/middlewared/etc_files/pam.d/middleware.mako +++ b/src/middlewared/middlewared/etc_files/pam.d/middleware.mako @@ -1,4 +1,6 @@ <% + from middlewared.utils.pam import STANDALONE_AUTH + ds_auth = render_ctx['datastore.config']['stg_ds_auth'] %>\ # PAM configuration for the middleware (Web UI / API login) @@ -6,11 +8,7 @@ %if ds_auth: @include common-auth %else: -<%namespace name="pam" file="pam.inc.mako" />\ -<% - dsp = pam.getNoDirectoryServicePam().pam_auth() -%>\ -${'\n'.join(dsp['primary'])} +${'\n'.join(line.as_conf() for line in STANDALONE_AUTH.primary)} @include common-auth-unix %endif @include common-account diff --git a/src/middlewared/middlewared/etc_files/pam.d/pam.inc.mako b/src/middlewared/middlewared/etc_files/pam.d/pam.inc.mako deleted file mode 100644 index c8ea4783971a3..0000000000000 --- a/src/middlewared/middlewared/etc_files/pam.d/pam.inc.mako +++ /dev/null @@ -1,220 +0,0 @@ -<%! - class DirectoryServicePamBase(object): - def __init__(self, **kwargs): - self.middleware = kwargs.get('middleware') - self.pam_mkhomedir = "pam_mkhomedir.so" - self.pam_sss = "pam_sss.so" - self.pam_winbind = "pam_winbind.so" - self.pam_unix = "pam_unix.so" - self.render_ctx = kwargs.get("render_ctx") - - def base_control(self, success=1, default='ignore', **kwargs): - out = {'success': success} | (kwargs | {'default': default}) - return [f'{key}={val}' for key, val in out.items()] - - def name(self): - return 'Base' - - def enabled(self): - return False - - def generate_pam_line(self, pam_type, pam_control, pam_path, pam_args=None): - return '\t'.join([ - pam_type, - f'[{" ".join(pam_control)}]' if isinstance(pam_control, list) else pam_control, - pam_path, - ' '.join(pam_args or []) - ]) - - def pam_auth(self, **kwargs): - pam_path = kwargs.pop('pam_path', self.pam_unix) - pam_args = kwargs.pop('pam_args', None) - pam_control = self.base_control(**kwargs) - - pam_line = self.generate_pam_line( - 'auth', - pam_control, - pam_path, - pam_args - ) - if self.name() != 'Base': - return pam_line - - return {'primary': [pam_line], 'additional': []} - - def pam_account(self, **kwargs): - pam_path = kwargs.pop('pam_path', self.pam_unix) - pam_args = kwargs.pop('pam_args', None) - pam_control = self.base_control(**(kwargs | {'new_authtok_reqd': 'done'})) - - pam_line = self.generate_pam_line( - 'account', - pam_control, - pam_path, - pam_args - ) - if self.name() != 'Base': - return pam_line - - return {'primary': [pam_line], 'additional': []} - - def pam_session(self, **kwargs): - pam_control = kwargs.pop('pam_control', 'required') - pam_path = kwargs.pop('pam_path', self.pam_unix) - pam_args = kwargs.pop('pam_args', None) - - pam_line = self.generate_pam_line( - 'session', - pam_control, - pam_path, - pam_args - ) - if self.name() != 'Base': - return pam_line - - mkhomedir = self.generate_pam_line( - 'session', - pam_control, - self.pam_mkhomedir, - pam_args - ) - - return {'primary': [], 'additional': [pam_line, mkhomedir]} - - def pam_password(self, **kwargs): - pam_path = kwargs.pop('pam_path', self.pam_unix) - pam_args = kwargs.pop('pam_args', [ - 'use_authtok', - 'try_first_pass', - 'obscure', - 'sha512', - ]) - pam_control = self.base_control(**kwargs) - - pam_line = self.generate_pam_line( - 'password', - pam_control, - pam_path, - pam_args - ) - if self.name() != 'Base': - return pam_line - - return {'primary': [pam_line], 'additional': []} - - class ActiveDirectoryPam(DirectoryServicePamBase): - def __init__(self, **kwargs): - super(ActiveDirectoryPam, self).__init__(**kwargs) - - def name(self): - return 'ActiveDirectory' - - def enabled(self): - config = self.render_ctx['activedirectory.config'] - if config['restrict_pam']: - return False - - return config['enable'] - - def pam_auth(self): - args = ["try_first_pass", "try_authtok", "krb5_auth"] - - unix_auth = super().pam_auth(success=2) - this_auth = super().pam_auth(pam_path=self.pam_winbind, success=1, pam_args=args) - return {'primary': [unix_auth, this_auth], 'additional': []} - - def pam_account(self): - args = ["krb5_auth", "krb5_ccache_type=FILE"] - - unix_account = super().pam_account(success=2) - wb_account = super().pam_account(pam_path=self.pam_winbind, success=1, pam_args=args) - return {'primary': [unix_account, wb_account], 'additional': []} - - def pam_session(self): - unix_session = super().pam_session() - mkhomedir = super().pam_session(pam_path=self.pam_mkhomedir, pam_control='required') - wb_session = super().pam_session(pam_path=self.pam_winbind, pam_control='optional') - return {'primary': [], 'additional': [unix_session, mkhomedir, wb_session]} - - def pam_password(self): - args = ["try_first_pass", "krb5_auth", "krb5_ccache_type=FILE"] - - unix_passwd = super().pam_password(success=2) - wb_passwd = super().pam_password(success=1, pam_path=self.pam_winbind, pam_args=args) - return {'primary': [unix_passwd, wb_passwd], 'additional': []} - - - class LDAPPam(DirectoryServicePamBase): - def __init__(self, **kwargs): - super(LDAPPam, self).__init__(**kwargs) - - def name(self): - return 'LDAP' - - def enabled(self): - return self.render_ctx['ldap.config']['enable'] - - def pam_auth(self): - ldap_args = [ - "ignore_unknown_user", - "use_first_pass" - ] - - unix_entry = super().pam_auth(success=2) - ldap_entry = super().pam_auth(pam_path=self.pam_sss, success=1, pam_args=ldap_args) - entries = [unix_entry, ldap_entry] - - return {'primary': entries, 'additional': []} - - def pam_account(self): - ldap_args = { - 'user_unknown': 'ignore', - 'default': 'bad' - } - unix_entry = super().pam_account() - ldap_entry = super().pam_account(success="ok", pam_path=self.pam_sss, **ldap_args) - - return {'primary': [unix_entry], 'additional': [ldap_entry]} - - def pam_session(self): - entries = [super().pam_session()] - entries.append(super().pam_session(pam_path=self.pam_sss, pam_control='optional')) - entries.append(super().pam_session(pam_path=self.pam_mkhomedir, pam_control='required')) - - return {'primary': [], 'additional': entries} - - def pam_password(self): - ldap_args = [ - "use_authtok", - ] - - unix_entry = super().pam_password(success=2) - ldap_entry = super().pam_password(pam_path=self.pam_sss, pam_args=ldap_args, success=1) - - entries = [unix_entry, ldap_entry] - - return {'primary': entries, 'additional': []} - - class DirectoryServicePam(DirectoryServicePamBase): - def __new__(cls, **kwargs): - obj = None - - try: - if ActiveDirectoryPam(**kwargs).enabled(): - obj = ActiveDirectoryPam(**kwargs) - elif LDAPPam(**kwargs).enabled(): - obj = LDAPPam(**kwargs) - except Exception: - obj = None - - if not obj: - obj = DirectoryServicePamBase() - - return obj -%> -<%def name="getDirectoryServicePam(**kwargs)"> - <% return DirectoryServicePam(**kwargs) %> - -<%def name="getNoDirectoryServicePam()"> - <% return DirectoryServicePamBase() %> - diff --git a/src/middlewared/middlewared/plugins/etc.py b/src/middlewared/middlewared/plugins/etc.py index b029b2da0987f..7dc9beb174c49 100644 --- a/src/middlewared/middlewared/plugins/etc.py +++ b/src/middlewared/middlewared/plugins/etc.py @@ -167,8 +167,7 @@ class EtcService(Service): }, 'pam': { 'ctx': [ - {'method': 'activedirectory.config'}, - {'method': 'ldap.config'}, + {'method': 'directoryservices.status'}, ], 'entries': [ {'type': 'mako', 'path': 'pam.d/common-account'}, diff --git a/src/middlewared/middlewared/utils/pam.py b/src/middlewared/middlewared/utils/pam.py new file mode 100644 index 0000000000000..db79c3f739aa6 --- /dev/null +++ b/src/middlewared/middlewared/utils/pam.py @@ -0,0 +1,302 @@ +import enum +from dataclasses import dataclass + + +class PAMModule(enum.StrEnum): + DENY = 'pam_deny.so' + ENV = 'pam_env.so' + KEYINIT = 'pam_keyinit.so' + MKHOMEDIR = 'pam_mkhomedir.so' + LIMITS = 'pam_limits.so' + LOGINUID = 'pam_loginuid.so' + MOTD = 'pam_motd.so' + PERMIT = 'pam_permit.so' + OATH = 'pam_oath.so' + UNIX = 'pam_unix.so' + SSS = 'pam_sss.so' + TDB = 'pam_tdb.so' + TTY_AUDIT = 'pam_tty.so' + WINBIND = 'pam_winbind.so' + + +class PAMService(enum.StrEnum): + ACCOUNT = 'account' + AUTH = 'auth' + PASSWORD = 'password' + SESSION = 'session' + + +class PAMSimpleControl(enum.StrEnum): + # Subset of the simple (historical) pam control values + # See man(5) pam.conf + + # Equivalent to [success=ok new_authtok_reqd=ok ignore=ignore default=bad] + REQUIRED = 'required' + # Equivalent to [success=ok new_authtok_reqd=ok ignore=ignore default=die] + REQUISITE = 'requisite' + # Equivalent to [success=ok new_authtok_reqd=done ignore=ignore] + SUFFICIENT = 'sufficient' + # Equivalent to [success=ok new_authtok_reqd=ok ignore=ignore] + OPTIONAL = 'optional' + + +class PAMResponse(enum.StrEnum): + # Subset of possible control keys for pam line + # See man(5) pam.conf and _pam_types.h from pam development libraries + DEFAULT = 'default' + SUCCESS = 'success' + NEW_AUTHTOK_REQD = 'new_authtok_reqd' + USER_UNKNOWN = 'user_unknown' + + +class PAMAction(enum.StrEnum): + # Subset of possible actions for control keys in pam line + IGNORE = 'ignore' + BAD = 'bad' + DIE = 'die' + OK = 'ok' + DONE = 'done' + RESET = 'reset' + + +@dataclass(slots=True, frozen=True) +class PAMControl: + response: PAMResponse + action: PAMAction | int + + def as_conf(self): + return f'{self.response}={self.action}' + + +@dataclass(slots=True, frozen=True) +class PAMLine: + pam_service: PAMService + pam_control: PAMSimpleControl | tuple[PAMControl] + pam_module: PAMModule + pam_module_args: tuple[str] | None = None + + def __dump_control(self): + if isinstance(self.pam_control, PAMSimpleControl): + return self.pam_control + + return f'[{" ".join(ctrl.as_conf() for ctrl in self.pam_control)}]' + + def as_conf(self): + if self.pam_module_args is None: + return '\t'.join([ + self.pam_service, + self.__dump_control(), + self.pam_module + ]) + + return '\t'.join([ + self.pam_service, + self.__dump_control(), + self.pam_module, + ' '.join(self.pam_module_args) + ]) + + +@dataclass(slots=True, frozen=True) +class PAMConfLines: + service: PAMService + primary: tuple[PAMLine] + secondary: tuple[PAMLine] | None = None + + +# Below this point are intialized PAM configuration objects for use in mako files + +STANDALONE_ACCOUNT = PAMConfLines( + service=PAMService.ACCOUNT, + primary=( + PAMLine( + pam_service=PAMService.ACCOUNT, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 1), + PAMControl(PAMResponse.NEW_AUTHTOK_REQD, PAMAction.DONE), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.UNIX + ), + ) +) + +AD_ACCOUNT = PAMConfLines( + service=PAMService.ACCOUNT, + primary=( + PAMLine( + pam_service=PAMService.ACCOUNT, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 2), + PAMControl(PAMResponse.NEW_AUTHTOK_REQD, PAMAction.DONE), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.UNIX + ), + PAMLine( + pam_service=PAMService.ACCOUNT, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 1), + PAMControl(PAMResponse.NEW_AUTHTOK_REQD, PAMAction.DONE), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.WINBIND, + pam_module_args=('krb5_auth', 'krb5_ccache_type=FILE') + ) + ) +) + +SSS_ACCOUNT = PAMConfLines( + service=PAMService.ACCOUNT, + primary=( + PAMLine( + pam_service=PAMService.ACCOUNT, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 2), + PAMControl(PAMResponse.NEW_AUTHTOK_REQD, PAMAction.DONE), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.UNIX + ), + ), + secondary=( + PAMLine( + pam_service=PAMService.ACCOUNT, + pam_control=( + PAMControl(PAMResponse.SUCCESS, PAMAction.OK), + PAMControl(PAMResponse.NEW_AUTHTOK_REQD, PAMAction.DONE), + PAMControl(PAMResponse.USER_UNKNOWN, PAMAction.IGNORE), + PAMControl(PAMResponse.DEFAULT, PAMAction.BAD) + ), + pam_module=PAMModule.SSS + ), + ) +) + +STANDALONE_AUTH = PAMConfLines( + service=PAMService.AUTH, + primary=( + PAMLine( + pam_service=PAMService.AUTH, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 1), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.UNIX + ), + ), +) + +AD_AUTH = PAMConfLines( + service=PAMService.AUTH, + primary=( + PAMLine( + pam_service=PAMService.AUTH, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 2), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.UNIX + ), + PAMLine( + pam_service=PAMService.AUTH, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 1), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.WINBIND, + pam_module_args=('try_first_pass', 'try_authtok', 'krb5_auth') + ) + ) +) + +SSS_AUTH = PAMConfLines( + service=PAMService.AUTH, + primary=( + PAMLine( + pam_service=PAMService.AUTH, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 2), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.UNIX + ), + PAMLine( + pam_service=PAMService.AUTH, + pam_control=( + PAMControl(PAMResponse.SUCCESS, 2), + PAMControl(PAMResponse.DEFAULT, PAMAction.IGNORE) + ), + pam_module=PAMModule.SSS, + pam_module_args=('ignore_unknown_user', 'use_first_pass') + ) + ) +) + +STANDALONE_SESSION = PAMConfLines( + service=PAMService.SESSION, + primary=(), + secondary=( + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.REQUIRED, + pam_module=PAMModule.UNIX + ), + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.REQUIRED, + pam_module=PAMModule.MKHOMEDIR + ) + ) +) + +AD_SESSION = PAMConfLines( + service=PAMService.SESSION, + primary=(), + secondary=( + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.REQUIRED, + pam_module=PAMModule.UNIX + ), + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.REQUIRED, + pam_module=PAMModule.MKHOMEDIR + ), + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.OPTIONAL, + pam_module=PAMModule.WINBIND + ), + ) +) + +SSS_SESSION = PAMConfLines( + service=PAMService.SESSION, + primary=(), + secondary=( + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.REQUIRED, + pam_module=PAMModule.UNIX + ), + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.OPTIONAL, + pam_module=PAMModule.SSS, + ), + PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.REQUIRED, + pam_module=PAMModule.MKHOMEDIR + ) + ) +) + +TTY_AUDIT_LINE = PAMLine( + pam_service=PAMService.SESSION, + pam_control=PAMSimpleControl.REQUIRED, + pam_module=PAMModule.TTY_AUDIT, + pam_module_args=('disable=*', 'enable=root') +) diff --git a/tests/unit/test_pam_config.py b/tests/unit/test_pam_config.py new file mode 100644 index 0000000000000..48f9a6f44039d --- /dev/null +++ b/tests/unit/test_pam_config.py @@ -0,0 +1,49 @@ +import os +import pytest + +from middlewared.utils import pam + + +def test_pam_control(): + # First check with a generic PAMResponse and PAMAction + p = pam.PAMControl(pam.PAMResponse.SUCCESS, pam.PAMAction.OK) + assert p.as_conf() == 'success=ok' + + # Next check that specifying an integer jump also works + p = pam.PAMControl(pam.PAMResponse.SUCCESS, 2) + assert p.as_conf() == 'success=2' + + +@pytest.mark.parametrize('svc,control,module,module_args,expected', [ + ( + pam.PAMService.SESSION, + pam.PAMSimpleControl.REQUIRED, + pam.PAMModule.SSS, + ('test1', 'test2'), + 'session\trequired\tpam_sss.so\ttest1 test2' + ), + ( + pam.PAMService.AUTH, + ( + pam.PAMControl( + pam.PAMResponse.SUCCESS, + 1 + ), + pam.PAMControl( + pam.PAMResponse.NEW_AUTHTOK_REQD, + pam.PAMAction.OK + ) + ), + pam.PAMModule.WINBIND, + None, + 'auth\t[success=1 new_authtok_reqd=ok]\tpam_winbind.so' + ), +]) +def test_pam_line(svc, control, module, module_args, expected): + p = pam.PAMLine(svc, control, module, module_args) + assert p.as_conf() == expected + + +@pytest.mark.parametrize('pam_module', pam.PAMModule) +def test_map_module_exists(pam_module): + assert os.path.exists(f'/usr/lib/x86_64-linux-gnu/security/{pam_module}')