368 lines
14 KiB
PHP
368 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Leantime\Domain\Ldap\Services;
|
|
|
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
|
use Illuminate\Support\Facades\Log;
|
|
use LDAP\Connection;
|
|
use Leantime\Core\Configuration\Environment;
|
|
use Leantime\Domain\Setting\Repositories\Setting as SettingRepository;
|
|
use Leantime\Domain\Users\Repositories\Users as UserRepository;
|
|
|
|
class Ldap
|
|
{
|
|
private false|Connection $ldapConnection;
|
|
|
|
private mixed $host;
|
|
|
|
private mixed $port;
|
|
|
|
private mixed $ldapDomain;
|
|
|
|
private mixed $ldapUri;
|
|
|
|
private mixed $ldapDn; // DN where users are located (including baseDn)
|
|
|
|
private mixed $ldapKeys = [
|
|
'username' => 'uid',
|
|
'groups' => 'memberof',
|
|
'email' => 'mail',
|
|
'firstname' => 'displayname',
|
|
'lastname' => '',
|
|
'phone' => 'telephonenumber',
|
|
'jobTitle' => 'title',
|
|
'jobLevel' => 'level',
|
|
'department' => 'department',
|
|
];
|
|
|
|
private mixed $ldapLtGroupAssignments = [];
|
|
|
|
private mixed $settingsRepo;
|
|
|
|
private int $defaultRoleKey;
|
|
|
|
private mixed $directoryType = 'OL';
|
|
|
|
private Environment $config;
|
|
|
|
public mixed $useLdap;
|
|
|
|
public mixed $autoCreateUser;
|
|
|
|
/**
|
|
* @throws BindingResolutionException
|
|
*/
|
|
public function __construct(bool|Environment $differentConfig = false)
|
|
{
|
|
|
|
$this->settingsRepo = app()->make(SettingRepository::class);
|
|
|
|
if (! $differentConfig) {
|
|
$this->config = app()->make(Environment::class);
|
|
// Map config vars
|
|
$this->useLdap = $this->config->useLdap;
|
|
|
|
// Don't do anything else if ldap is turned off
|
|
if ($this->useLdap === false) {
|
|
return;
|
|
}
|
|
|
|
// Prepare and map in case we want to get the config from somewhere else in the future
|
|
$this->host = $this->config->ldapHost;
|
|
$this->ldapDn = $this->config->ldapDn;
|
|
$this->defaultRoleKey = (int) $this->config->ldapDefaultRoleKey;
|
|
$this->port = $this->config->ldapPort;
|
|
$this->ldapLtGroupAssignments = json_decode(stripslashes(trim($this->config->ldapLtGroupAssignments)));
|
|
$this->ldapKeys = $this->settingsRepo->getSetting('companysettings.ldap.ldapKeys') ? json_decode($this->settingsRepo->getSetting('companysettings.ldap.ldapKeys')) : json_decode(stripslashes(trim($this->config->ldapKeys)));
|
|
$this->directoryType = $this->config->ldapType;
|
|
|
|
$this->ldapDomain = $this->config->ldapDomain;
|
|
$this->ldapUri = $this->config->ldapUri;
|
|
|
|
if (! is_object($this->ldapLtGroupAssignments) || json_last_error() !== JSON_ERROR_NONE) {
|
|
Log::error('LDAP: Group Assignment array failed to parse. Please check for valid json. Error: '.json_last_error_msg());
|
|
$this->ldapLtGroupAssignments = [];
|
|
}
|
|
|
|
if (! is_object($this->ldapKeys) || json_last_error() !== JSON_ERROR_NONE) {
|
|
Log::error('LDAP: Ldap Keys failed to parse. Please check for valid json. Error: '.json_last_error_msg());
|
|
$this->ldapKeys = (object) [
|
|
'username' => 'uid',
|
|
'groups' => 'memberof',
|
|
'email' => 'mail',
|
|
'firstname' => 'displayname',
|
|
'lastname' => '',
|
|
'phone' => 'telephonenumber',
|
|
'jobTitle' => 'title',
|
|
'jobLevel' => 'level',
|
|
'department' => 'department',
|
|
];
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public function connect(): bool
|
|
{
|
|
|
|
if (! $this->config->useLdap) {
|
|
return false;
|
|
}
|
|
|
|
if (function_exists('ldap_connect')) {
|
|
if ($this->ldapUri !== '' && str_starts_with($this->ldapUri, 'ldap')) {
|
|
$this->ldapConnection = ldap_connect($this->ldapUri);
|
|
} else {
|
|
$this->ldapConnection = ldap_connect($this->host, $this->port);
|
|
}
|
|
|
|
ldap_set_option($this->ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3) or exit('Unable to set LDAP protocol version');
|
|
ldap_set_option($this->ldapConnection, LDAP_OPT_REFERRALS, 0); // We need this for doing an LDAP search.
|
|
|
|
if ($this->config->debug) {
|
|
ldap_set_option($this->ldapConnection, LDAP_OPT_DEBUG_LEVEL, 7);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Log::error('ldap extension not installed');
|
|
|
|
return false;
|
|
}
|
|
|
|
public function bind(string $username = '', string $password = ''): bool
|
|
{
|
|
|
|
if ($username !== '' && $password !== '') {
|
|
$passwordBind = $password;
|
|
|
|
// AD allows usenrame login
|
|
if ($this->directoryType === 'AD') {
|
|
$usernameDN = $username;
|
|
|
|
// Suppress the PHP warning ldap_bind() emits on invalid
|
|
// credentials. Without @, Laravel's error handler promotes it to
|
|
// an ErrorException and a 500, instead of the normal
|
|
// wrong-username/password message. We act on the bool return. (#3374)
|
|
if (str_contains($usernameDN, '@')) {
|
|
$bind = @ldap_bind($this->ldapConnection, $usernameDN, $passwordBind);
|
|
} else {
|
|
$bind = @ldap_bind($this->ldapConnection, $usernameDN.'@'.$this->ldapDomain, $passwordBind);
|
|
}
|
|
|
|
if ($bind) {
|
|
return true;
|
|
}
|
|
} else {
|
|
// OL requires distinguished name login
|
|
$usernameDN = $this->ldapKeys->username.'='.ldap_escape($username, '', LDAP_ESCAPE_DN).','.$this->ldapDn;
|
|
|
|
$bind = @ldap_bind($this->ldapConnection, $usernameDN, $passwordBind);
|
|
}
|
|
if ($bind) {
|
|
return true;
|
|
}
|
|
|
|
if ((bool) $this->config->debug === true) {
|
|
Log::error(ldap_error($this->ldapConnection));
|
|
ldap_get_option($this->ldapConnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $err);
|
|
if ($err) {
|
|
Log::error($err);
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getEmail(string $username): string
|
|
{
|
|
if (! $this->ldapConnection) {
|
|
Log::error('LDAP: No connection established');
|
|
|
|
return '';
|
|
}
|
|
$filter = '('.$this->ldapKeys->username.'='.ldap_escape($this->extractLdapFromUsername($username), '', LDAP_ESCAPE_FILTER).')';
|
|
|
|
$attr = [$this->ldapKeys->groups, $this->ldapKeys->firstname, $this->ldapKeys->lastname, $this->ldapKeys->email];
|
|
|
|
$result = ldap_search($this->ldapConnection, $this->ldapDn, $filter, $attr) or exit('Unable to search LDAP server');
|
|
$entries = ldap_get_entries($this->ldapConnection, $result);
|
|
|
|
if ($entries === false && (bool) $this->config->debug === true) {
|
|
Log::error(ldap_error($this->ldapConnection));
|
|
ldap_get_option($this->ldapConnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $err);
|
|
if ($err) {
|
|
Log::error($err);
|
|
}
|
|
}
|
|
|
|
return isset($entries[0][$this->ldapKeys->email]) ? $entries[0][$this->ldapKeys->email][0] : '';
|
|
}
|
|
|
|
public function getSingleUser(string $username): array|false
|
|
{
|
|
|
|
if (! $this->ldapConnection) {
|
|
Log::error('LDAP: No connection established');
|
|
|
|
return false;
|
|
}
|
|
|
|
$filter = '('.$this->ldapKeys->username.'='.ldap_escape($this->extractLdapFromUsername($username), '', LDAP_ESCAPE_FILTER).')';
|
|
|
|
$attr = [$this->ldapKeys->groups, $this->ldapKeys->firstname, $this->ldapKeys->lastname, $this->ldapKeys->email, $this->ldapKeys->phone, $this->ldapKeys->jobTitle, $this->ldapKeys->jobLevel, $this->ldapKeys->department];
|
|
|
|
$result = ldap_search($this->ldapConnection, $this->ldapDn, $filter, $attr) or exit('Unable to search LDAP server');
|
|
$entries = ldap_get_entries($this->ldapConnection, $result);
|
|
|
|
if ($entries === false || ! isset($entries[0])) {
|
|
if ((bool) $this->config->debug === true) {
|
|
Log::error(ldap_error($this->ldapConnection));
|
|
ldap_get_option($this->ldapConnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $err);
|
|
if ($err) {
|
|
Log::error($err);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Find Role
|
|
$role = $this->defaultRoleKey;
|
|
|
|
if (isset($entries[0][$this->ldapKeys->groups]) && is_array($entries[0][$this->ldapKeys->groups])) {
|
|
foreach ($entries[0][$this->ldapKeys->groups] as $grps) {
|
|
foreach ($this->ldapLtGroupAssignments as $key => $row) {
|
|
if ($row->ldapRole !== '' && (int) $key > $role && strpos($grps, $row->ldapRole) !== false) {
|
|
$role = $key;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The ldap_get_entries function returns all LDAP atribute names in lowercase to insure consistency.
|
|
* A few of these were not camelCase and were already lowercase so they showed up ie; lastname
|
|
* But givenName did not as it needed to be normalized to match the PHP LDAP return
|
|
* Example before and after code change - which has been applied to all LDAP vars in the same fashion
|
|
$firstname = isset($entries[0][$this->ldapKeys->firstname]) ? $entries[0][ $this->ldapKeys->firstname ][0] : '';
|
|
$firstname = isset($entries[0][strtolower($this->ldapKeys->firstname)]) ? $entries[0][strtolower($this->ldapKeys->firstname)][0] : '';
|
|
*/
|
|
|
|
// Find Firstname & Lastname
|
|
$firstname = isset($entries[0][strtolower($this->ldapKeys->firstname)]) ? $entries[0][strtolower($this->ldapKeys->firstname)][0] : '';
|
|
$lastname = isset($entries[0][strtolower($this->ldapKeys->lastname)]) ? $entries[0][strtolower($this->ldapKeys->lastname)][0] : '';
|
|
$phone = isset($entries[0][strtolower($this->ldapKeys->phone)]) ? $entries[0][strtolower($this->ldapKeys->phone)][0] : '';
|
|
$uname = isset($entries[0][strtolower($this->ldapKeys->email)]) ? $entries[0][strtolower($this->ldapKeys->email)][0] : '';
|
|
$jobTitle = isset($entries[0][strtolower($this->ldapKeys->jobTitle)]) ? $entries[0][strtolower($this->ldapKeys->jobTitle)][0] : '';
|
|
$jobLevel = isset($entries[0][strtolower($this->ldapKeys->jobLevel)]) ? $entries[0][strtolower($this->ldapKeys->jobLevel)][0] : '';
|
|
$department = isset($entries[0][strtolower($this->ldapKeys->department)]) ? $entries[0][strtolower($this->ldapKeys->department)][0] : '';
|
|
|
|
if ($this->config->debug) {
|
|
Log::debug("LEANTIME: Testing the logging\n");
|
|
|
|
Log::debug("LEANTIME: >>>Attributes Begin>>>>>>\n");
|
|
Log::debug("LEANTIME: fn $firstname");
|
|
Log::debug("LEANTIME: sn $lastname");
|
|
Log::debug("LEANTIME: phone $phone");
|
|
Log::debug("LEANTIME: role $role");
|
|
Log::debug("LEANTIME: username $uname ");
|
|
Log::debug("LEANTIME: jobTitle $jobTitle ");
|
|
Log::debug("LEANTIME: jobLevel $jobLevel ");
|
|
Log::debug("LEANTIME: department $department ");
|
|
Log::debug("LEANTIME: >>>Attributes End>>>>>>\n");
|
|
}
|
|
|
|
return [
|
|
'user' => $uname,
|
|
'firstname' => $firstname,
|
|
'lastname' => $lastname,
|
|
'role' => $role,
|
|
'phone' => $phone,
|
|
'jobTitle' => $jobTitle,
|
|
'jobLevel' => $jobLevel,
|
|
'department' => $department,
|
|
];
|
|
}
|
|
|
|
public function extractLdapFromUsername(string $username): string
|
|
{
|
|
|
|
$getLdap = explode('@', $username);
|
|
|
|
if (is_array($getLdap)) {
|
|
return $getLdap[0];
|
|
}
|
|
}
|
|
|
|
public function getAllMembers(): array|false
|
|
{
|
|
|
|
if (function_exists('ldap_search')) {
|
|
$attr = [$this->ldapKeys->groups, $this->ldapKeys->firstname, $this->ldapKeys->lastname];
|
|
|
|
$filter = '(cn=*)';
|
|
|
|
$result = ldap_search($this->ldapConnection, $this->ldapDn, $filter, $attr) or exit('Unable to search LDAP server');
|
|
$entries = ldap_get_entries($this->ldapConnection, $result);
|
|
|
|
$allUsers = [];
|
|
|
|
foreach ($entries as $key => $row) {
|
|
if (isset($row['dn'])) {
|
|
preg_match('/(?:^|.*,)uid=(.*?)(?:,.*$|$)/', $row['dn'], $usernameArray);
|
|
|
|
if (count($usernameArray) > 0) {
|
|
$allUsers[] = $this->getSingleUser($usernameArray[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $allUsers;
|
|
}
|
|
|
|
Log::error('ldap extension not installed');
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @throws BindingResolutionException
|
|
*/
|
|
public function upsertUsers(array $ldapUsers): bool
|
|
{
|
|
|
|
$userRepo = app()->make(UserRepository::class);
|
|
|
|
foreach ($ldapUsers as $user) {
|
|
// Update
|
|
$checkUser = $userRepo->getUserByEmail($user['user']);
|
|
|
|
if (is_array($checkUser)) {
|
|
$userRepo->patchUser($checkUser['id'], ['firstname' => $user['firstname'], 'lastname' => $user['lastname'], 'role' => $user['role']]);
|
|
} else {
|
|
// Insert
|
|
$userArray = [
|
|
'firstname' => $user['firstname'],
|
|
'lastname' => $user['lastname'],
|
|
'phone' => $user['phone'],
|
|
'user' => $user['user'],
|
|
'role' => $user['role'],
|
|
'password' => '',
|
|
'clientId' => '',
|
|
'jobTitle' => $user['jobTitle'],
|
|
'jobLevel' => $user['jobLevel'],
|
|
'department' => $user['department'],
|
|
'source' => 'ldap',
|
|
];
|
|
|
|
$userRepo->addUser($userArray);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|