diff --git a/.gitignore b/.gitignore index 1b2b4d16..2d923e99 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ __pycache__/ .ropeproject/ #django local_settings.py - +Pipfile media/ !media/keep /CACHE/ @@ -43,3 +43,4 @@ secret-key # to keep empty dirs !.gitkeep *.orig +.vscode/settings.json diff --git a/Changelog b/Changelog index 09b362fe..45c650be 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,15 @@ +2.8: 2019-12-20 + * ldap_migration: Migrate django users to Ldap + Notes for deployment: + ``` + 1. Git Pull + 2. Ensure the newly dependencies in requirements.txt are installed + 3. Put new values in .env + 4. Run migrations + 5. Restart uwsgi + ``` +2.7.3: 2019-12-18 + * Bugfix: Swiss VAT being wrongly added to non-EU customers 2.7.2: 2019-12-17 * Add vat rates for AD, TK and IS * Improve billing address' string representation diff --git a/INSTALLATION.rst b/INSTALLATION.rst index ee36b3ad..efa299f3 100644 --- a/INSTALLATION.rst +++ b/INSTALLATION.rst @@ -10,13 +10,35 @@ Requirements Install ======= + +.. note:: + lxml that is one of the dependency of dynamicweb couldn't + get build on Python 3.7 so, please use Python 3.5. + + +First install packages from requirements.archlinux.txt or +requirements.debian.txt based on your distribution. + + The quick way: ``pip install -r requirements.txt`` Next find the dump.db file on stagging server. Path for the file is under the base application folder. +or you can create one for yourself by running the following commands on dynamicweb server + +.. code:: sh + + sudo su - postgres + pg_dump app > /tmp/postgres_db.bak + exit + cp /tmp/postgres_db.bak /root/postgres_db.bak + +Now, you can download this using sftp. + + Install the postgresql server and import the database:: - ``psql -d app < dump.db`` + ``psql -d app -U root < dump.db`` **No migration is needed after a clean install, and You are ready to start developing.** @@ -25,9 +47,9 @@ Development Project is separated in master branch and development branch, and feature branches. Master branch is currently used on `Digital Glarus <https://digitalglarus.ungleich.ch/en-us/digitalglarus/>`_ and `Ungleich blog <https://digitalglarus.ungleich.ch/en-us/blog/>`_. -If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch. +If You are starting to create a new feature fork the github `repo <https://github.com/ungleich/dynamicweb>`_ and branch the development branch. -After You have complited the task create a pull request and ask someone to review the code from other developers. +After You have completed the task, create a pull request and ask someone to review the code from other developers. **Cheat sheet for branching and forking**: diff --git a/datacenterlight/views.py b/datacenterlight/views.py index 51466d93..301049aa 100644 --- a/datacenterlight/views.py +++ b/datacenterlight/views.py @@ -625,6 +625,7 @@ class OrderConfirmationView(DetailView, FormView): vm_specs["vat_country"] = user_vat_country vm_specs["discount"] = discount vm_specs["total_price"] = round(price + vat - discount['amount'], 2) + request.session['specs'] = vm_specs context.update({ 'vm': vm_specs, diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py index fc971141..bdbb8f8f 100644 --- a/dynamicweb/settings/base.py +++ b/dynamicweb/settings/base.py @@ -52,7 +52,7 @@ PROJECT_DIR = os.path.abspath( ) # load .env file -dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR)) +dotenv.load_dotenv("{0}/.env".format(PROJECT_DIR)) from multisite import SiteID @@ -244,8 +244,9 @@ DATABASES = { } AUTHENTICATION_BACKENDS = ( + 'utils.backend.MyLDAPBackend', 'guardian.backends.ObjectPermissionBackend', - 'django.contrib.auth.backends.ModelBackend', + ) # Internationalization @@ -721,6 +722,31 @@ X_FRAME_OPTIONS = ('SAMEORIGIN' if X_FRAME_OPTIONS_ALLOW_FROM_URI is None else DEBUG = bool_env('DEBUG') + +# LDAP setup +LDAP_ADMIN_DN = env('LDAP_ADMIN_DN') +LDAP_ADMIN_PASSWORD = env('LDAP_ADMIN_PASSWORD') +AUTH_LDAP_SERVER = env('LDAPSERVER') + +LDAP_CUSTOMER_DN = env('LDAP_CUSTOMER_DN') +LDAP_CUSTOMER_GROUP_ID = int(env('LDAP_CUSTOMER_GROUP_ID')) +LDAP_MAX_UID_FILE_PATH = os.environ.get('LDAP_MAX_UID_FILE_PATH', + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'ldap_max_uid_file') +) +LDAP_DEFAULT_START_UID = int(env('LDAP_DEFAULT_START_UID')) + +# Search union over OUs +AUTH_LDAP_START_TLS = bool(os.environ.get('LDAP_USE_TLS', False)) + +ENTIRE_SEARCH_BASE = env("ENTIRE_SEARCH_BASE") + + +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": "givenName", + "last_name": "sn", + "email": "mail" +} + READ_VM_REALM = env('READ_VM_REALM') AUTH_NAME = env('AUTH_NAME') AUTH_SEED = env('AUTH_SEED') diff --git a/hosting/static/hosting/css/dashboard.css b/hosting/static/hosting/css/dashboard.css index c7bbecd9..0b718178 100644 --- a/hosting/static/hosting/css/dashboard.css +++ b/hosting/static/hosting/css/dashboard.css @@ -23,7 +23,6 @@ .hosting-dashboard .dashboard-container-head { color: #fff; - margin-bottom: 60px; } .hosting-dashboard-item { diff --git a/hosting/static/hosting/css/virtual-machine.css b/hosting/static/hosting/css/virtual-machine.css index 726b0f35..4d490ff7 100644 --- a/hosting/static/hosting/css/virtual-machine.css +++ b/hosting/static/hosting/css/virtual-machine.css @@ -248,6 +248,9 @@ .dashboard-title-thin { font-size: 22px; } + .dashboard-greetings-thin { + font-size: 16px; + } } .btn-vm-invoice { @@ -315,6 +318,11 @@ font-size: 32px; } +.dashboard-greetings-thin { + font-weight: 300; + font-size: 24px; +} + .dashboard-title-thin .un-icon { height: 34px; margin-right: 5px; @@ -411,6 +419,9 @@ .dashboard-title-thin { font-size: 22px; } + .dashboard-greetings-thin { + font-size: 16px; + } .dashboard-title-thin .un-icon { height: 22px; width: 22px; diff --git a/hosting/templates/hosting/dashboard.html b/hosting/templates/hosting/dashboard.html index 35ee9b6e..bda6eb11 100644 --- a/hosting/templates/hosting/dashboard.html +++ b/hosting/templates/hosting/dashboard.html @@ -7,6 +7,9 @@ <div class="dashboard-container-head"> <h1 class="dashboard-title-thin">{% trans "My Dashboard" %}</h1> </div> + <div style="color:#fff; font-size: 18px; font-weight:300; padding: 0 8px; margin-top: 30px; margin-bottom: 30px;"> + {% trans "Welcome" %} {{request.user.name}} + </div> <div class="hosting-dashboard-content"> <a href="{% url 'hosting:create_virtual_machine' %}" class="hosting-dashboard-item"> <h2>{% trans "Create VM" %}</h2> diff --git a/hosting/templates/hosting/includes/_navbar_user.html b/hosting/templates/hosting/includes/_navbar_user.html index 7362f447..bd77eb5c 100644 --- a/hosting/templates/hosting/includes/_navbar_user.html +++ b/hosting/templates/hosting/includes/_navbar_user.html @@ -26,7 +26,7 @@ </li> <li class="dropdown highlights-dropdown"> <a class="dropdown-toggle" role="button" data-toggle="dropdown" href="#"> - <i class="fa fa-fw fa-user"></i> {{request.user.name}} <span class="fa fa-fw fa-caret-down"></span> + <i class="fa fa-fw fa-user"></i> {{request.user.username}} <span class="fa fa-fw fa-caret-down"></span> </a> <ul id="g-account-menu" class="dropdown-menu" role="menu"> <li><a href="{% url 'hosting:logout' %}">{% trans "Logout"%}</a></li> diff --git a/hosting/templates/hosting/settings.html b/hosting/templates/hosting/settings.html index 5cdd830c..3ca0eee6 100644 --- a/hosting/templates/hosting/settings.html +++ b/hosting/templates/hosting/settings.html @@ -15,6 +15,10 @@ <div class="settings-container"> <div class="row"> <div class="col-sm-5 col-md-6 billing dcl-billing"> + <h3><b>{%trans "My Username"%}</b></h3> + <hr class="top-hr"> + <p>{{request.user.username}}</p> + <br> <h3>{%trans "Billing Address" %}</h3> <hr> <form role="form" id="billing-form" method="post" action="" novalidate> diff --git a/hosting/views.py b/hosting/views.py index 0c7077e5..39b43ad8 100644 --- a/hosting/views.py +++ b/hosting/views.py @@ -57,6 +57,8 @@ from utils.hosting_utils import ( from utils.mailer import BaseEmail from utils.stripe_utils import StripeUtils from utils.tasks import send_plain_email_task +from utils.ldap_manager import LdapManager + from utils.views import ( PasswordResetViewMixin, PasswordResetConfirmViewMixin, LoginViewMixin, ResendActivationLinkViewMixin @@ -394,21 +396,30 @@ class PasswordResetConfirmView(HostingContextMixin, if user is not None and default_token_generator.check_token(user, token): if form.is_valid(): + ldap_manager = LdapManager() new_password = form.cleaned_data['new_password2'] - user.set_password(new_password) - user.save() - messages.success(request, _('Password has been reset.')) - # Change opennebula password - opennebula_client.change_user_password(user.password) + # Make sure the user have an ldap account already + user.create_ldap_account(new_password) - return self.form_valid(form) - else: - messages.error( - request, _('Password reset has not been successful.')) - form.add_error(None, - _('Password reset has not been successful.')) - return self.form_invalid(form) + # We are changing password in ldap before changing in database because + # ldap have more chances of failure than local database + if ldap_manager.change_password(user.username, new_password): + user.set_password(new_password) + user.save() + + messages.success(request, _('Password has been reset.')) + + # Change opennebula password + opennebula_client.change_user_password(user.password) + + return self.form_valid(form) + + messages.error( + request, _('Password reset has not been successful.')) + form.add_error(None, + _('Password reset has not been successful.')) + return self.form_invalid(form) else: error_msg = _('The reset password link is no longer valid.') diff --git a/membership/migrations/0011_auto_20191218_1050.py b/membership/migrations/0011_auto_20191218_1050.py new file mode 100644 index 00000000..58ad06ba --- /dev/null +++ b/membership/migrations/0011_auto_20191218_1050.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2019-12-18 10:50 +from __future__ import unicode_literals + +from django.db import migrations, models +import membership.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('membership', '0010_customuser_import_stripe_bill_remark'), + ] + + operations = [ + migrations.AddField( + model_name='customuser', + name='in_ldap', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='customuser', + name='username', + field=models.CharField(max_length=60, null=True, unique=True), + ), + migrations.AlterField( + model_name='customuser', + name='name', + field=models.CharField(max_length=50, validators=[membership.models.validate_name]), + ), + ] diff --git a/membership/migrations/0011_customuser_vat_number.py b/membership/migrations/0011_customuser_vat_number.py deleted file mode 100644 index f814d84f..00000000 --- a/membership/migrations/0011_customuser_vat_number.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.4 on 2019-12-17 16:37 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('membership', '0010_customuser_import_stripe_bill_remark'), - ] - - operations = [ - migrations.AddField( - model_name='customuser', - name='vat_number', - field=models.CharField(default='', max_length=100), - ), - ] diff --git a/membership/models.py b/membership/models.py index 28b5951d..34ef73cc 100644 --- a/membership/models.py +++ b/membership/models.py @@ -1,5 +1,8 @@ -from datetime import datetime +import logging +import random +import unicodedata +from datetime import datetime from django.conf import settings from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \ @@ -7,13 +10,17 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \ from django.contrib.sites.models import Site from django.core.urlresolvers import reverse from django.core.validators import RegexValidator -from django.db import models +from django.db import models, IntegrityError from django.utils.crypto import get_random_string from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ValidationError from utils.mailer import BaseEmail from utils.mailer import DigitalGlarusRegistrationMailer from utils.stripe_utils import StripeUtils +from utils.ldap_manager import LdapManager + +logger = logging.getLogger(__name__) REGISTRATION_MESSAGE = {'subject': "Validation mail", 'message': 'Please validate Your account under this link ' @@ -42,6 +49,7 @@ class MyUserManager(BaseUserManager): user.is_admin = False user.set_password(password) user.save(using=self._db) + user.create_ldap_account(password) return user def create_superuser(self, email, name, password): @@ -63,14 +71,56 @@ def get_validation_slug(): return make_password(None) +def get_first_and_last_name(full_name): + first_name, *last_name = full_name.split(" ") + last_name = " ".join(last_name) + return first_name, last_name + + +def assign_username(user): + if not user.username: + ldap_manager = LdapManager() + + # Try to come up with a username + first_name, last_name = get_first_and_last_name(user.name) + user.username = unicodedata.normalize('NFKD', first_name + last_name) + user.username = "".join([char for char in user.username if char.isalnum()]).lower() + exist = True + while exist: + # Check if it exists + exist, entries = ldap_manager.check_user_exists(user.username) + if exist: + # If username exists in ldap, come up with a new user name and check it again + user.username = user.username + str(random.randint(0, 2 ** 10)) + else: + # If username does not exists in ldap, try to save it in database + try: + user.save() + except IntegrityError: + # If username exists in database then come up with a new username + user.username = user.username + str(random.randint(0, 2 ** 10)) + exist = True + + +def validate_name(value): + valid_chars = [char for char in value if (char.isalpha() or char == "-" or char == " ")] + if len(valid_chars) < len(value): + raise ValidationError( + _('%(value)s is not a valid name. A valid name can only include letters, spaces or -'), + params={'value': value}, + ) + + class CustomUser(AbstractBaseUser, PermissionsMixin): VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated')) site = models.ForeignKey(Site, default=1) - name = models.CharField(max_length=50) + name = models.CharField(max_length=50, validators=[validate_name]) email = models.EmailField(unique=True) + username = models.CharField(max_length=60, unique=True, null=True) vat_number = models.CharField(max_length=100, default="") validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0) + in_ldap = models.BooleanField(default=False) # By default, we initialize the validation_slug with appropriate value # This is required for User(page) admin validation_slug = models.CharField( @@ -165,6 +215,29 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): # The user is identified by their email address return self.email + def create_ldap_account(self, password): + # create ldap account for user if it does not exists already. + if self.in_ldap: + return + + assign_username(self) + ldap_manager = LdapManager() + try: + user_exists_in_ldap, entries = ldap_manager.check_user_exists(self.username) + except Exception: + logger.exception("Exception occur while searching for user in LDAP") + else: + if not user_exists_in_ldap: + # IF no ldap account + first_name, last_name = get_first_and_last_name(self.name) + if not last_name: + last_name = first_name + ldap_manager.create_user(self.username, password=password, + firstname=first_name, lastname=last_name, + email=self.email) + self.in_ldap = True + self.save() + def __str__(self): # __unicode__ on Python 2 return self.email diff --git a/opennebula_api/models.py b/opennebula_api/models.py index f8ef6481..19e3e4f7 100644 --- a/opennebula_api/models.py +++ b/opennebula_api/models.py @@ -485,9 +485,15 @@ class OpenNebulaManager(): ) def change_user_password(self, passwd_hash): + if type(self.opennebula_user) == int: + logger.debug("opennebula_user is int and has value = %s" % + self.opennebula_user) + else: + logger.debug("opennebula_user is object and corresponding id is %s" + % self.opennebula_user.id) self.oneadmin_client.call( oca.User.METHODS['passwd'], - self.opennebula_user.id, + self.opennebula_user if type(self.opennebula_user) == int else self.opennebula_user.id, passwd_hash ) diff --git a/requirements.archlinux.txt b/requirements.archlinux.txt index b4cab6e4..15184f0d 100644 --- a/requirements.archlinux.txt +++ b/requirements.archlinux.txt @@ -1 +1,2 @@ +base-devel libmemcached diff --git a/requirements.txt b/requirements.txt index c60c83e9..5fb2ec67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ django-classy-tags==0.7.2 django-cms==3.2.5 django-compressor==2.0 django-debug-toolbar==1.4 -django-dotenv==1.4.1 +python-dotenv==0.10.3 django-extensions==1.6.7 django-filer==1.2.0 django-filter==0.13.0 @@ -63,6 +63,7 @@ djangocms-text-ckeditor==2.9.3 djangocms-video==1.0.0 easy-thumbnails==2.3 html5lib==0.9999999 +ldap3==2.6.1 lxml==3.6.0 model-mommy==1.2.6 phonenumbers==7.4.0 diff --git a/utils/backend.py b/utils/backend.py new file mode 100644 index 00000000..2b5c86e5 --- /dev/null +++ b/utils/backend.py @@ -0,0 +1,13 @@ + +import logging + +from django.contrib.auth.backends import ModelBackend +logger = logging.getLogger(__name__) + + +class MyLDAPBackend(ModelBackend): + def authenticate(self, username=None, password=None, **kwargs): + user = super().authenticate(username, password, **kwargs) + if user: + user.create_ldap_account(password) + return user diff --git a/utils/ldap_manager.py b/utils/ldap_manager.py new file mode 100644 index 00000000..fd039ad5 --- /dev/null +++ b/utils/ldap_manager.py @@ -0,0 +1,281 @@ +import base64 +import hashlib +import random +import ldap3 +import logging + +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class LdapManager: + __instance = None + + def __new__(cls): + if LdapManager.__instance is None: + LdapManager.__instance = object.__new__(cls) + return LdapManager.__instance + + def __init__(self): + """ + Initialize the LDAP subsystem. + """ + self.rng = random.SystemRandom() + self.server = ldap3.Server(settings.AUTH_LDAP_SERVER) + + def get_admin_conn(self): + """ + Return a bound :class:`ldap3.Connection` instance which has write + permissions on the dn in which the user accounts reside. + """ + conn = self.get_conn(user=settings.LDAP_ADMIN_DN, + password=settings.LDAP_ADMIN_PASSWORD, + raise_exceptions=True) + conn.bind() + return conn + + def get_conn(self, **kwargs): + """ + Return an unbound :class:`ldap3.Connection` which talks to the configured + LDAP server. + + The *kwargs* are passed to the constructor of :class:`ldap3.Connection` and + can be used to set *user*, *password* and other useful arguments. + """ + return ldap3.Connection(self.server, **kwargs) + + def _ssha_password(self, password): + """ + Apply the SSHA password hashing scheme to the given *password*. + *password* must be a :class:`bytes` object, containing the utf-8 + encoded password. + + Return a :class:`bytes` object containing ``ascii``-compatible data + which can be used as LDAP value, e.g. after armoring it once more using + base64 or decoding it to unicode from ``ascii``. + """ + SALT_BYTES = 15 + + sha1 = hashlib.sha1() + salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, "little") + sha1.update(password) + sha1.update(salt) + + digest = sha1.digest() + passwd = b"{SSHA}" + base64.b64encode(digest + salt) + return passwd + + def create_user(self, user, password, firstname, lastname, email): + conn = self.get_admin_conn() + uidNumber = self._get_max_uid() + 1 + logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber)) + user_exists = True + while user_exists: + user_exists, _ = self.check_user_exists( + "", + '(&(objectClass=inetOrgPerson)(objectClass=posixAccount)' + '(objectClass=top)(uidNumber={uidNumber}))'.format( + uidNumber=uidNumber + ) + ) + if user_exists: + logger.debug( + "{uid} exists. Trying next.".format(uid=uidNumber) + ) + uidNumber += 1 + logger.debug("{uid} does not exist. Using it".format(uid=uidNumber)) + self._set_max_uid(uidNumber) + try: + uid = user.encode("utf-8") + conn.add("uid={uid},{customer_dn}".format( + uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN + ), + ["inetOrgPerson", "posixAccount", "ldapPublickey"], + { + "uid": [uid], + "sn": [lastname.encode("utf-8")], + "givenName": [firstname.encode("utf-8")], + "cn": [uid], + "displayName": ["{} {}".format(firstname, lastname).encode("utf-8")], + "uidNumber": [str(uidNumber)], + "gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)], + "loginShell": ["/bin/bash"], + "homeDirectory": ["/home/{}".format(user).encode("utf-8")], + "mail": email.encode("utf-8"), + "userPassword": [self._ssha_password( + password.encode("utf-8") + )] + } + ) + logger.debug('Created user %s %s' % (user.encode('utf-8'), + uidNumber)) + except Exception as ex: + logger.debug('Could not create user %s' % user.encode('utf-8')) + logger.error("Exception: " + str(ex)) + raise Exception(ex) + finally: + conn.unbind() + + def change_password(self, uid, new_password): + """ + Changes the password of the user identified by user_dn + + :param uid: str The uid that identifies the user + :param new_password: str The new password string + :return: True if password was changed successfully False otherwise + """ + conn = self.get_admin_conn() + + # Make sure the user exists first to change his/her details + user_exists, entries = self.check_user_exists( + uid=uid, + search_base=settings.ENTIRE_SEARCH_BASE + ) + return_val = False + if user_exists: + try: + return_val = conn.modify( + entries[0].entry_dn, + { + "userpassword": ( + ldap3.MODIFY_REPLACE, + [self._ssha_password(new_password.encode("utf-8"))] + ) + } + ) + except Exception as ex: + logger.error("Exception: " + str(ex)) + else: + logger.error("User {} not found".format(uid)) + + conn.unbind() + return return_val + + + def change_user_details(self, uid, details): + """ + Updates the user details as per given values in kwargs of the user + identified by user_dn. + + Assumes that all attributes passed in kwargs are valid. + + :param uid: str The uid that identifies the user + :param details: dict A dictionary containing the new values + :return: True if user details were updated successfully False otherwise + """ + conn = self.get_admin_conn() + + # Make sure the user exists first to change his/her details + user_exists, entries = self.check_user_exists( + uid=uid, + search_base=settings.ENTIRE_SEARCH_BASE + ) + + return_val = False + if user_exists: + details_dict = {k: (ldap3.MODIFY_REPLACE, [v.encode("utf-8")]) for + k, v in details.items()} + try: + return_val = conn.modify(entries[0].entry_dn, details_dict) + msg = "success" + except Exception as ex: + msg = str(ex) + logger.error("Exception: " + msg) + finally: + conn.unbind() + else: + msg = "User {} not found".format(uid) + logger.error(msg) + conn.unbind() + return return_val, msg + + def check_user_exists(self, uid, search_filter="", attributes=None, + search_base=settings.LDAP_CUSTOMER_DN, search_attr="uid"): + """ + Check if the user with the given uid exists in the customer group. + + :param uid: str representing the user + :param search_filter: str representing the filter condition to find + users. If its empty, the search finds the user with + the given uid. + :param attributes: list A list of str representing all the attributes + to be obtained in the result entries + :param search_base: str + :return: tuple (bool, [ldap3.abstract.entry.Entry ..]) + A bool indicating if the user exists + A list of all entries obtained in the search + """ + conn = self.get_admin_conn() + entries = [] + try: + result = conn.search( + search_base=search_base, + search_filter=search_filter if len(search_filter) > 0 else + '(uid={uid})'.format(uid=uid), + attributes=attributes + ) + entries = conn.entries + finally: + conn.unbind() + return result, entries + + def delete_user(self, uid): + """ + Deletes the user with the given uid from ldap + + :param uid: str representing the user + :return: True if the delete was successful False otherwise + """ + conn = self.get_admin_conn() + try: + return_val = conn.delete( + ("uid={uid}," + settings.LDAP_CUSTOMER_DN).format(uid=uid), + ) + msg = "success" + except Exception as ex: + msg = str(ex) + logger.error("Exception: " + msg) + return_val = False + finally: + conn.unbind() + return return_val, msg + + def _set_max_uid(self, max_uid): + """ + a utility function to save max_uid value to a file + + :param max_uid: an integer representing the max uid + :return: + """ + with open(settings.LDAP_MAX_UID_FILE_PATH, 'w+') as handler: + handler.write(str(max_uid)) + + def _get_max_uid(self): + """ + A utility function to read the max uid value that was previously set + + :return: An integer representing the max uid value that was previously + set + """ + try: + with open(settings.LDAP_MAX_UID_FILE_PATH, 'r+') as handler: + try: + return_value = int(handler.read()) + except ValueError as ve: + logger.error( + "Error reading int value from {}. {}" + "Returning default value {} instead".format( + settings.LDAP_MAX_UID_PATH, + str(ve), + settings.LDAP_DEFAULT_START_UID + ) + ) + return_value = settings.LDAP_DEFAULT_START_UID + return return_value + except FileNotFoundError as fnfe: + logger.error("File not found : " + str(fnfe)) + return_value = settings.LDAP_DEFAULT_START_UID + logger.error("So, returning UID={}".format(return_value)) + return return_value +