From 17cb13364b5c366d2e9f2d1e2bf2e8d2081857bd Mon Sep 17 00:00:00 2001
From: Viharm <viharm@users.noreply.github.com>
Date: Tue, 15 Dec 2020 23:21:45 +0000
Subject: [PATCH] Feature ldapauth (#507)

* Composer dependency added

* Updated code for language and config controller

* Added LDAP auth code

* Added blank discord value for new user during install
---
 composer.json                                 |  3 +-
 src/lang/en_US.lang.php                       | 49 +++++++++++++
 .../Config/Controller/ConfigController.php    | 69 ++++++++++++++++++-
 .../Install/Controller/InstallController.php  |  1 +
 src/psm/Service/User.php                      | 47 ++++++++++---
 .../default/module/config/config.tpl.html     | 42 +++++++++++
 6 files changed, 201 insertions(+), 10 deletions(-)

diff --git a/composer.json b/composer.json
index 0498c033..31228c8a 100644
--- a/composer.json
+++ b/composer.json
@@ -18,7 +18,8 @@
 		"php-pushover/php-pushover": "dev-master",
 		"paragonie/random_compat": "^2.0",
 		"twig/twig": "~1.35",
-		"jaxl/jaxl": "^3.1"
+		"jaxl/jaxl": "^3.1",
+		"viharm/psm-ldap-auth": "^1.1"
 	},
 	"autoload": {
 		"files": [
diff --git a/src/lang/en_US.lang.php b/src/lang/en_US.lang.php
index ff6895ac..0d64ce17 100644
--- a/src/lang/en_US.lang.php
+++ b/src/lang/en_US.lang.php
@@ -342,6 +342,53 @@ $sm_lang = array(
         'jabber_password' => 'Password',
         'jabber_password_description' => 'Fill only to set or change.',
         'jabber_check' => 'Check your Jabber account if message was received.',
+        'dirauth_status' => 'Authenticate with directory service',
+        'authdir_host_locn' => 'Directory host',
+        'authdir_host_port' => 'Directory port',
+        'authdir_type' => 'Service type',
+        'authdir_type_description' => 'OpenLDAP: Directory is an OpenLDAP service.<br>AD
+        DS: Directory is an Active Directory Domain Service.<br>AD
+        LDS: Directory is an Active Directory Lightweight Directory
+        Service.',
+        'authdir_type_openldap' => 'OpenLDAP',
+        'authdir_type_adds' => 'AD DS',
+        'authdir_type_adlds' => 'AD LDS',
+        'authdir_userdomain' => 'Active Directory domain',
+        'authdir_userdomain_description' => 'User domain for Active Directory. This is typically the NETBIOS domain
+        for AD DS and the DNS domain for AD LDS. Not used for OpenLDAP
+        directories.',
+        'authdir_ldapver' => 'LDAP protocol version',
+        'authdir_ldapver_description' => 'Version of the LDAP specification. This is typically Version 3 (default).
+        Version 2 was deprecated in 2003 (RFC3494).',
+        'authdir_ldapfollowref' => 'Follow referrals',
+        'authdir_ldapfollowref_description' => 'Follow referrals if the specified server refers to another server for
+        the required information. Leave unchecked if you are unaware of this
+        functionality.',
+        'authdir_basedn' => 'Base DN*',
+        'authdir_basedn_description' => 'Base distinguished name (DN) of the directory service. E.g.,
+        dc=domain,dc=tld. This is a required field.',
+        'authdir_usernameattrib' => 'Username attribute',
+        'authdir_usernameattrib_description' => 'Attribute used by the directory service to refer to the username of
+        the user.',
+        'authdir_groupnameattrib' => 'Group name attribute',
+        'authdir_groupnameattrib_description' => 'Attribute used by the directory service to refer to the group name
+        of a group. This is used to check for group membership.',
+        'authdir_groupmemattrib' => 'Group member attribute',
+        'authdir_groupmemattrib_description' => 'Attribute used by the directory service to refer to the group(s) of
+        which the user is a member. This is used to check for group
+        membership.',
+        'authdir_usercontainerrdn' => 'User container RDN',
+        'authdir_usercontainerrdn_description' => 'Relative distinguished name of the users container in the
+        directory. E.g., ou=Users',
+        'authdir_groupcontainerrdn' => 'Group container RDN',
+        'authdir_groupcontainerrdn_description' => 'Relative distinguished name of the groups container in the
+        directory. E.g., ou=Groups',
+        'authdir_groupname' => 'Authorised directory group',
+        'authdir_groupname_description' => 'Directory group authorised to access application. Directory users not
+        members of this group will not be authenticated (currently not available
+        for AD).',
+        'authdir_defaultrole' => 'Default role',
+        'authdir_defaultrole_description' => 'Default role to be assigned to users logging in for the first time.',
         'alert_type' => 'Select when you\'d like to be notified.',
         'alert_type_description' => '<b>Status change:</b> You will receive a notification when a server has a change in status. So from online -> offline or offline -> online.<br><br><b>Offline:</b> You will receive a notification when a server goes offline for the *FIRST TIME ONLY*. For example, your cronjob is every 15 minutes and your server goes down at 1 am and stays down till 6 am. You will get 1 notification at 1 am and that\'s it.<br><br><b>Always:</b> You will receive a notification every time the script runs and a site is down, even if the site has been offline for hours.',
         'alert_type_status' => 'Status change',
@@ -368,6 +415,7 @@ $sm_lang = array(
         'tab_webhook' => 'Webhook',
         'tab_telegram' => 'Telegram',
         'tab_jabber' => 'Jabber',
+        'tab_auth' => 'Authentication',
         'settings_email' => 'Email settings',
         'settings_sms' => 'Text message settings',
         'settings_discord' => 'Discord settings',
@@ -378,6 +426,7 @@ $sm_lang = array(
         'settings_notification' => 'Notification settings',
         'settings_log' => 'Log settings',
         'settings_proxy' => 'Proxy settings',
+        'settings_dirauth' => 'LDAP settings',
         'auto_refresh' => 'Auto-refresh',
         'auto_refresh_description' => 'Auto-refresh servers page.<br><span class="small">Time in seconds, if 0 the page won\'t refresh.</span>',
         'test' => 'Test',
diff --git a/src/psm/Module/Config/Controller/ConfigController.php b/src/psm/Module/Config/Controller/ConfigController.php
index df524c4d..c93ca375 100644
--- a/src/psm/Module/Config/Controller/ConfigController.php
+++ b/src/psm/Module/Config/Controller/ConfigController.php
@@ -58,6 +58,8 @@ class ConfigController extends AbstractController
         'log_jabber',
         'show_update',
         'combine_notifications',
+        'dirauth_status',
+        'authdir_ldapfollowref',
     );
 
     /**
@@ -85,7 +87,18 @@ class ConfigController extends AbstractController
         'jabber_username',
         'jabber_domain',
         'user_agent',
-        'site_title'
+        'site_title',
+        'authdir_host_locn',
+        'authdir_host_port',
+        'authdir_userdomain',
+        'authdir_ldapver',
+        'authdir_basedn',
+        'authdir_usernameattrib',
+        'authdir_groupnameattrib',
+        'authdir_groupmemattrib',
+        'authdir_usercontainerrdn',
+        'authdir_groupcontainerrdn',
+        'authdir_groupname',
     );
 
     /**
@@ -162,6 +175,20 @@ class ConfigController extends AbstractController
             );
         }
 
+        foreach (array("20", "10") as $authdir_defaultrole) {
+            $tpl_data['authdir_defaultroles'][] = array(
+                'value' => $authdir_defaultrole,
+                'label' => psm_get_lang('users', 'level_' . $authdir_defaultrole),
+            );
+        }
+
+        foreach (array("openldap", "adds", "adlds") as $authdir_type) {
+            $tpl_data['authdir_type'][] = array(
+                'value' => $authdir_type,
+                'label' => psm_get_lang('config', 'authdir_type_' . $authdir_type),
+            );
+        }
+
         $tpl_data['email_smtp_security'] = array(
             array(
                 'value' => '',
@@ -181,6 +208,10 @@ class ConfigController extends AbstractController
             $config['sms_gateway'] : current($sms_gateways);
         $tpl_data['alert_type_selected'] = isset($config['alert_type']) ?
             $config['alert_type'] : '';
+        $tpl_data['authdir_type_selected'] =  isset($config['authdir_type']) ?
+            $config['authdir_type'] : '';
+        $tpl_data['authdir_defaultrole_selected'] =  isset($config['authdir_defaultrole']) ?
+            $config['authdir_defaultrole'] : '20';
         $tpl_data['email_smtp_security_selected'] =  isset($config['email_smtp_security']) ?
             $config['email_smtp_security'] : '';
         $tpl_data['auto_refresh_servers'] = isset($config['auto_refresh_servers']) ?
@@ -244,6 +275,8 @@ class ConfigController extends AbstractController
                 'site_title' => $_POST['site_title'],
                 'sms_gateway' => $_POST['sms_gateway'],
                 'alert_type' => $_POST['alert_type'],
+                'authdir_defaultrole' => $_POST['authdir_defaultrole'],
+                'authdir_type' => $_POST['authdir_type'],
                 'email_smtp_security' =>
                     in_array($_POST['email_smtp_security'], array('', 'ssl', 'tls'))
                     ? $_POST['email_smtp_security']
@@ -296,6 +329,8 @@ class ConfigController extends AbstractController
 
             if (isset($_POST['general_submit'])) {
                 $this->default_tab = 'general';
+            } elseif (isset($_POST['auth_submit'])) {
+                $this->default_tab = 'auth';
             } elseif (isset($_POST['email_submit']) || !empty($_POST['test_email'])) {
                 $this->default_tab = 'email';
             } elseif (isset($_POST['sms_submit']) || !empty($_POST['test_sms'])) {
@@ -546,6 +581,7 @@ class ConfigController extends AbstractController
             'label_tab_webhook' => psm_get_lang('config', 'tab_webhook'),
             'label_tab_telegram' => psm_get_lang('config', 'tab_telegram'),
             'label_tab_jabber' => psm_get_lang('config', 'tab_jabber'),
+            'label_tab_auth' => psm_get_lang('config', 'tab_auth'),
             'label_settings_email' => psm_get_lang('config', 'settings_email'),
             'label_settings_sms' => psm_get_lang('config', 'settings_sms'),
             'label_settings_discord' => psm_get_lang('config', 'settings_discord'),
@@ -553,6 +589,7 @@ class ConfigController extends AbstractController
             'label_settings_pushover' => psm_get_lang('config', 'settings_pushover'),
             'label_settings_telegram' => psm_get_lang('config', 'settings_telegram'),
             'label_settings_jabber' => psm_get_lang('config', 'settings_jabber'),
+            'label_settings_dirauth' => psm_get_lang('config', 'settings_dirauth'),
             'label_settings_notification' => psm_get_lang('config', 'settings_notification'),
             'label_settings_log' => psm_get_lang('config', 'settings_log'),
             'label_settings_proxy' => psm_get_lang('config', 'settings_proxy'),
@@ -613,6 +650,36 @@ class ConfigController extends AbstractController
             'label_jabber_domain_description' => psm_get_lang('config', 'jabber_domain_description'),
             'label_jabber_password' => psm_get_lang('config', 'jabber_password'),
             'label_jabber_password_description' => psm_get_lang('config', 'jabber_password_description'),
+            'label_dirauth_status' => psm_get_lang('config', 'dirauth_status'),
+            'label_authdir_host_locn' => psm_get_lang('config', 'authdir_host_locn'),
+            'label_authdir_host_port' => psm_get_lang('config', 'authdir_host_port'),
+            'label_authdir_type' => psm_get_lang('config', 'authdir_type'),
+            'label_authdir_type_description' => psm_get_lang('config', 'authdir_type_description'),
+            'label_authdir_userdomain' => psm_get_lang('config', 'authdir_userdomain'),
+            'label_authdir_userdomain_description' => psm_get_lang('config', 'authdir_userdomain_description'),
+            'label_authdir_ldapver' => psm_get_lang('config', 'authdir_ldapver'),
+            'label_authdir_ldapver_description' => psm_get_lang('config', 'authdir_ldapver_description'),
+            'label_authdir_ldapfollowref' => psm_get_lang('config', 'authdir_ldapfollowref'),
+            'label_authdir_ldapfollowref_description' => psm_get_lang('config', 'authdir_ldapfollowref_description'),
+            'label_authdir_basedn' => psm_get_lang('config', 'authdir_basedn'),
+            'label_authdir_basedn_description' => psm_get_lang('config', 'authdir_basedn_description'),
+            'label_authdir_usernameattrib' => psm_get_lang('config', 'authdir_usernameattrib'),
+            'label_authdir_usernameattrib_description' => psm_get_lang('config', 'authdir_usernameattrib_description'),
+            'label_authdir_groupnameattrib' => psm_get_lang('config', 'authdir_groupnameattrib'),
+            'label_authdir_groupnameattrib_description' =>
+                psm_get_lang('config', 'authdir_groupnameattrib_description'),
+            'label_authdir_groupmemattrib' => psm_get_lang('config', 'authdir_groupmemattrib'),
+            'label_authdir_groupmemattrib_description' => psm_get_lang('config', 'authdir_groupmemattrib_description'),
+            'label_authdir_usercontainerrdn' => psm_get_lang('config', 'authdir_usercontainerrdn'),
+            'label_authdir_usercontainerrdn_description' =>
+                psm_get_lang('config', 'authdir_usercontainerrdn_description'),
+            'label_authdir_groupcontainerrdn' => psm_get_lang('config', 'authdir_groupcontainerrdn'),
+            'label_authdir_groupcontainerrdn_description' =>
+                psm_get_lang('config', 'authdir_groupcontainerrdn_description'),
+            'label_authdir_groupname' => psm_get_lang('config', 'authdir_groupname'),
+            'label_authdir_groupname_description' => psm_get_lang('config', 'authdir_groupname_description'),
+            'label_authdir_defaultrole' => psm_get_lang('config', 'authdir_defaultrole'),
+            'label_authdir_defaultrole_description' => psm_get_lang('config', 'authdir_defaultrole_description'),
             'label_alert_type' => psm_get_lang('config', 'alert_type'),
             'label_alert_type_description' => psm_get_lang('config', 'alert_type_description'),
             'label_combine_notifications' => psm_get_lang('config', 'combine_notifications'),
diff --git a/src/psm/Module/Install/Controller/InstallController.php b/src/psm/Module/Install/Controller/InstallController.php
index b1b9bc0b..7afd97f1 100644
--- a/src/psm/Module/Install/Controller/InstallController.php
+++ b/src/psm/Module/Install/Controller/InstallController.php
@@ -306,6 +306,7 @@ class InstallController extends AbstractController
             'webhook_url' => '',
             'webhook_json' => '',
             'telegram_id' => '',
+            'discord' => '',
             'jabber' => ''
         );
 
diff --git a/src/psm/Service/User.php b/src/psm/Service/User.php
index fce6213b..bc01db2a 100644
--- a/src/psm/Service/User.php
+++ b/src/psm/Service/User.php
@@ -230,20 +230,51 @@ class User
     {
         $user_name = trim($user_name);
         $user_password = trim($user_password);
+        $ldapauthstatus = false;
 
         if (empty($user_name) && empty($user_password)) {
             return false;
         }
+
+        $dirauthconfig = psm_get_conf('dirauth_status');
+        
+        // LDAP auth enabled
+        if ($dirauthconfig === '1') {
+            $ldaplibpath = realpath(
+                PSM_PATH_SRC . '..' . DIRECTORY_SEPARATOR .
+                'vendor' . DIRECTORY_SEPARATOR .
+                'viharm' . DIRECTORY_SEPARATOR .
+                'psm-ldap-auth' . DIRECTORY_SEPARATOR .
+                'psmldapauth.php'
+            );
+            // If the library is found
+            if ($ldaplibpath) {
+                // Delegate the authentication to the PsmLDAPauth module.
+                // If LDAP auth fails or if library not found, fall back to native auth
+                include_once($ldaplibpath);
+                $ldapauthstatus = psmldapauth($user_name, $user_password, $GLOBALS['sm_config'], $this->db_connection);
+            }
+        }
+
         $user = $this->getUserByUsername($user_name);
 
-        // using PHP 5.5's password_verify() function to check if the provided passwords
-        // fits to the hash of that user's password
-        if (!isset($user->user_id)) {
-            password_verify($user_password, 'dummy_call_against_timing');
-            return false;
-        } elseif (!password_verify($user_password, $user->password)) {
-            return false;
-        }
+        // Authenticated
+        if ($ldapauthstatus === true) {
+          // Remove password to prevent it from being saved in the DB.
+          // Otherwise, user may still be authenticated if LDAP is disabled later.
+          $user_password = null;
+          @fn_Debug('Authenticated', $user);
+        } else {
+
+          // using PHP 5.5's password_verify() function to check if the provided passwords
+          // fits to the hash of that user's password
+          if (!isset($user->user_id)) {
+              password_verify($user_password, 'dummy_call_against_timing');
+              return false;
+          } elseif (!password_verify($user_password, $user->password)) {
+              return false;
+          }
+        } // not authenticated
 
         $this->setUserLoggedIn($user->user_id, true);
 
diff --git a/src/templates/default/module/config/config.tpl.html b/src/templates/default/module/config/config.tpl.html
index ffb36a54..db5ab029 100644
--- a/src/templates/default/module/config/config.tpl.html
+++ b/src/templates/default/module/config/config.tpl.html
@@ -7,6 +7,11 @@
                 role="tab" aria-controls="config-general" aria-selected="{% if general_active %}true{% else %}false{% endif %}">{{
                 label_general }}</a>
         </li>
+        <li class="nav-item">
+            <a class="nav-link {{ auth_active }}" id="config-auth-tab" data-toggle="tab" href="#config-auth" role="tab"
+                aria-controls="config-auth" aria-selected="{% if auth_active %}true{% else %}false{% endif %}">{{
+                    label_tab_auth }}</a>
+        </li>
         <li class="nav-item">
             <a class="nav-link {{ email_active }}" id="config-email-tab" data-toggle="tab" href="#config-email" role="tab"
                 aria-controls="config-email" aria-selected="{% if email_active %}true{% else %}false{% endif %}">{{
@@ -93,6 +98,43 @@
                 {{ macro.button_save("general_submit",  label_save) }}
             </fieldset>
         </div>
+        <div class="tab-pane {{ auth_active }}" id="config-auth" role="tabpanel" aria-labelledby="config-auth-tab">
+            <!-- Auth settings -->
+            <fieldset>
+                <legend>{{ label_settings_dirauth }}</legend>
+                <!-- enable ldap -->
+                {{ macro.input_checkbox("dirauth_status", "dirauth_status[]", label_dirauth_status, dirauth_status_checked) }}
+                <!-- Directory host -->
+                {{ macro.input_field("text", "authdir_host_locn", null, "authdir_host_locn", label_authdir_host_locn, authdir_host_locn, label_authdir_host_locn, "100") }}
+                <!-- smtp security -->
+                {{ macro.input_select("authdir_type", "authdir_type", label_authdir_type, authdir_type, authdir_type_selected, "authdir_type_help", label_authdir_type_description) }}
+                <!-- Directory port -->
+                {{ macro.input_field("text", "authdir_host_port", null, "authdir_host_port", label_authdir_host_port, authdir_host_port, label_authdir_host_port, "10") }}
+                <!-- Active Directory domain -->
+                {{ macro.input_field("text", "authdir_userdomain", null, "authdir_userdomain", label_authdir_userdomain, authdir_userdomain, label_authdir_userdomain, "100", "authdir_userdomain_help", label_authdir_userdomain_description) }}
+                <!-- LDAP protecol version -->
+                {{ macro.input_field("text", "authdir_ldapver", null, "authdir_ldapver", label_authdir_ldapver, authdir_ldapver, label_authdir_ldapver, "100", "authdir_ldapver_help", label_authdir_ldapver_description) }}
+                <!-- Follow referrals -->
+                {{ macro.input_checkbox("authdir_ldapfollowref", "authdir_ldapfollowref[]", label_authdir_ldapfollowref, authdir_ldapfollowref_checked, "authdir_ldapfollowref_help", label_authdir_ldapfollowref_description) }}
+                <!-- Base DN* -->
+                {{ macro.input_field("text", "authdir_basedn", null, "authdir_basedn", label_authdir_basedn, authdir_basedn, "dc=domain,dc=tld", "100", "authdir_basedn_help", label_authdir_basedn_description) }}
+                <!-- Username attribute -->
+                {{ macro.input_field("text", "authdir_usernameattrib", null, "authdir_usernameattrib", label_authdir_usernameattrib, authdir_usernameattrib, label_authdir_usernameattrib, "100", "authdir_usernameattrib_help", label_authdir_usernameattrib_description) }}
+                <!-- Group name attribute -->
+                {{ macro.input_field("text", "authdir_groupnameattrib", null, "authdir_groupnameattrib", label_authdir_groupnameattrib, authdir_groupnameattrib, label_authdir_groupnameattrib, "100", "authdir_groupnameattrib_help", label_authdir_groupnameattrib_description) }}
+                <!-- Group member attribute -->
+                {{ macro.input_field("text", "authdir_groupmemattrib", null, "authdir_groupmemattrib", label_authdir_groupmemattrib, authdir_groupmemattrib, label_authdir_groupmemattrib, "100", "authdir_groupmemattrib_help", label_authdir_groupmemattrib_description) }}
+                <!-- User container RDN -->
+                {{ macro.input_field("text", "authdir_usercontainerrdn", null, "authdir_usercontainerrdn", label_authdir_usercontainerrdn, authdir_usercontainerrdn, "ou=Users", "100", "authdir_usercontainerrdn_help", label_authdir_usercontainerrdn_description) }}
+                <!-- Group container RDN -->
+                {{ macro.input_field("text", "authdir_groupcontainerrdn", null, "authdir_groupcontainerrdn", label_authdir_groupcontainerrdn, authdir_groupcontainerrdn, "ou=Groups", "100", "authdir_groupcontainerrdn_help", label_authdir_groupcontainerrdn_description) }}
+                <!-- Authorised directory group -->
+                {{ macro.input_field("text", "authdir_groupname", null, "authdir_groupname", label_authdir_groupname, authdir_groupname, label_authdir_groupname, "100", "authdir_groupname_help", label_authdir_groupname_description) }}
+                <!-- Default role -->
+                {{ macro.input_select("authdir_defaultrole", "authdir_defaultrole", label_authdir_defaultrole, authdir_defaultroles, authdir_defaultrole_selected, "authdir_defaultrole_help", label_authdir_defaultrole_description) }}
+                {{ macro.button_save("auth_submit",  label_save) }}
+            </fieldset>
+        </div>
         <div class="tab-pane {{ email_active }}" id="config-email" role="tabpanel" aria-labelledby="config-email-tab">
             <fieldset>
                 <legend>{{ label_settings_email }}</legend>