Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for LDAP. #1124

Open
wants to merge 45 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9ef2ecc
add SMTP email sending, LDAP authentication with auto user creation
epsilon-0 Jul 4, 2022
74e92aa
Added support for LDAP filters and attribute search
El-Virus Jul 19, 2022
6cc928e
Removed new field hiding system and changed LDAP and SMTP parameters …
El-Virus Jul 19, 2022
32aeff2
Add support for LDAP Groups, minor fixes
El-Virus Jul 20, 2022
16922d2
Added Morphology hook to Standard Settings Controller of Baikal Admin.
El-Virus Jul 24, 2022
75dbdf1
Merge commit '9ef2ecc184c72332f142061452f261e117d60986'
El-Virus Jul 24, 2022
7b2bb3e
Merge commit '74e92aa3c48bc3cdce3d16e3b3fc552cd8cc9318'
El-Virus Jul 24, 2022
e8237c9
Merge commit '6cc928e050a77789622a8d450ac60ebba76fe254'
El-Virus Jul 24, 2022
7abdc21
Merge commit '32aeff23ef9bd05d07a7d22a775501c9e36ad47b'
El-Virus Jul 24, 2022
11fd40d
Added missing refresh on "WebDAV authentication type" change
El-Virus Jul 24, 2022
248a4a8
Fix LDAP.php, according to linter.
El-Virus Jul 24, 2022
9806378
Fix Standard.php, according to linter.
El-Virus Jul 24, 2022
4eb5981
Fix (BaikalAdmin) Standard.php, according to linter.
El-Virus Jul 24, 2022
996ea8d
Added Curly Braces to if statements.
El-Virus Jul 28, 2022
7d6067f
Added a couple of missing spaces
El-Virus Jul 28, 2022
42a926b
Added quotation marks surrounding url, and a period.
El-Virus Jul 30, 2022
25741a6
Fixed https://github.com/sabre-io/Baikal/pull/1124#issuecomment-12394…
El-Virus Sep 7, 2022
d9c9d3d
Fixed linter errors
El-Virus Sep 7, 2022
7fda407
fix the patternReplace function
epsilon-0 Sep 7, 2022
34bc4a9
LDAP bind Password hidden
El-Virus Sep 8, 2022
e8b2178
Merge pull request #1 from bsd-ac/LDAP
El-Virus Sep 8, 2022
3e6ab43
Fix LDAP.php, according to linter.
El-Virus Sep 8, 2022
38ca1f0
Epsilon0's merge fix
El-Virus Sep 8, 2022
f8d25b4
Merge remote-tracking branch 'refs/remotes/upstream/master'
El-Virus Oct 9, 2022
3d3e756
Actually allow LDAP bind password to be set
El-Virus Oct 15, 2022
6e455eb
Added LDAP Config Struct and default LDAP Params to dist config.
El-Virus Oct 16, 2022
9a175df
Fix LDAP.php, according to linter.
El-Virus Oct 16, 2022
0bfa4a8
Moved Structs folder to correct location.
El-Virus Oct 16, 2022
8633f78
Fix LDAPConfig.php, according to linter.
El-Virus Oct 16, 2022
b7d68b3
Added empty value on config set safeguard.
El-Virus Oct 17, 2022
bf0288f
Fix LDAP.php's license
El-Virus Oct 30, 2022
4fb8397
Fix LDAPConfig.php's license
El-Virus Oct 30, 2022
171dab0
Changed copyright notice. Added check for empty bind password.
El-Virus Nov 4, 2022
8885a9f
Changed $username to $dn
El-Virus Nov 20, 2022
3f4c6a7
Removed an article from a settings label
El-Virus Dec 31, 2022
8583553
Removed an article from a settings label
El-Virus Dec 31, 2022
afb5d38
Added slash to ldap_connect
El-Virus Dec 31, 2022
de8e4ff
Fixed typo in settings
El-Virus Dec 31, 2022
12b4121
Remove articles from config page.
El-Virus Dec 31, 2022
b492d70
Fixed 'Undefined array key 0' on incorrect username
El-Virus Jan 13, 2023
cd967f1
Added check for LDAP extension availability
El-Virus Jan 13, 2023
4b3213a
Fix LDAP.php, according to linter
El-Virus Jan 13, 2023
3677285
Merge commit 'aa7e340113545f8be18b6e8c44d001fc4e684526'
El-Virus Jun 23, 2023
d62f271
Applied standard settings morphology hook to initialization wizard.
El-Virus Jun 23, 2023
d576221
Merge remote-tracking branch 'refs/remotes/upstream/master'
El-Virus Aug 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
319 changes: 319 additions & 0 deletions Core/Frameworks/Baikal/Core/LDAP.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
<?php

namespace Baikal\Core;

/**
* This is an authentication backend that uses ldap.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Aisha Tammy <[email protected]>
* @author El-Virus <[email protected]>
* @license http://sabre.io/license/ Modified BSD License
*/
class LDAP extends \Sabre\DAV\Auth\Backend\AbstractBasic {
/**
* Reference to PDO connection.
*
* @var PDO
*/
protected $pdo;

/**
* PDO table name we'll be using.
*
* @var string
*/
protected $table_name;

/**
* LDAP mode.
* Defines if LDAP authentication should match
* by DN, Attribute, or Filter.
*
* @var string
*/
protected $ldap_mode;

/**
* LDAP server uri.
* e.g. "ldaps://ldap.example.org".
*
* @var string
*/
protected $ldap_uri;

/**
* LDAP bind dn.
* Defines the bind dn that Baikal is going to use
* when looking for an attribute or filtering.
*
* @var string
*/
protected $ldap_bind_dn;

/**
* LDAP bind password.
* Defines the password used by Baikal for binding.
*
* @var string
*/
protected $ldap_bind_password;

/**
* LDAP dn pattern for binding.
*
* %u - gets replaced by full username
* %U - gets replaced by user part when the
* username is an email address
* %d - gets replaced by domain part when the
* username is an email address
* %1-9 - gets replaced by parts of the the domain
* split by '.' in reverse order
* mail.example.org: %1 = org, %2 = example, %3 = mail
*
* @var string
*/
protected $ldap_dn;

/**
* LDAP attribute to use for name.
*
* @var string
*/
protected $ldap_cn;

/**
* LDAP attribute used for mail.
*
* @var string
*/
protected $ldap_mail;

/**
* LDAP base path where to search for attributes
* and apply filters.
*
* @var string
*/
protected $ldap_search_base;

/**
* LDAP attribute to search for.
*
* @var string
*/
protected $ldap_search_attribute;

/**
* LDAP filter to apply.
*
* @var string
*/
protected $ldap_search_filter;

/**
* LDAP group to check if a user is member of.
*
* @var string
*/
protected $ldap_group;

/**
* Replaces patterns for their assigned value.
*
* @param string &$base
* @param string $username
*/
protected function patternReplace(&$base, $username) {
$user_split = explode('@', $username, 2);
$ldap_user = $user_split[0];
$ldap_domain = '';
if (count($user_split) > 1) {
$ldap_domain = $user_split[1];
}
$domain_split = array_reverse(explode('.', $ldap_domain));

$base = str_replace('%u', $username, $base);
$base = str_replace('%U', $ldap_user, $base);
$base = str_replace('%d', $ldap_domain, $base);
for ($i = 1; $i <= count($domain_split) and $i <= 9; ++$i) {
$base = str_replace('%' . $i, $domain_split[$i - 1], $base);
}
}

/**
* Checks if a user can bind with a password.
* If an error is produced, it will be logged.
*
* @param \LDAP\Connection &$conn
* @param string $dn
* @param string $password
*
* @return bool
*/
protected function doesBind(&$conn, $dn, $password) {
try {
$bind = ldap_bind($conn, $dn, $password);
if ($bind) {
return true;
}
} catch (\ErrorException $e) {
error_log($e->getMessage());
error_log(ldap_error($conn));
}

return false;
}

/**
* Creates the backend object.
*
* @param string $ldap_uri
* @param string $ldap_dn
* @param string $ldap_cn
* @param string $ldap_mail
*/
public function __construct(\PDO $pdo, $table_name = 'users', $ldap_mode = 'DN', $ldap_uri = 'ldap://127.0.0.1', $ldap_bind_dn = 'cn=baikal,ou=apps,dc=example,dc=com', $ldap_bind_password = '', $ldap_dn = 'mail=%u', $ldap_cn = 'cn', $ldap_mail = 'mail', $ldap_search_base = 'ou=users,dc=example,dc=com', $ldap_search_attribute = 'uid=%U', $ldap_search_filter = '(objectClass=*)', $ldap_group = 'cn=baikal,ou=groups,dc=example,dc=com') {
El-Virus marked this conversation as resolved.
Show resolved Hide resolved
$this->pdo = $pdo;
$this->table_name = $table_name;
$this->ldap_mode = $ldap_mode;
$this->ldap_uri = $ldap_uri;
$this->ldap_bind_dn = $ldap_bind_dn;
$this->ldap_bind_password = $ldap_bind_password;
$this->ldap_dn = $ldap_dn;
$this->ldap_cn = $ldap_cn;
$this->ldap_mail = $ldap_mail;
$this->ldap_search_base = $ldap_search_base;
$this->ldap_search_attribute = $ldap_search_attribute;
$this->ldap_search_filter = $ldap_search_filter;
$this->ldap_group = $ldap_group;
}

/**
* Connects to an LDAP server and tries to authenticate.
*
* @param string $username
* @param string $password
*
* @return bool
*/
protected function ldapOpen($username, $password) {
$conn = ldap_connect($this->ldap_uri);
if (!$conn) {
return false;
}
if (!ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3)) {
return false;
}

$success = false;

if ($this->ldap_mode == 'DN') {
$this->patternReplace($dn, $username);

$success = $this->doesBind($conn, $dn, $password);
} elseif ($this->ldap_mode == 'Attribute' || $this->ldap_mode == 'Group') {
try {
if (!$this->doesBind($conn, $this->ldap_bind_dn, $this->ldap_bind_password)) {
return false;
}

$attribute = $this->ldap_search_attribute;
$this->patternReplace($attribute, $username);

$result = ldap_get_entries($conn, ldap_search($conn, $this->ldap_search_base, '(' . $attribute . ')', [explode('=', $attribute, 2)[0]], 0, 1, 0, LDAP_DEREF_ALWAYS, []))[0];
El-Virus marked this conversation as resolved.
Show resolved Hide resolved

$dn = $result["dn"];

if ($this->ldap_mode == 'Group') {
$inGroup = false;
$members = ldap_get_entries($conn, ldap_read($conn, $this->ldap_group, '(objectClass=*)', ['member', 'uniqueMember'], 0, 0, 0, LDAP_DEREF_NEVER, []))[0];
if (isset($members["member"])) {
foreach ($members["member"] as $member) {
if ($member == $result["dn"]) {
$inGroup = true;
break;
}
}
}
if (isset($members["uniqueMember"])) {
foreach ($members["uniqueMember"] as $member) {
if ($member == $result["dn"]) {
$inGroup = false;
break;
}
}
}
if (!$inGroup) {
return false;
}
}

$success = $this->doesBind($conn, $dn, $password);
} catch (\ErrorException $e) {
error_log($e->getMessage());
error_log(ldap_error($conn));
}
} elseif ($this->ldap_mode == 'Filter') {
try {
if (!$this->doesBind($conn, $this->ldap_bind_dn, $this->ldap_bind_password)) {
return false;
}

$filter = $this->ldap_search_filter;
$this->patternReplace($filter, $username);

$result = ldap_get_entries($conn, ldap_search($conn, $this->ldap_search_base, $filter, [], 0, 1, 0, LDAP_DEREF_ALWAYS, []))[0];

$dn = $result["dn"];
$success = $this->doesBind($conn, $dn, $password);
} catch (\ErrorException $e) {
error_log($e->getMessage());
error_log(ldap_error($conn));
}
} else {
error_log('Unknown LDAP authentication mode');
}

if ($success) {
$stmt = $this->pdo->prepare('SELECT username, digesta1 FROM ' . $this->table_name . ' WHERE username = ?');
$stmt->execute([$username]);
$result = $stmt->fetchAll();

if (empty($result)) {
$search_results = ldap_read($conn, $dn, '(objectclass=*)', [$this->ldap_cn, $this->ldap_mail]);
$entry = ldap_get_entries($conn, $search_results);
$user_displayname = $username;
$user_email = 'unset-email';
if (!empty($entry[0][$this->ldap_cn])) {
$user_displayname = $entry[0][$this->ldap_cn][0];
}
if (!empty($entry[0][$this->ldap_mail])) {
$user_email = $entry[0][$this->ldap_mail][0];
}

$user = new \Baikal\Model\User();
$user->set('username', $username);
$user->set('displayname', $user_displayname);
$user->set('email', $user_email);
$user->persist();
}
}

ldap_close($conn);

return $success;
}

/**
* Validates a username and password by trying to authenticate against LDAP.
*
* @param string $username
* @param string $password
*
* @return bool
*/
protected function validateUserPass($username, $password) {
return $this->ldapOpen($username, $password);
}
}
2 changes: 2 additions & 0 deletions Core/Frameworks/Baikal/Core/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ protected function initServer() {
$authBackend = new \Baikal\Core\PDOBasicAuth($this->pdo, $this->authRealm);
} elseif ($this->authType === 'Apache') {
$authBackend = new \Sabre\DAV\Auth\Backend\Apache();
} elseif ($this->authType === 'LDAP') {
$authBackend = new \Baikal\Core\LDAP($this->pdo, 'users', $config['system']['ldap_mode'], $config['system']['ldap_uri'], $config['system']['ldap_bind_dn'], $config['system']['ldap_bind_password'], $config['system']['ldap_dn'], $config['system']['ldap_cn'], $config['system']['ldap_mail'], $config['system']['ldap_search_base'], $config['system']['ldap_search_attribute'], $config['system']['ldap_search_filter'], $config['system']['ldap_group']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With an LDAP-specific settings section, you could pass that full section in a single parameter instead of extracting this huge number of settings here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite get what you mean, that parameter would still need to read the values from the config file, resulting in even larger code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about something like $authBackend = new \Baikal\Core\LDAP($this->pdo, 'users', $config['ldap]);. The constructor internally needs a ton of lines to assign everything to member variables anyway.

Alternatively, how about using an LdapConfig struct? Something like this:

class LdapConfig {
    public $ldap_mode;
    public $ldap_uri;
    ...
}
$LdapConfig = new LdapConfig();
$LdapConfig->ldap_uri = $config['xy];
...
$authBackend = new \Baikal\Core\LDAP($this->pdo, 'users', $LdapConfig);
class LDAP extends \Sabre\DAV\Auth\Backend\AbstractBasic {
    protected $LdapConfig;
    public function __construct(..., $LdapConfig) {
        $this->LdapConfig = $LdapConfig;
    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ByteHamster, I see what you mean, but as LDAP config is set in "System Settings", it'll be saved under $config['system']. So either we create a new tab or we could add code in set to handle LDAP set the values independently.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, an additional settings section sounds like a lot of work and also quite hacky. How do you feel about that "struct"? It would reduce the long list of constructor parameters, making it a bit easier to comprehend

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ByteHamster, is this ok?

} else {
$authBackend = new \Sabre\DAV\Auth\Backend\PDO($this->pdo);
$authBackend->setRealm($this->authRealm);
Expand Down
Loading