Skip to content

Instantly share code, notes, and snippets.

@moqmar
Created September 6, 2020 20:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save moqmar/9f2a16af8e68641d8466c22e387e57e5 to your computer and use it in GitHub Desktop.
Save moqmar/9f2a16af8e68641d8466c22e387e57e5 to your computer and use it in GitHub Desktop.
LDAP login module for ProcessWire admin - it's only "Quick and Dirty" as it's only built for our own use cases and hard-codes all the options currently, but it automatically updates users when they try to log in, checks for changed passwords, has group support, and deletes users who don't have access anymore if they log in
<?php
class QuickAndDirtyLDAP extends WireData implements Module, ConfigurableModule
{
// TODO: make those settings modifiable from the admin interface
private $LDAPServer = "ldaps://example.org:636";
private $LDAPBindUser = "cn=readonly,dc=example,dc=org";
private $LDAPBindPassword = "blubb";
private $LDAPSearchScope = "ou=users,dc=example,dc=org";
private $LDAPSearchFilter = "(|(uid=%u)(mail=%u))";
private $ValidEmailDomains = array("example.org");
private $GroupRoles = array(
"cn=admin,ou=groups,dc=example,dc=org" => "superuser",
"cn=webmaster,ou=groups,dc=example,dc=org" => "webmaster",
);
public static function getModuleInfo() {
return array(
'title' => __('Quick & Dirty LDAP'),
'version' => '001',
'author' => 'Moritz Marquardt',
'summary' => __('Enables users to sign in via LDAP'),
'singular' => true,
'autoload' => true
);
}
public function ___install() {
if (!function_exists('ldap_connect')) throw new WireException ('Please make sure that extension php_ldap is loaded.');
}
public function init() {
$this->session->addHookAfter('login', $this, 'hookLogin');
//$this->addHook('ProcessLogin::buildLoginForm', $this, 'hookLoginForm');
//$this->addHookAfter('Modules::saveModuleConfigData', $this, 'hookModuleSave');
}
private function deleteUser($username) {
if ($username == "admin") return;
$wire_username = $this->sanitizer->pageName($username);
$user = $this->users->get("name=$wire_username");
if (!($user instanceof NullPage)) {
$this->users->delete($user);
}
}
private $x = 0;
public function ___hookLogin(HookEvent $event) {
$this->x++;
if ($this->x > 10) die("recursion");
$original_username = $event->arguments[0];
$password = $event->arguments[1];
$force = $event->arguments[2];
// Disallow invalid names that could cause trouble when used in LDAP filters
if (!preg_match('@^[a-zA-Z0-9_.-]+$@', $original_username)) { $event->object->error("Invalid username"); $event->object->logout(); $event->return = null; return; }
// Connect to the LDAP server
$ldap = ldap_connect($this->LDAPServer);
if ($ldap === FALSE) { $event->object->error("Can't connect to LDAP server"); $event->object->logout(); $event->return = null; return; }
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
// "@" gets sanitized to "-" by ProcessWire, but we want to allow email addresses as well!
$ldap_username = $original_username;
foreach ($this->ValidEmailDomains as $domain) {
if (substr_compare($ldap_username, "-$domain", -1 * strlen("-$domain")) === 0) {
$ldap_username = substr($ldap_username, 0, -1 * strlen("-$domain")) . "@$domain";
}
}
// Check if the user exists & get their details
if (!ldap_bind($ldap, $this->LDAPBindUser, $this->LDAPBindPassword)) { $event->object->error("Can't bind to LDAP server"); $event->object->logout(); $event->return = null; return; }
$result = ldap_search($ldap, $this->LDAPSearchScope, str_replace("%u", $ldap_username, $this->LDAPSearchFilter), array("uid", "memberOf", "mail"));
// TODO: make field names adjustable
// If the user doesn't exist, delete the corresponding ProcessWire user
if ($result === FALSE) { $this->deleteUser($ldap_username); $event->object->error("No such user"); $event->object->logout(); $event->return = null; return; }
$info = ldap_get_entries($ldap, $result);
if ($info === FALSE || $info["count"] < 1) { $this->deleteUser($ldap_username); $event->object->error("No such user"); $event->object->logout(); $event->return = null; return; }
$ldap_username = $info[0]["uid"][0];
// Get the groups of the user
$roles = array();
foreach ($this->GroupRoles as $group => $role) {
if (in_array($group, $info[0]["memberof"])) {
array_push($roles, $role);
}
}
// Try to bind with the user's password
if (!$force && !ldap_bind($ldap, $info[0]["dn"], $password)) { $event->object->error("Wrong password"); $event->object->logout(); $event->return = null; return; }
// Delete the ProcessWire user if he's in no matching group
if (count($roles) < 1) { $this->deleteUser($ldap_username); $event->object->error("Missing permission"); $event->object->logout(); $event->return = null; return; }
// Create or update the user for ProcessWire
$wire_username = $this->sanitizer->pageName($ldap_username);
$user = $this->users->get("name=$wire_username");
if ($wire_username != $original_username && !$force) {
// Use sanitized username
$event->return = $this->session->forceLogin($wire_username, $password);
} elseif ($user instanceof NullPage) {
$usersPath = $this->users->getGuestUser()->parent;
$user = new User();
$user->parent = $usersPath;
$user->name = $wire_username;
$user->pass = $password;
$user->email = $info[0]["mail"][0];
//$user->language = $info[0]["language"][];
foreach ($roles as $role) {
$user->addRole($role);
}
$user->save();
$event->return = $this->session->forceLogin($wire_username, $password);
} else {
$modified = false;
echo $this->x;
if (!$user->pass->matches($password)) {
$user->pass = $password;
$modified = true;
}
if ($user->email != $info[0]["mail"][0]) {
$user->email = $info[0]["mail"][0];
$modified = true;
}
//if ($user->language != $info[0]["language"][0]) {
// $user->language = $info[0]["language"][0];
//}
$user->roles->each(function($role) use($roles, $user, $modified) {
if (!in_array($role, $roles)) {
$user->removeRole($role);
$modified = true;
}
});
foreach ($roles as $role) {
if (!$user->hasRole($role)) {
$user->addRole($role);
$modified = true;
}
}
if ($modified && !$force) {
$user->save();
$event->return = $this->session->forceLogin($wire_username, $password);
}
}
}
static public function getModuleConfigInputfields(array $data) {
$inputfields = new InputfieldWrapper();
return $inputfields;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment