From add7a8931aac0da149057069d7faa8e79643cdba Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Mon, 19 Sep 2022 18:07:52 +0200 Subject: [PATCH] Adding additional properties to configured LDAP instances. --- etc/ldap.yaml.default | 83 +++++++++++++++++++++++ lib/pp_admintools/app/ldap.py | 35 ++++++++-- lib/pp_admintools/app/remove_ldap_user.py | 38 +++++++++++ lib/pp_admintools/config/__init__.py | 4 +- lib/pp_admintools/config/ldap.py | 74 +++++++++++++++++++- 5 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 etc/ldap.yaml.default diff --git a/etc/ldap.yaml.default b/etc/ldap.yaml.default new file mode 100644 index 0000000..47c885a --- /dev/null +++ b/etc/ldap.yaml.default @@ -0,0 +1,83 @@ +--- +ldap: + timeout: 5 + default: + host: 'prd-ds.pixelpark.com' + ldaps: true + port: 636 + base_dn: 'o=isp' + bind_dn: 'uid=readonly,ou=People,o=isp' + # bind_pw: ****** + is_admin: false + readonly: true + tier: 'prod' + dpx-dev: + host: 'dev-ldap2.pixelpark.com' + ldaps: true + port: 636 + base_dn: 'o=isp' + bind_dn: 'cn=admin' + # bind_pw: ****** + is_admin: true + readonly: false + tier: 'dev' + dpx-dev-ro: + host: 'dev-ldap2.pixelpark.com' + ldaps: true + port: 636 + base_dn: 'o=isp' + bind_dn: 'uid=readonly,ou=People,o=isp' + # bind_pw: ****** + is_admin: false + readonly: true + tier: 'dev' + dpx-prod: + host: 'prd-ds.pixelpark.com' + ldaps: true + port: 636 + base_dn: 'o=isp' + bind_dn: 'cn=admin' + # bind_pw: ****** + is_admin: true + readonly: false + tier: 'prod' + dpx-prod-ro: + host: 'prd-ds.pixelpark.com' + ldaps: true + port: 636 + base_dn: 'o=isp' + bind_dn: 'uid=readonly,ou=People,o=isp' + # bind_pw: ****** + is_admin: false + readonly: true + tier: 'prod' + dpx-legacy: + host: 'ldap-legacy.pixelpark.com' + ldaps: false + port: 389 + base_dn: 'o=isp' + bind_dn: 'cn=admin' + # bind_pw: ****** + is_admin: true + readonly: false + tier: 'prod' + spk-live: + host: 'live-ldap.spk.pixelpark.net' + ldaps: true + port: 636 + base_dn: 'dc=spk,dc=pixelpark,dc=net' + bind_dn: 'cn=admin' + # bind_pw: ****** + is_admin: true + readonly: false + tier: 'prod' + spk-stage: + host: 'stage-ldap.spk.pixelpark.net' + ldaps: true + port: 636 + base_dn: 'dc=spk,dc=pixelpark,dc=net' + bind_dn: 'cn=admin' + # bind_pw: ****** + is_admin: true + readonly: false + tier: 'test' diff --git a/lib/pp_admintools/app/ldap.py b/lib/pp_admintools/app/ldap.py index ec73916..7293036 100644 --- a/lib/pp_admintools/app/ldap.py +++ b/lib/pp_admintools/app/ldap.py @@ -30,14 +30,14 @@ from ldap3 import ALL_ATTRIBUTES from ldap3.core.exceptions import LDAPException # from ldap3.core.exceptions import LDAPException, LDAPBindError -from fb_tools.common import pp, is_sequence +from fb_tools.common import pp, is_sequence, to_bool from fb_tools.mailaddress import MailAddress from fb_tools.collections import FrozenCIStringSet, CIStringSet, CIDict # Own modules from .. import __version__ as GLOBAL_VERSION -from ..xlate import XLATOR +from ..xlate import XLATOR, format_list from .. import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR @@ -50,7 +50,7 @@ from ..config.ldap import LdapConnectionInfo, LdapConfiguration # rom ..config.ldap import DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS from ..config.ldap import DEFAULT_TIMEOUT -__version__ = '0.5.1' +__version__ = '0.6.1' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -441,15 +441,38 @@ class BaseLdapApplication(BaseDPXApplication): print() # ------------------------------------------------------------------------- - def _verify_instances(self): + def _verify_instances(self, is_admin=None, readonly=None, tier=None): if self.verbose > 1: LOG.debug(_("Verifying given instances ...")) + if self.verbose > 2: + show_filter = [] + show_filter.append("inst != 'default'") + if is_admin is not None: + show_filter.append('is_admin = {!r}'.format(is_admin)) + if readonly is not None: + show_filter.append('readonly = {!r}'.format(readonly)) + if tier is not None: + show_filter.append('tier = {!r}'.format(tier)) + msg = _("Used filter:") + ' ' + format_list(show_filter) + LOG.debug(msg) + instances = [] for inst in self.cfg.ldap_connection.keys(): - if inst != 'default': - instances.append(inst.lower()) + if inst == 'default': + continue + instance = self.cfg.ldap_connection[inst] + if is_admin is not None: + if to_bool(is_admin) != instance.is_admin: + continue + if readonly is not None: + if to_bool(readonly) != instance.readonly: + continue + if tier is not None: + if tier.strip().lower() != instance.tier: + continue + instances.append(inst.lower()) if len(self.ldap_instances) == 1 and self.ldap_instances[0].lower() == 'all': self.ldap_instances = instances diff --git a/lib/pp_admintools/app/remove_ldap_user.py b/lib/pp_admintools/app/remove_ldap_user.py index f94f799..9c75561 100644 --- a/lib/pp_admintools/app/remove_ldap_user.py +++ b/lib/pp_admintools/app/remove_ldap_user.py @@ -141,6 +141,11 @@ class RemoveLdapUserApplication(BaseLdapApplication): "different in the particular LDAP instances).") ) + # ------------------------------------------------------------------------- + def _verify_instances(self): + + super(RemoveLdapUserApplication, self)._verify_instances(is_admin=True, readonly=False) + # ------------------------------------------------------------------------- def post_init(self): """ @@ -158,6 +163,39 @@ class RemoveLdapUserApplication(BaseLdapApplication): self.given_users = given_users + self.check_instances() + + # ------------------------------------------------------------------------- + def check_instances(self): + """Checking given instances for admin and read/write access.""" + + msg = _("Checking given instances for admin and read/write access.") + LOG.debug(msg) + + all_ok = True + + for inst_name in self.ldap_instances: + if inst_name not in self.cfg.ldap_connection: + msg = _("LDAP instance {!r} not found in configuration.").format(inst_name) + LOG.error(msg) + all_ok = False + continue + + inst = self.cfg.ldap_connection[inst_name] + + if inst.readonly: + msg = _("LDAP instance {!r} has only readonly access.").format(inst_name) + LOG.error(msg) + all_ok = False + + if not inst.is_admin: + msg = _("No admin access to LDAP instance {!r}.").format(inst_name) + LOG.error(msg) + all_ok = False + + if not all_ok: + self.exit(8) + # ------------------------------------------------------------------------- def _run(self): diff --git a/lib/pp_admintools/config/__init__.py b/lib/pp_admintools/config/__init__.py index d272baf..cd7c683 100644 --- a/lib/pp_admintools/config/__init__.py +++ b/lib/pp_admintools/config/__init__.py @@ -30,10 +30,12 @@ from ..errors import PpError from ..xlate import XLATOR CONFIG_DIR = 'pixelpark' -__version__ = '0.2.1' +__version__ = '0.2.2' LOG = logging.getLogger(__name__) VALID_MAIL_METHODS = ('smtp', 'sendmail') DEFAULT_DOMAIN = 'pixelpark.com' +VALID_TIERS = ('prod', 'test', 'dev') +DEFAULT_TIER = 'prod' _ = XLATOR.gettext diff --git a/lib/pp_admintools/config/ldap.py b/lib/pp_admintools/config/ldap.py index 377d892..113722f 100644 --- a/lib/pp_admintools/config/ldap.py +++ b/lib/pp_admintools/config/ldap.py @@ -28,9 +28,11 @@ from fb_tools.obj import FbGenericBaseObject, FbBaseObject from .. import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR +from . import VALID_TIERS, DEFAULT_TIER + from ..xlate import XLATOR -__version__ = '0.2.6' +__version__ = '0.3.1' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -56,7 +58,8 @@ class LdapConnectionInfo(FbBaseObject): def __init__( self, appname=None, verbose=0, version=__version__, base_dir=None, host=None, use_ldaps=False, port=DEFAULT_PORT_LDAP, base_dn=None, - bind_dn=None, bind_pw=None, initialized=False): + bind_dn=None, bind_pw=None, is_admin=None, readonly=None, tier=None, + initialized=False): self._host = None self._use_ldaps = False @@ -64,6 +67,9 @@ class LdapConnectionInfo(FbBaseObject): self._base_dn = None self._bind_dn = None self._bind_pw = None + self._is_admin = False + self._readonly = True + self._tier = DEFAULT_TIER super(LdapConnectionInfo, self).__init__( appname=appname, verbose=verbose, version=version, base_dir=base_dir, @@ -79,6 +85,9 @@ class LdapConnectionInfo(FbBaseObject): self.bind_dn = bind_dn if bind_pw is not None: self.bind_pw = bind_pw + self.is_admin = is_admin + self.readonly = readonly + self.tier = tier if initialized: self.initialized = True @@ -105,6 +114,9 @@ class LdapConnectionInfo(FbBaseObject): res['bind_pw'] = None res['schema'] = self.schema res['url'] = self.url + res['is_admin'] = self.is_admin + res['readonly'] = self.readonly + res['tier'] = self.tier if self.bind_pw: if self.verbose > 4: @@ -216,6 +228,44 @@ class LdapConnectionInfo(FbBaseObject): return '{s}://{h}{p}'.format(s=self.schema, h=self.host, p=port) + # ----------------------------------------------------------- + @property + def is_admin(self): + """Is this an admin connection with all permissions.""" + return self._is_admin + + @is_admin.setter + def is_admin(self, value): + if value is not None: + self._is_admin = to_bool(value) + + # ----------------------------------------------------------- + @property + def readonly(self): + """Is this a readonly connection.""" + return self._readonly + + @readonly.setter + def readonly(self, value): + if value is not None: + self._readonly = to_bool(value) + + # ----------------------------------------------------------- + @property + def tier(self): + """The tier of production level of the LDAP instance (prod, test or dev).""" + return self._tier + + @tier.setter + def tier(self, value): + if value is None: + return + val_lc = str(value).strip().lower() + if val_lc not in VALID_TIERS: + msg = _("Invalid production tier {!r} given.").format(value) + raise LdapConfigError(msg) + self._tier = val_lc + # ------------------------------------------------------------------------- def __repr__(self): """Typecasting into a string for reproduction.""" @@ -230,6 +280,9 @@ class LdapConnectionInfo(FbBaseObject): fields.append("base_dn={!r}".format(self.base_dn)) fields.append("bind_dn={!r}".format(self.bind_dn)) fields.append("bind_pw={!r}".format(self.bind_pw)) + fields.append("is_admin={!r}".format(self.is_admin)) + fields.append("readonly={!r}".format(self.readonly)) + fields.append("tier={!r}".format(self.tier)) fields.append("initialized={!r}".format(self.initialized)) out += ", ".join(fields) + ")>" @@ -241,7 +294,8 @@ class LdapConnectionInfo(FbBaseObject): new = self.__class__( appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, host=self.host, use_ldaps=self.use_ldaps, port=self.port, base_dn=self.base_dn, bind_dn=self.bind_dn, - bind_pw=self.bind_pw, initialized=self.initialized) + bind_pw=self.bind_pw, is_admin=self.is_admin, readonly=self.readonly, tier=self.tier, + initialized=self.initialized) return new @@ -301,6 +355,8 @@ class LdapConfiguration(BaseMultiConfig): re_ldap_base_dn_key = re.compile(r'^\s*base[_-]*dn\s*$', re.IGNORECASE) re_ldap_bind_dn_key = re.compile(r'^\s*bind[_-]*dn\s*$', re.IGNORECASE) re_ldap_bind_pw_key = re.compile(r'^\s*bind[_-]*pw\s*$', re.IGNORECASE) + re_ldap_is_admin_key = re.compile(r'^\s*(?:is[_-]*)?admin\s*$', re.IGNORECASE) + re_ldap_readonly_key = re.compile(r'^\s*read[_-]*only\s*$', re.IGNORECASE) # ------------------------------------------------------------------------- def __init__( @@ -448,6 +504,18 @@ class LdapConfiguration(BaseMultiConfig): connection.bind_pw = value continue + if self.re_ldap_is_admin_key.match(key): + connection.is_admin = value + continue + + if self.re_ldap_readonly_key.match(key): + connection.readonly = value + continue + + if key.strip().lower() == 'tier': + connection.tier = value + continue + msg = _("Unknown LDAP configuration key {key} found in section {sec!r}.").format( key=key, sec=section_name) LOG.error(msg) -- 2.39.5