diff --git a/src/ipa-tuura/ipatuura/ipa.py b/src/ipa-tuura/ipatuura/ipa.py index b682be16..6fe1c1c4 100644 --- a/src/ipa-tuura/ipatuura/ipa.py +++ b/src/ipa-tuura/ipatuura/ipa.py @@ -9,10 +9,9 @@ import six import ldap import ldap.modlist as modlist -import SSSDConfig import datetime +import domains -from django.conf import settings from ipalib.krb_utils import get_credentials_if_valid from ipalib import api from ipalib.errors import EmptyModlist @@ -88,7 +87,6 @@ def _ipa_connect(self): def _valid_creds(self): # try GSSAPI first - logger.info('valid creds') if "KRB5CCNAME" in os.environ: ccache = os.environ["KRB5CCNAME"] logger.info(f'ipa: init KRB5CCNAME set to {ccache}') @@ -104,7 +102,6 @@ def _valid_creds(self): # KRB5_CLIENT_KTNAME os env is defined in settings.py elif "KRB5_CLIENT_KTNAME" in os.environ: - logger.info('ktname') keytab = os.environ.get('KRB5_CLIENT_KTNAME', None) logger.info(f'KRB5_CLIENT_KTNAME set to {keytab}') ccache_name = "MEMORY:%s" % str(uuid.uuid4()) @@ -113,7 +110,7 @@ def _valid_creds(self): try: logger.info('kinit keytab') cred = kinit_keytab( - settings.SCIM_SERVICE_PROVIDER['WRITABLE_USER'], + domains.models.Domain.objects.last().client_id, keytab, ccache_name ) @@ -123,11 +120,10 @@ def _valid_creds(self): logger.info(f'Using principal {cred.name}') return True - logger.info('get credentials if valid') creds = get_credentials_if_valid() if creds and \ creds.lifetime > 0 and \ - "%s@" % settings.SCIM_SERVICE_PROVIDER['WRITABLE_USER'] in \ + "%s@" % domains.models.Domain.objects.last().client_id in \ creds.name.display_as(creds.name.name_type): return True return False @@ -195,84 +191,62 @@ class LDAP: """ Initialization of the LDAP writable interface """ - def __init__(self): self._conn = None - self._backend = None - self._context = "client" - self._ccache_dir = None - self._ccache_name = None - - # replace by sssd.conf settings. - self._base_dn = 'dc=ipa,dc=test' - self._uri = 'ldaps://idm.ipa.test' - self._tls_cacert = '/etc/ipa/ca.crt' - - # read from keycloak + self._dn = None + self._users_dn = None + self._ldap_uri = None + self._ldap_search_base = None + self._ldap_user_extra_attrs = None + self._user_object_classes = None + # TLS + self._ldap_tls_cacert = None self._sasl_gssapi = ldap.sasl.sasl({}, 'GSSAPI') - self._group = 'cn=users,cn=accounts' - self._objectClass = None - - self._read_sssd_config() - self._ldap_connect() - - def _read_sssd_config(self): + self._client_id = None + self._client_secret = None + # init and connect + self._fetch_domain() + self._conn = self._bind() + + def _fetch_domain(self): + """ + Fetch relevant information from the integration domain + """ + domain = domains.models.Domain.objects.last() + suffix = domain.name.split('.') + + self._dn = domain.client_id + self._ldap_uri = domain.integration_domain_url + self._ldap_user_extra_attrs = domain.user_extra_attrs + self._ldap_search_base = "dc=" + suffix[0] + ", dc=" + suffix[1] + self._ldap_tls_cacert = domain.ldap_tls_cacert + self._client_id = domain.client_id + self._client_secret = domain.client_secret + self._users_dn = domain.users_dn + self._user_object_classes = [ + x.strip() for x in domain.user_object_classes.split(',') + ] + + logger.info(f'Domain info: {domain}') + + def _bind(self): + """ + Bind to ldap server + """ + self._fetch_domain() + # TODO enable TLS support + # self._conn = ldap.initialize(self._ldap_uri) + # self._conn.set_option(ldap.OPT_X_TLS_CACERTFILE, self._tls_cacert) + # self._conn.sasl_interactive_bind_s('', self._sasl_gssapi) + self._conn = ldap.initialize(self._ldap_uri) + self._conn.protocol_version = 3 + self._conn.set_option(ldap.OPT_REFERRALS, 0) try: - sssdconfig = SSSDConfig.SSSDConfig() - sssdconfig.import_config() - except Exception as e: - # SSSD configuration does not exist or cannot be parsed - print("Unable to parse SSSD configuration") - print("Please ensure the host is properly configured.") - raise e - # Read attributes from the domain section - self._read_ldap_domains(sssdconfig) - - def _read_ldap_domains(self, sssdconfig): - """ - Configure the domains with extra attribute mappings - - Loop on the configured domains and configure the domain with extra - attribute mappings if the id_provider is "ldap". - """ - # Configure each ipa/ad/ldap domain - domains = sssdconfig.list_active_domains() - for name in domains: - domain = sssdconfig.get_domain(name) - provider = domain.get_option('id_provider') - if provider in {"ldap"}: - self._read_ldap_domain(domain) - - def _read_ldap_domain(self, domain): - """ - Configure the domain with extra attribute mappings - - Add the following ldap_user_extra_attrs mappings to the [domain/] - section: - mail:mail, sn:sn, givenname:givenname - If the section already defines some mappings, they are kept. - """ - try: - self._uri = domain.get_option('ldap_uri') - self._base_dn = domain.get_option('ldap_search_base') - self._tls_cacert = domain.get_option('ldap_tls_cacert') - except Exception as e: - # SSSD configuration does not exist or cannot be parsed - print("Unable to parse SSSD configuration") - print("Please ensure the host is properly configured.") - raise e - - def _ldap_connect(self): - """ - Create a connection to LDAP and bind to it. - """ - try: - # PYTHON-LDAP - self._conn = ldap.initialize(self._uri) - self._conn.set_option(ldap.OPT_X_TLS_CACERTFILE, self._tls_cacert) - self._conn.sasl_interactive_bind_s('', self._sasl_gssapi) + self._conn.simple_bind_s(self._dn, self._client_secret) except Exception as e: logger.error(f'Unable to bind to LDAP server {e}') + else: + return self._conn def encode(self, val): """ @@ -315,25 +289,28 @@ def add(self, scim_user): Add a new user :param scim_user: user object conforming to the SCIM User Schema + For a RHDS deployment: + dc=ipa,dc=com + cn=accounts + cn=users + uid=oneuser """ + # TODO: implement dynamic list based on _ldap_user_extra_attrs attrs = {} - # TODO: objectclasses should be propagated from keycloak - attrs['objectclass'] = [b'inetOrgPerson', - b'organizationalPerson', - b'person', - b'top'] attrs['cn'] = self.encode(scim_user.obj.username) - attrs['mail'] = self.encode(scim_user.obj.email) - attrs['givenname'] = self.encode(scim_user.obj.first_name) attrs['sn'] = self.encode(scim_user.obj.last_name) + attrs['givenname'] = self.encode(scim_user.obj.first_name) + attrs['mail'] = self.encode(scim_user.obj.email) + attrs['objectClass'] = self.encode(self._user_object_classes) ldif = modlist.addModlist(attrs) - self._ldap_connect() + self._bind() try: - self._conn.add_s("uid={uid},{group},{basedn}".format( + # AD: cn, LDAP: uid + self._conn.add_s("uid={uid},{usersdn},{basedn}".format( uid=scim_user.obj.username, - group=self._group, - basedn=self._base_dn), ldif) + usersdn=self._users_dn, + basedn=self._ldap_search_base), ldif) except ldap.LDAPError as e: desc = e.args[0]['desc'].strip() info = e.args[0].get('info', '').strip() @@ -346,23 +323,19 @@ def modify(self, scim_user): :param scim_user: user object conforming to the SCIM User Schema """ attrs = {} - # TODO: objectclasses should be propagated from keycloak - attrs['objectclass'] = [b'inetOrgPerson', - b'organizationalPerson', - b'person', - b'top'] attrs['cn'] = self.encode(scim_user.obj.username) - attrs['mail'] = self.encode(scim_user.obj.email) - attrs['givenname'] = self.encode(scim_user.obj.first_name) attrs['sn'] = self.encode(scim_user.obj.last_name) + attrs['givenname'] = self.encode(scim_user.obj.first_name) + attrs['mail'] = self.encode(scim_user.obj.email) + attrs['objectClass'] = self.encode(self._user_object_classes) ldif = modlist.addModlist(attrs) - self._ldap_connect() + self._bind() try: - self._conn.modify_s("uid={uid},{group},{basedn}".format( + self._conn.modify_s("uid={uid},{usersdn},{basedn}".format( uid=scim_user.obj.username, - group=self._group, - basedn=self._base_dn), ldif) + usersdn=self._users_dn, + basedn=self._ldap_search_base), ldif) except ldap.LDAPError as e: desc = e.args[0]['desc'].strip() info = e.args[0].get('info', '').strip() @@ -374,12 +347,12 @@ def delete(self, scim_user): :param scim_user: user object conforming to the SCIM User Schema """ - self._ldap_connect() + self._bind() try: - self._conn.delete_s("uid={uid},{group},{basedn}".format( + self._conn.delete_s("uid={uid},{usersdn},{basedn}".format( uid=scim_user.obj.username, - group=self._group, - basedn=self._base_dn)) + usersdn=self._users_dn, + basedn=self._ldap_search_base)) except ldap.LDAPError as e: desc = e.args[0]['desc'].strip() info = e.args[0].get('info', '').strip() @@ -391,15 +364,128 @@ class AD: Initialization of the LDAP AD writable interface """ def __init__(self): - pass + self._conn = None + self._dn = None + self._users_dn = None + self._ldap_uri = None + self._ldap_search_base = None + self._ldap_user_extra_attrs = None + self._user_object_classes = None + # TLS + self._ldap_tls_cacert = None + self._sasl_gssapi = ldap.sasl.sasl({}, 'GSSAPI') + self._client_id = None + self._client_secret = None + # init and connect + self._fetch_domain() + self._conn = self._bind() + + def _fetch_domain(self): + """ + Fetch relevant information from the integration domain + """ + domain = domains.models.Domain.objects.last() + suffix = domain.name.split('.') + + self._dn = domain.client_id + "@" + domain.name + self._ldap_uri = domain.integration_domain_url + self._ldap_user_extra_attrs = domain.user_extra_attrs + self._ldap_search_base = "dc=" + suffix[0] + ", dc=" + suffix[1] + self._ldap_tls_cacert = domain.ldap_tls_cacert + self._client_id = domain.client_id + self._client_secret = domain.client_secret + self._users_dn = domain.users_dn + self._user_object_classes = [ + x.strip() for x in domain.user_object_classes.split(',') + ] + logger.info(f'Domain info: {domain}') + + def _bind(self): + """ + Bind to ldap server + """ + self._fetch_domain() + # TODO enable TLS support + # self._conn = ldap.initialize(self._ldap_uri) + # self._conn.set_option(ldap.OPT_X_TLS_CACERTFILE, self._tls_cacert) + # self._conn.sasl_interactive_bind_s('', self._sasl_gssapi) + self._conn = ldap.initialize(self._ldap_uri) + self._conn.protocol_version = 3 + self._conn.set_option(ldap.OPT_REFERRALS, 0) + try: + self._conn.simple_bind_s(self._dn, self._client_secret) + except Exception as e: + logger.error(f'Unable to bind to LDAP server {e}') + else: + return self._conn + + def encode(self, val): + """ + Encode attribute value to LDAP representation (str/bytes) + """ + # Booleans are both an instance of bool and int, therefore + # test for bool before int otherwise the int clause will be + # entered for a boolean value instead of the boolean clause. + if isinstance(val, bool): + if val: + return b'TRUE' + else: + return b'FALSE' + elif isinstance(val, (unicode, int, Decimal, DN, Principal)): + return str(val).encode('utf-8') + elif isinstance(val, DNSName): + return val.to_text().encode('ascii') + elif isinstance(val, bytes): + return val + elif isinstance(val, list): + return [self.encode(m) for m in val] + elif isinstance(val, tuple): + return tuple(self.encode(m) for m in val) + elif isinstance(val, dict): + # key in dict must be str not bytes + dct = dict((k, self.encode(v)) for k, v in val.items()) + return dct + elif isinstance(val, datetime.datetime): + return val.strftime(LDAP_GENERALIZED_TIME_FORMAT).encode('utf-8') + elif isinstance(val, crypto_x509.Certificate): + return val.public_bytes(x509.Encoding.DER) + elif val is None: + return None + else: + raise TypeError("attempt to pass unsupported type to ldap, " + "value=%s type=%s" % (val, type(val))) def add(self, scim_user): """ Add a new user :param scim_user: user object conforming to the SCIM User Schema + + For an AD deployment: + dc=ad,dc=com + cn=users + cn=oneuser """ - pass + # TODO: implement dynamic list based on _ldap_user_extra_attrs + attrs = {} + attrs['objectclass'] = self.encode(self._user_object_classes) + attrs['cn'] = self.encode(scim_user.obj.username) + attrs['mail'] = self.encode(scim_user.obj.email) + attrs['givenname'] = self.encode(scim_user.obj.first_name) + attrs['sn'] = self.encode(scim_user.obj.last_name) + ldif = modlist.addModlist(attrs) + + self._bind() + try: + # AD: cn, LDAP: uid + self._conn.add_s("cn={uid},{usersdn},{basedn}".format( + uid=scim_user.obj.username, + usersdn=self._users_dn, + basedn=self._ldap_search_base), ldif) + except ldap.LDAPError as e: + desc = e.args[0]['desc'].strip() + info = e.args[0].get('info', '').strip() + logger.error(f'LDAP Error: {desc}: {info}') def modify(self, scim_user): """ @@ -407,7 +493,24 @@ def modify(self, scim_user): :param scim_user: user object conforming to the SCIM User Schema """ - pass + attrs = {} + attrs['objectclass'] = self.encode(self._user_object_classes) + attrs['cn'] = self.encode(scim_user.obj.username) + attrs['mail'] = self.encode(scim_user.obj.email) + attrs['givenname'] = self.encode(scim_user.obj.first_name) + attrs['sn'] = self.encode(scim_user.obj.last_name) + ldif = modlist.addModlist(attrs) + + self._bind() + try: + self._conn.modify_s("cn={uid},{usersdn},{basedn}".format( + uid=scim_user.obj.username, + usersdn=self._users_dn, + basedn=self._ldap_search_base), ldif) + except ldap.LDAPError as e: + desc = e.args[0]['desc'].strip() + info = e.args[0].get('info', '').strip() + logger.error(f'LDAP Error: {desc}: {info}') def delete(self, scim_user): """ @@ -415,7 +518,16 @@ def delete(self, scim_user): :param scim_user: user object conforming to the SCIM User Schema """ - pass + self._bind() + try: + self._conn.delete_s("cn={uid},{usersdn},{basedn}".format( + uid=scim_user.obj.username, + usersdn=self._users_dn, + basedn=self._ldap_search_base)) + except ldap.LDAPError as e: + desc = e.args[0]['desc'].strip() + info = e.args[0].get('info', '').strip() + logger.error(f'LDAP Error: {desc}: {info}') class _IPA(): @@ -424,11 +536,9 @@ class _IPA(): def __init__(self): """ Initialize writable interface - Instantiate the writable interface depending on the current - configuration settings.py: SCIM_SERVICE_PROVIDER['WRITABLE_IFACE'] """ self._apiconn = self._write( - settings.SCIM_SERVICE_PROVIDER['WRITABLE_IFACE'] + domains.models.Domain.objects.last().id_provider ) def _write(self, iface="ipa"): diff --git a/src/ipa-tuura/root/settings.py b/src/ipa-tuura/root/settings.py index 10e64462..2457341c 100644 --- a/src/ipa-tuura/root/settings.py +++ b/src/ipa-tuura/root/settings.py @@ -131,9 +131,8 @@ # ipa-tuura configuration - # We assume that an admin keytab is available -os.environ["KRB5_CLIENT_KTNAME"] = '/root/scim.keytab' +os.environ["KRB5_CLIENT_KTNAME"] = '/var/lib/ipa/ipatuura/service.keytab' AUTH_USER_MODEL = 'ipatuura.User' @@ -155,9 +154,6 @@ 'documentationUri': '', }, ], - # TODO administrative end-point must configure these values: - 'WRITABLE_IFACE': 'ipa', - 'WRITABLE_USER': 'admin', } # admin endpoint so that we can handle permissions and required fields only for authenticated users @@ -166,3 +162,25 @@ #} REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' } + +# enable logging +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'root': { + 'handlers': ['console'], + 'level': 'DEBUG', + }, + 'loggers': { + 'django': { + 'handlers': ['console'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'), + 'propagate': False, + }, + }, +}