From 95c2327a012d560462d46f04a42a92115f18ae73 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Tue, 16 Jan 2018 22:08:53 +0100
Subject: [PATCH] Add command to sync users with LDAP server

Command shall be run every night to keep the users in the database in
sync with the LDAP server.
If a user is not found:
- set its groups to []
- revoke all its tokens
---
 app/commands.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++++
 app/factory.py  | 17 +----------
 app/models.py   |  2 +-
 3 files changed, 77 insertions(+), 17 deletions(-)
 create mode 100644 app/commands.py

diff --git a/app/commands.py b/app/commands.py
new file mode 100644
index 0000000..204a2c8
--- /dev/null
+++ b/app/commands.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+"""
+app.commands
+~~~~~~~~~~~~
+
+This module defines extra flask commands.
+
+:copyright: (c) 2018 European Spallation Source ERIC
+:license: BSD 2-Clause, see LICENSE for more details.
+
+"""
+import ldap3
+import sqlalchemy as sa
+from flask import current_app
+from .extensions import db, ldap_manager
+from .defaults import defaults
+from .models import User
+from . import utils
+
+
+def sync_user(connection, user):
+    """Synchronize the user from the database with information from the LDAP server"""
+    search_attr = current_app.config.get('LDAP_USER_LOGIN_ATTR')
+    object_filter = current_app.config.get('LDAP_USER_OBJECT_FILTER')
+    search_filter = f'(&{object_filter}({search_attr}={user.username}))'
+    connection.search(
+        search_base=ldap_manager.full_user_search_dn,
+        search_filter=search_filter,
+        search_scope=getattr(
+            ldap3, current_app.config.get('LDAP_USER_SEARCH_SCOPE')),
+        attributes=current_app.config.get('LDAP_GET_USER_ATTRIBUTES')
+    )
+    if len(connection.response) == 1:
+        ldap_user = connection.response[0]
+        attributes = ldap_user['attributes']
+        user.display_name = utils.attribute_to_string(attributes['cn'])
+        user.email = utils.attribute_to_string(attributes['mail'])
+        groups = ldap_manager.get_user_groups(dn=ldap_user['dn'], _connection=connection)
+        user.groups = sorted([utils.attribute_to_string(group['cn']) for group in groups])
+        current_app.logger.info(f'{user} updated')
+    else:
+        # Clear user's groups
+        user.groups = []
+        # Revoke all user's tokens
+        for token in user.tokens:
+            db.session.delete(token)
+        current_app.logger.info(f'{user} disabled')
+    return user
+
+
+def register_cli(app):
+    @app.cli.command()
+    def initdb():
+        """Create the database tables and initialize them with default values"""
+        db.engine.execute('CREATE EXTENSION IF NOT EXISTS citext')
+        db.create_all()
+        for instance in defaults:
+            db.session.add(instance)
+            try:
+                db.session.commit()
+            except sa.exc.IntegrityError as e:
+                db.session.rollback()
+                app.logger.debug(f'{instance} already exists')
+
+    @app.cli.command()
+    def syncusers():
+        """Synchronize all users from the database with information the LDAP server"""
+        try:
+            connection = ldap_manager.connection
+        except ldap3.core.exceptions.LDAPException as e:
+            current_app.logger.warning(f'Failed to connect to the LDAP server: {e}')
+            return
+        for user in User.query.all():
+            sync_user(connection, user)
+        db.session.commit()
diff --git a/app/factory.py b/app/factory.py
index 79bc856..6a05d14 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -24,22 +24,7 @@ from .user.views import bp as user
 from .api.user import bp as user_api
 from .api.inventory import bp as inventory_api
 from .api.network import bp as network_api
-from .defaults import defaults
-
-
-def register_cli(app):
-    @app.cli.command()
-    def initdb():
-        """Create the database tables and initialize them with default values"""
-        db.engine.execute('CREATE EXTENSION IF NOT EXISTS citext')
-        db.create_all()
-        for instance in defaults:
-            db.session.add(instance)
-            try:
-                db.session.commit()
-            except sa.exc.IntegrityError as e:
-                db.session.rollback()
-                app.logger.debug(f'{instance} already exists')
+from .commands import register_cli
 
 
 def create_app(config=None):
diff --git a/app/models.py b/app/models.py
index 4d931f1..ed58eed 100644
--- a/app/models.py
+++ b/app/models.py
@@ -89,7 +89,7 @@ def save_user(dn, username, data, memberships):
                     display_name=utils.attribute_to_string(data['cn']),
                     email=utils.attribute_to_string(data['mail']))
     # Always update the user groups to keep them up-to-date
-    user.groups = [utils.attribute_to_string(group['cn']) for group in memberships]
+    user.groups = sorted([utils.attribute_to_string(group['cn']) for group in memberships])
     db.session.add(user)
     db.session.commit()
     return user
-- 
GitLab