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>&nbsp;&nbsp;{{request.user.name}}&nbsp;<span class="fa fa-fw fa-caret-down"></span>
+                        <i class="fa fa-fw fa-user"></i>&nbsp;&nbsp;{{request.user.username}}&nbsp;<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
+