Files
luos/workflow/engine/classes/AuthSources.php
2025-09-22 22:29:47 +00:00

1034 lines
46 KiB
PHP

<?php
use ProcessMaker\Model\RbacAuthenticationSource;
use ProcessMaker\Model\RbacUsers;
use ProcessMaker\Model\Configuration;
use ProcessMaker\Model\GroupUser;
use ProcessMaker\Model\Groupwf;
use ProcessMaker\Model\User;
use ProcessMaker\Model\Department;
/**
* AuthSources Class
*
* This class manages authentication sources for the Lurana workflow engine.
* It provides functionality to handle LDAP authentication sources including:
* - Listing and managing authentication sources
* - Testing LDAP connections
* - Importing users from LDAP
* - Managing groups and departments synchronization
*
* @package Lurana\Workflow\Engine\Classes
* @author Lurana Team
* @version 1.0
*/
class AuthSources
{
/**
* Retrieves a paginated list of authentication sources
*
* This method fetches authentication sources with pagination support,
* filtering, and sorting capabilities. It also includes user count
* information for each authentication source.
*
* @param string $userUid The unique identifier of the requesting user
* @param int $start Starting position for pagination (default: 0)
* @param int $limit Maximum number of records to return (default: 25)
* @param string $orderBy Field name to sort by (optional)
* @param string $ascending Sort direction: 'asc' or 'desc' (default: 'asc', invalid values auto-convert to 'asc')
* @param string $filter Text filter to search authentication sources (optional)
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - sources: array of authentication source data
* - total_sources: total count of authentication sources
* - message: error message if operation fails
*/
public function getListAuthSources($userUid, $start = 0, $limit = 25, $orderBy = '', $ascending = 'asc' , $filter = '') {
try {
if ($limit == 0) {
$limit = 25;
$filters = array(
'fields' => ['CFG_VALUE'],
'conditions' => ['CFG_UID' => 'authSourcesList', 'OBJ_UID' => 'pageSize', 'USR_UID' => $userUid]
);
$configuration = new Configuration();
$configurationReturn = $configuration->show($filters);
if ($configurationReturn['total'] > 0) {
$configValue = unserialize($configurationReturn['data'][0]['CFG_VALUE']);
$limit = $configValue['pageSize'] ?? $limit;
}
}
$filters = array(
'fields' => ['*'],
'start' => $start,
'limit'=> $limit
);
if ($orderBy != '') {
if (!in_array($ascending, ['asc', 'desc'])) {
$ascending = 'asc';
}
$filters['orderBy'] = [$orderBy, $ascending];
}
if ($filter != '') {
$filters['conditions'] = ['text' => $filter];
}
$rbacAuthenticationSource = new RbacAuthenticationSource();
$authSourceReturn = $rbacAuthenticationSource->show($filters);
$rbacUsers = new RbacUsers();
$usersByAuthSource = $rbacUsers->getAllUsersByAuthSource();
$auth = [];
foreach (($usersByAuthSource['data'] ?? []) as $user) {
$auth[$user['UID_AUTH_SOURCE']] = $user['CNT'];
}
$sources = [];
foreach (($authSourceReturn['data'] ?? []) as $authSourceRow) {
$label = G::LoadTranslation('ID_DISABLE');
if ($authSourceRow['AUTH_SOURCE_ENABLED_TLS'] === '1') {
$label = G::LoadTranslation('ID_ENABLE');
}
$authSourceRow['AUTH_SOURCE_ENABLED_TLS_LABEL'] = $label;
$authSourceRow['AUTH_SOURCE_PASSWORD'] = '';
//additional information
$authSourceData = json_decode($authSourceRow['AUTH_SOURCE_DATA'], true);
if (is_array($authSourceData)) {
$authSourceRow = array_merge($authSourceRow, $authSourceData);
}
$authSourceRow['AUTH_ANONYMOUS'] = (string)$authSourceRow['AUTH_ANONYMOUS'];
$sources[] = $authSourceRow;
$index = array_key_last($sources);
$sources[$index]['CURRENT_USERS'] = isset($auth[$sources[$index]['AUTH_SOURCE_UID']]) ? $auth[$sources[$index]['AUTH_SOURCE_UID']] : 0;
}
$response = [
'success' => true,
'sources' => $sources,
'total_sources' => $authSourceReturn['total']
];
return $response;
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Removes an authentication source from the system
*
* This method permanently deletes an authentication source
* identified by its unique identifier. It validates that the
* authentication source exists before attempting removal.
*
* @param string $authSourceUid The unique identifier of the authentication source to remove
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - deleteRows: number of rows deleted (if successful)
* - message: error message if operation fails
*
* @throws Exception When authSourceUid is empty or authentication source not found
*/
public function removeAuthSource($authSourceUid) {
try {
if (empty($authSourceUid)) {
throw new Exception('Authentication source UID is required');
}
$conditions = ['AUTH_SOURCE_UID' => $authSourceUid];
$rbacAuthenticationSource = new RbacAuthenticationSource();
$removeResponse = $rbacAuthenticationSource->remove($conditions);
return ['success' => true, 'deleteRows' => $removeResponse['deleteRows'] ];
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Verifies if an authentication source name already exists and suggests alternatives
*
* This method checks if a given authentication source name is already in use.
* If the name exists, it generates a suggested alternative name by appending
* a number in parentheses (e.g., "Source Name (1)").
*
* @param string $authSourceName The authentication source name to verify
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - row: existing authentication source data (false if not found)
* - suggestName: suggested alternative name if original exists
* - message: error message if operation fails
*/
public function verifyAuthSourceName($authSourceName) {
try {
$row = false;
$suggestName = '';
$filters = [
'fields' => ['AUTH_SOURCE_UID', 'AUTH_SOURCE_NAME'],
'conditions' => ['AUTH_SOURCE_NAME' => $authSourceName]
];
$rbacAuthenticationSource = new RbacAuthenticationSource();
$authSourceReturn = $rbacAuthenticationSource->show($filters);
$authSourceReturn['AUTH_SOURCE_DATA'] = json_decode($authSourceReturn['AUTH_SOURCE_DATA'], true);
if ($authSourceReturn['total'] > 0) {
$row = $authSourceReturn['data'][0];
$filters['fields'] = ['AUTH_SOURCE_NAME'];
$filters['conditions'] = ['text' => $authSourceName];
$filters['orderBy'] = ['AUTH_SOURCE_NAME', 'desc'];
$lastAuthSource = $rbacAuthenticationSource->show($filters);
if ($lastAuthSource['total'] > 0) {
$name = $lastAuthSource['data'][0]['AUTH_SOURCE_NAME'];
//get suggest name
$pieces = explode( ' ', $name);
$last = array_pop($pieces);
$number = trim($last, '()');
if ("({$number})" === $last) {
$number = intval($number) + 1;
$suggestName = implode('', $pieces) . " ({$number})";
} else {
$suggestName = $name . ' (1)';
}
}
}
return ['success' => true, 'row' => $row, 'suggestName' => $suggestName];
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Tests the connection to an LDAP authentication source
*
* This method validates the connection parameters for an LDAP server
* and checks if TLS encryption is properly configured.
*
* @param array $authSourceData Array containing LDAP connection parameters:
* - server, port, username, password, base DN, etc.
*
* @return array Response array containing:
* - success: boolean indicating connection success
* - status: connection status ('OK' if successful)
* - message: warning/error message (e.g., TLS certificate issues)
*/
public function testConnection($authSourceData) {
try {
$authSourceData = $this->verifyEditAuthSourceData($authSourceData);
$ldapSource = new LdapSource();
$authSourceConnectionData = $ldapSource->ldapConnection($authSourceData);
$response = ['success' => true, 'status' => 'OK'];
if ($authSourceConnectionData['startTLS'] === false) {
$response['message'] = G::LoadTranslation('ID_TLS_CERTIFICATE_IS_NOT_INSTALLED_IN_THE_SERVER');
}
return $response;
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Saves or updates an authentication source configuration
*
* This method creates a new authentication source or updates an existing one.
* It establishes an LDAP connection to validate the configuration and
* determines the LDAP page size limit for optimal performance.
*
* @param array $authSourceData Complete authentication source configuration including:
* - AUTH_SOURCE_UID: unique identifier (empty for new sources)
* - AUTH_SOURCE_DATA: LDAP connection parameters
* - AUTH_SOURCE_BASE_DN: base distinguished name
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - saveData: saved authentication source data
* - message: error message if operation fails
*/
public function saveAuthSource($authSourceData) {
try {
$ldapSource = new LdapSource();
$authSourceData['AUTH_SOURCE_VERSION'] = 3;
$authSourceData = $this->verifyEditAuthSourceData($authSourceData);
$ldapConnection = $ldapSource->ldapConnection($authSourceData);
if (!isset($ldapConnection['connection']) || !$ldapConnection['connection']) {
throw new Exception('LDAP connection failed');
}
$authSourceData['AUTH_SOURCE_DATA']['LDAP_PAGE_SIZE_LIMIT'] = $ldapSource->getPageSizeLimit(
$ldapConnection['connection'],
$authSourceData['AUTH_SOURCE_BASE_DN']
);
$authSourceData['AUTH_SOURCE_UID'] = $authSourceData['AUTH_SOURCE_UID'] ?? '';
if (!empty($authSourceData['AUTH_SOURCE_UID']) && empty($authSourceData['AUTH_SOURCE_PASSWORD'])) {
unset($authSourceData['AUTH_SOURCE_PASSWORD']);
} else {
$authSourceData['AUTH_SOURCE_PASSWORD'] = G::encrypt($authSourceData['AUTH_SOURCE_PASSWORD'], URL_KEY);
}
$authSourceData['AUTH_SOURCE_DATA'] = json_encode($authSourceData['AUTH_SOURCE_DATA']);
$rbacAuthenticationSource = new RbacAuthenticationSource();
$saveDataResponse = $rbacAuthenticationSource->saveData($authSourceData);
return ['success' => true, 'saveData' => $saveDataResponse];
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Searches for users in an LDAP authentication source
*
* This method queries an LDAP server to find users based on search criteria.
* It compares found users with existing system users to determine import status.
*
* @param string $authSourceUid The unique identifier of the authentication source
* @param array $filters Search parameters containing:
* - text: search term for user lookup
* - start: pagination start position
* - limit: maximum number of results
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - status: operation status ('OK' if successful)
* - resultTotal: total number of users found
* - resultRoot: array of user data with import status
* - message: error message if operation fails
*/
public function searchUsers($authSourceUid, $filters) {
try {
$rbacUsers = new RbacUsers();
$usersAuthSources = $rbacUsers->listUsersAuthSources();
$listUsers = [];
foreach (($usersAuthSources['data'] ?? []) as $row) {
$listUsers[strtolower($row['USR_USERNAME'])] = $row['UID_AUTH_SOURCE'];
}
$ldapSource = new LdapSource();
$ldapSource->authSourceUid = $authSourceUid;
$result = $ldapSource->searchUsersLdap($filters['text'], $filters['start'], $filters['limit']);
$arrayData = [];
foreach (($result['data'] ?? []) as $value) {
$listUsersData = $value;
$usernameLower = strtolower($listUsersData['sUsername']);
if (!isset($listUsers[$usernameLower])) {
$listUsersData['STATUS'] = G::LoadTranslation('ID_NOT_IMPORTED');
$listUsersData['IMPORT'] = 1;
} elseif ($authSourceUid === $listUsers[$usernameLower]) {
$listUsersData['STATUS'] = G::LoadTranslation('ID_IMPORTED');
$listUsersData['IMPORT'] = 0;
} else {
$listUsersData['STATUS'] = G::LoadTranslation('ID_CANNOT_IMPORT');
$listUsersData['IMPORT'] = 0;
}
$arrayData[] = $listUsersData;
}
return ['success' => true, 'status' => 'OK', 'resultTotal' => $result['numRecTotal'], 'resultRoot' => $arrayData];
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Imports users from an LDAP authentication source into the system
*
* This method creates new user accounts in the system based on LDAP user data.
* It handles user attribute mapping, role assignment, and calendar assignment.
*
* @param string $authSourceUid The unique identifier of the authentication source
* @param array $usersImport Array of user objects to import, each containing:
* - sUsername: LDAP username
* - sFirstname: user's first name
* - sLastname: user's last name
* - sEmail: user's email address
* - sDN: user's distinguished name
* - USR_STATUS: user status (optional)
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - message: error message if operation fails
*/
public function importUsers($authSourceUid, $usersImport) {
try {
$filters = ['conditions' => ['AUTH_SOURCE_UID'=> $authSourceUid]];
$rbacAuthenticationSource = new RbacAuthenticationSource();
$authSourceReturn = $rbacAuthenticationSource->show($filters);
if (empty($authSourceReturn['data']) || !isset($authSourceReturn['data'][0])) {
throw new Exception('Authentication source not found');
}
$authSourceReturn = $authSourceReturn['data'][0];
$authSourceReturn['AUTH_SOURCE_DATA'] = json_decode($authSourceReturn['AUTH_SOURCE_DATA'], true);
$aAttributes = $authSourceReturn['AUTH_SOURCE_DATA']['AUTH_SOURCE_GRID_ATTRIBUTE'] ?? [];
global $RBAC;
foreach ($usersImport as $sUser) {
$aUser = (array) $sUser;
$matches = array();
$aData = array();
$aData['USR_USERNAME'] = str_replace('*', "'", $aUser['sUsername']);
$aData['USR_PASSWORD'] = '00000000000000000000000000000000';
$aData['USR_FIRSTNAME'] = str_replace('*', "'", $aUser['sFirstname']);
$aData['USR_FIRSTNAME'] = ($aData['USR_FIRSTNAME'] == '') ? $aData['USR_USERNAME'] : $aData['USR_FIRSTNAME'];
$aData['USR_LASTNAME'] = str_replace('*', "'", $aUser['sLastname']);
$aData['USR_EMAIL'] = $aUser['sEmail'];
$aData['USR_DUE_DATE'] = date('Y-m-d', mktime(0, 0, 0, date('m'), date('d'), date('Y') + 2));
$aData['USR_CREATE_DATE'] = date('Y-m-d H:i:s');
$aData['USR_UPDATE_DATE'] = date('Y-m-d H:i:s');
$aData['USR_BIRTHDAY'] = date('Y-m-d');
// Set USR_STATUS for RBAC (numeric format)
$aData['USR_STATUS'] = (isset($aUser['USR_STATUS'])) ? (($aUser['USR_STATUS'] == 'ACTIVE') ? 1 : 0) : 1;
$aData['USR_AUTH_TYPE'] = strtolower($authSourceReturn['AUTH_SOURCE_PROVIDER']);
$aData['UID_AUTH_SOURCE'] = $authSourceReturn['AUTH_SOURCE_UID'];
preg_match('/[a-zA-Z]\*[a-zA-Z]/', $aUser['sDN'], $matches);
foreach ($matches as $match) {
$newMatch = str_replace('*', '\'', $match);
$aUser['sDN'] = str_replace($match, $newMatch, $aUser['sDN']);
}
$aData['USR_AUTH_USER_DN'] = $aUser['sDN'];
$usrRole = 'LURANA_OPERATOR';
if (!empty($authSourceReturn['AUTH_SOURCE_DATA']['USR_ROLE'])) {
$usrRole = $authSourceReturn['AUTH_SOURCE_DATA']['USR_ROLE'];
}
$sUserUID = $RBAC->createUser($aData, $usrRole, $authSourceReturn['AUTH_SOURCE_NAME']);
// Set USR_STATUS for User model (string format)
$aData['USR_STATUS'] = (isset($aUser['USR_STATUS'])) ? $aUser['USR_STATUS'] : 'ACTIVE';
$aData['USR_UID'] = $sUserUID;
$aData['USR_ROLE'] = $usrRole;
$calendarObj = new Calendar();
$calendarObj->assignCalendarTo($sUserUID, '00000000000000000000000000000001', 'USER');
if (count($aAttributes)) {
foreach ($aAttributes as $value) {
if (isset($aUser[$value['attributeUser']])) {
$aData[$value['attributeUser']] = str_replace('*', "'", $aUser[$value['attributeUser']]);
if ($value['attributeUser'] == 'USR_STATUS') {
$statusValue = $aData['USR_STATUS'];
$aData[$value['attributeUser']] = $statusValue;
}
}
}
}
$oUser = new Users();
$oUser->create($aData);
}
return ['success' => true];
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Searches for groups in an LDAP authentication source
*
* This method retrieves all available groups from an LDAP server
* and compares them with existing system groups to show synchronization status.
*
* @param string $authSourceUid The unique identifier of the authentication source
*
* @return array|TreeNodeAuthSource[] Array of TreeNodeAuthSource objects representing groups, or
* error array containing:
* - success: false
* - message: error description
*/
public function searchGroups($authSourceUid) {
try {
$ldapSource = new LdapSource();
$ldapSource->authSourceUid = $authSourceUid;
$groupsLdap = $ldapSource->searchGroups();
$allGroupsLdap = [];
foreach ($groupsLdap as $group) {
$node = [];
$node['GRP_UID'] = $group['cn'];
$node['GRP_TITLE'] = $group['cn'];
$node['GRP_USERS'] = $group['users'];
$node['GRP_DN'] = $group['dn'];
$allGroupsLdap[] = $node;
}
$groupUser = new GroupUser();
$groupsNumberUsers = $groupUser->getNumberOfUsersByGroups();
$listGroupsNumberUsers = [];
foreach (($groupsNumberUsers['data'] ?? []) as $group) {
$listGroupsNumberUsers[$group['GRP_UID']] = $group['NUM_REC'];
}
$groupwf = new Groupwf();
$groupsObjects = [];
foreach ($allGroupsLdap as $group) {
$groupObject = new TreeNodeAuthSource();
$groupObject->text = htmlentities($group['GRP_TITLE'], ENT_QUOTES, 'UTF-8');
$groupResult = $groupwf->getGroupWithDN($group['GRP_DN']);
if (!empty($groupResult) && !empty($groupResult[0]['GRP_UID'])) {
$groupUid = $groupResult[0]['GRP_UID'];
$groupObject->text .= ' (' . ($listGroupsNumberUsers[$groupUid] ?? 0) . ')';
$groupObject->checked = true;
} else {
$groupObject->checked = false;
}
$groupObject->id = urlencode($group['GRP_DN']);
$groupsObjects[] = $groupObject;
}
return $groupsObjects;
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Searches for departments (organizational units) in an LDAP authentication source
*
* This method retrieves the organizational unit structure from an LDAP server
* and builds a hierarchical tree representation with user count information.
*
* @param string $authSourceUid The unique identifier of the authentication source
*
* @return array|TreeNodeAuthSource[] Hierarchical array of department objects, or
* error array containing:
* - success: false
* - message: error description
*/
public function searchDepartments($authSourceUid) {
try {
$ldapSource = new LdapSource();
$ldapSource->authSourceUid = $authSourceUid;
$departments = $ldapSource->searchDepartments();
$user = new User();
$departmentsNumberUsers = $user->getNumberOfUsersByDepartments();
$listDepartmentsNumberUsers = [];
foreach (($departmentsNumberUsers['data'] ?? []) as $department) {
$listDepartmentsNumberUsers[$department['DEP_UID']] = $department['NUM_REC'];
}
$departmentsObject = $this->getChildrenDepartments($departments, '', $listDepartmentsNumberUsers, $ldapSource->terminatedOu);
return $departmentsObject;
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Saves group synchronization settings for an authentication source
*
* This method processes a list of LDAP group distinguished names to synchronize
* with the system. It creates new groups or updates existing ones and handles
* unsynchronization of previously synchronized groups.
*
* @param string $groupsDN Pipe-separated list of URL-encoded group distinguished names
* @param string $authSourceUid The unique identifier of the authentication source
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - status: operation status ('OK' if successful)
* - message: error message if operation fails
*/
public function saveGroups($groupsDN, $authSourceUid) {
try {
$groupsToCheck = explode('|', $groupsDN);
$groupsToCheck = array_map('urldecode', $groupsToCheck);
$groupsToUncheck = $this->getGroupsToUncheck($groupsToCheck);
$filters = ['conditions' => ['AUTH_SOURCE_UID'=> $authSourceUid]];
$rbacAuthenticationSource = new RbacAuthenticationSource();
$authSourceReturn = $rbacAuthenticationSource->show($filters);
if (empty($authSourceReturn['data']) || !isset($authSourceReturn['data'][0])) {
throw new Exception('Authentication source not found');
}
$authenticationSourceData = $authSourceReturn['data'][0];
$authenticationSourceData['AUTH_SOURCE_DATA'] = json_decode($authenticationSourceData['AUTH_SOURCE_DATA'], true);
$ldapSource = new LdapSource();
$ldapSource->authSourceUid = $authSourceUid;
$groupwf = new Groupwf();
foreach ($groupsToCheck as $groupDN) {
$ous = $ldapSource->custom_ldap_explode_dn($groupDN);
$currentGroup = array_shift($ous);
$groupAux = explode('=', $currentGroup);
$groupTitle = isset($groupAux[1]) ? trim($groupAux[1]) : '';
$groupTitle = stripslashes($groupTitle);
if (empty($groupTitle)) {
continue;
}
$filters = array(
'fields' => ['GRP_UID'],
'conditions' => ['GRP_TITLE' => $groupTitle, 'GRP_STATUS' => 'ACTIVE']
);
$allGroups = $groupwf->show($filters);
$groupUid = '';
if (!empty($allGroups['data']) && isset($allGroups['data'][0]['GRP_UID'])) {
$groupUid = $allGroups['data'][0]['GRP_UID'];
}
if ($groupUid === '') {
$group = [
'GRP_TITLE' => $groupTitle,
'GRP_LDAP_DN' => $groupDN
];
} else {
$group = $allGroups['data'][0];
$group['GRP_LDAP_DN'] = $groupDN;
}
$groupwf->saveData($group);
}
if (count($groupsToUncheck) > 0) {
foreach ($groupsToUncheck as $groupDN) {
$ous = $ldapSource->custom_ldap_explode_dn($groupDN);
$currentGroup = array_shift($ous);
$groupAux = explode('=', $currentGroup);
$groupTitle = isset($groupAux[1]) ? trim($groupAux[1]) : '';
$groupTitle = stripslashes($groupTitle);
if (empty($groupTitle)) {
continue;
}
$filters = array(
'fields' => ['GRP_UID'],
'conditions' => ['GRP_TITLE' => $groupTitle, 'GRP_STATUS' => 'ACTIVE']
);
$allGroups = $groupwf->show($filters);
$groupUid = '';
if (!empty($allGroups['data']) && isset($allGroups['data'][0]['GRP_UID'])) {
$groupUid = $allGroups['data'][0]['GRP_UID'];
}
if ($groupUid != '' && !empty($allGroups['data'])) {
$group = $allGroups['data'][0];
$group['GRP_LDAP_DN'] = '';
$groupwf->saveData($group);
if (!isset($authenticationSourceData['AUTH_SOURCE_DATA']['GROUPS_TO_UNASSIGN'])) {
$authenticationSourceData['AUTH_SOURCE_DATA']['GROUPS_TO_UNASSIGN'] = [];
}
if (!in_array($groupUid, $authenticationSourceData['AUTH_SOURCE_DATA']['GROUPS_TO_UNASSIGN'])) {
$authenticationSourceData['AUTH_SOURCE_DATA']['GROUPS_TO_UNASSIGN'][] = $groupUid;
}
}
}
$authenticationSourceData['AUTH_SOURCE_DATA'] = json_encode($authenticationSourceData['AUTH_SOURCE_DATA']);
$rbacAuthenticationSource->saveData($authenticationSourceData);
}
return [
'status' => 'OK',
'success' => true
];
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
/**
* Saves department synchronization settings for an authentication source
*
* This method processes a list of LDAP organizational unit distinguished names
* to synchronize with the system. It creates new departments or updates existing
* ones while maintaining the hierarchical structure.
*
* @param string $departmentsDN Pipe-separated list of URL-encoded department distinguished names
* @param string $authSourceUid The unique identifier of the authentication source
*
* @return array Response array containing:
* - success: boolean indicating operation success
* - status: operation status ('OK' if successful)
* - message: error message if operation fails
*/
public function saveDepartments($departmentsDN, $authSourceUid) {
try {
$depsToCheck = ($departmentsDN != '') ? explode('|', $departmentsDN) : [];
$depsToCheck = array_map('urldecode', $depsToCheck);
$depsToUncheck = $this->getDepartmentsToUncheck($depsToCheck);
$filters = ['conditions' => ['AUTH_SOURCE_UID'=> $authSourceUid]];
$rbacAuthenticationSource = new RbacAuthenticationSource();
$authSourceReturn = $rbacAuthenticationSource->show($filters);
if (empty($authSourceReturn['data']) || !isset($authSourceReturn['data'][0])) {
throw new Exception('Authentication source not found');
}
$authenticationSourceData = $authSourceReturn['data'][0];
$authenticationSourceData['AUTH_SOURCE_DATA'] = json_decode($authenticationSourceData['AUTH_SOURCE_DATA'], true);
$ldapSource = new LdapSource();
$ldapSource->authSourceUid = $authSourceUid;
$department = new Department();
foreach ($depsToCheck as $departmentDn) {
$departmentUid = $department->getDepUidIfExistsDN($departmentDn);
$departmentUid = $departmentUid['data'][0]['DEP_UID'] ?? '';
if ($departmentUid == '') {
if (strcasecmp($departmentDn, $authenticationSourceData['AUTH_SOURCE_BASE_DN']) == 0) {
$departmentTitle = 'ROOT (' . $authenticationSourceData['AUTH_SOURCE_BASE_DN'] . ')';
$parentUid = '';
} else {
$ous = $ldapSource->custom_ldap_explode_dn($departmentDn);
$departmentCurrent = array_shift($ous);
$parentDn = implode(',', $ous);
$ous = explode('=', $departmentCurrent);
$departmentTitle = trim($ous[1]);
$parentUid = $department->getDepUidIfExistsDN($parentDn);
$parentUid = $parentUid['data'][0]['DEP_UID'] ?? '';
if (str_ireplace($authenticationSourceData['AUTH_SOURCE_BASE_DN'], '', $parentDn) != '' && $parentUid == '') {
$errorMessage = G::LoadTranslation(
'ID_DEPARTMENT_CHECK_PARENT_DEPARTMENT',
[$parentDn, $departmentTitle]
);
throw new Exception($errorMessage);
}
}
$filters = array(
'conditions' => ['DEP_STATUS' => 'ACTIVE', 'DEP_TITLE' => $departmentTitle]
);
$allDepartments = $department->show($filters);
$departmentUid = '';
if (!empty($allDepartments['data']) && isset($allDepartments['data'][0]['DEP_UID'])) {
$departmentUid = $allDepartments['data'][0]['DEP_UID'];
}
if (empty($departmentUid)) {
$data = [
'DEP_TITLE' => stripslashes($departmentTitle),
'DEP_PARENT' => $parentUid,
'DEP_LDAP_DN' => $departmentDn,
'DEP_REF_CODE' => ''
];
$saveDepartment = $department->saveData($data);
if (empty($saveDepartment)) {
throw new Exception(G::LoadTranslation('ID_DEPARTMENT_ERROR_CREATE'));
}
} else {
if (!empty($allDepartments['data'])) {
$data = $allDepartments['data'][0];
$data['DEP_LDAP_DN'] = $departmentDn;
$department->saveData($data);
}
}
}
}
if (count($depsToUncheck) > 0) {
$baseDnLength = strlen($authenticationSourceData['AUTH_SOURCE_BASE_DN']);
foreach ($depsToUncheck as $departmentDn) {
$departmentUid = $department->getDepUidIfExistsDN($departmentDn);
$departmentUid = $departmentUid['data'][0]['DEP_UID'] ?? '';
if ($departmentUid != '' &&
strcasecmp(
substr($departmentDn, strlen($departmentDn) - $baseDnLength),
$authenticationSourceData['AUTH_SOURCE_BASE_DN']
) == 0
) {
$filters = array(
'conditions' => ['DEP_UID' => $departmentUid]
);
$allDepartments = $department->show($filters);
$data = $allDepartments['data'][0] ?? [];
$data['DEP_LDAP_DN'] = '';
$department->saveData($data);
if (!isset($authenticationSourceData['AUTH_SOURCE_DATA']['DEPARTMENTS_TO_UNASSIGN'])) {
$authenticationSourceData['AUTH_SOURCE_DATA']['DEPARTMENTS_TO_UNASSIGN'] = [];
}
if (!in_array($departmentUid, $authenticationSourceData['AUTH_SOURCE_DATA']['DEPARTMENTS_TO_UNASSIGN'])) {
$authenticationSourceData['AUTH_SOURCE_DATA']['DEPARTMENTS_TO_UNASSIGN'][] = $departmentUid;
}
}
}
$authenticationSourceData['AUTH_SOURCE_DATA'] = json_encode($authenticationSourceData['AUTH_SOURCE_DATA']);
$rbacAuthenticationSource->saveData($authenticationSourceData);
}
return [
'status' => 'OK',
'success' => true
];
} catch (Exception $exception) {
return ['success' => false, 'message' => $exception->getMessage()];
}
}
private function verifyEditAuthSourceData($authSourceData) {
try {
if (!empty($authSourceData['AUTH_SOURCE_UID'])) {
if (empty($authSourceData['AUTH_SOURCE_PASSWORD'])) {
$filters = [
'fields' => ['AUTH_SOURCE_PASSWORD'],
'conditions' => ['AUTH_SOURCE_UID'=> $authSourceData['AUTH_SOURCE_UID']]
];
$rbacAuthenticationSource = new RbacAuthenticationSource();
$authSourceReturn = $rbacAuthenticationSource->show($filters);
if (!empty($authSourceReturn['data']) && !empty($authSourceReturn['data'][0]['AUTH_SOURCE_PASSWORD'])) {
$authSourceData['AUTH_SOURCE_PASSWORD'] = G::decrypt($authSourceReturn['data'][0]['AUTH_SOURCE_PASSWORD'], URL_KEY);
}
}
}
return $authSourceData;
} catch (Exception $exception) {
return [];
}
}
/**
* Filters and organizes departments based on parent-child relationships
*
* This private method processes a flat array of departments and returns
* only those that are direct children of the specified parent, excluding
* terminated organizational units. Returns empty array on error.
*
* @param array $departments Complete list of department data from LDAP
* @param string $parent Parent department distinguished name
* @param string $terminatedOu Name of the terminated organizational unit to exclude
*
* @return array Array of department objects that are children of the specified parent,
* empty array on error
*/
private function getDepartments($departments, $parent, $terminatedOu)
{
try {
$currentDepartments = array();
// Create hash map for O(1) parent-child lookups
$parentChildMap = [];
foreach ($departments as $dept) {
$parentKey = strtolower($dept['parent']);
if (!isset($parentChildMap[$parentKey])) {
$parentChildMap[$parentKey] = [];
}
$parentChildMap[$parentKey][] = $dept;
}
foreach ($departments as $key => $val) {
if (strtolower($val['dn']) != strtolower($parent)) {
if ((strtolower($val['parent']) == strtolower($parent)) && (strtolower($val['ou']) != strtolower($terminatedOu))) {
$node = array();
$node['DEP_UID'] = $val['ou'];
$node['DEP_TITLE'] = $val['ou'];
$node['DEP_USERS'] = $val['users'];
$node['DEP_DN'] = $val['dn'];
// Use hash map for O(1) lookup instead of nested loop
$valDnKey = strtolower($val['dn']);
$node['HAS_CHILDREN'] = isset($parentChildMap[$valDnKey]);
$node['DEP_LAST'] = false;
$currentDepartments[] = $node;
}
}
}
$lastIndex = count($currentDepartments) - 1;
if (isset($currentDepartments[$lastIndex])) {
$currentDepartments[$lastIndex]['DEP_LAST'] = true;
}
return $currentDepartments;
} catch (Exception $exception) {
return [];
}
}
/**
* Recursively builds a hierarchical tree structure of departments
*
* This private method creates a tree representation of departments with
* user count information and synchronization status. It includes recursion
* depth protection to prevent infinite loops. Returns empty array on error.
*
* @param array $departments Complete list of department data from LDAP
* @param string $parent Parent department distinguished name (empty for root level)
* @param array $listDepartmentsNumberUsers Associative array of department UIDs and user counts
* @param string $terminatedOu Name of the terminated organizational unit to exclude
* @param int $depth Current recursion depth (default: 0, max: 50)
*
* @return TreeNodeAuthSource[] Array of TreeNodeAuthSource objects representing the department hierarchy,
* empty array on error or max depth reached
*/
private function getChildrenDepartments($departments, $parent, $listDepartmentsNumberUsers, $terminatedOu, $depth = 0) {
try {
if ($depth >= 50) {
return [];
}
$allDepartments = $this->getDepartments($departments, $parent, $terminatedOu);
$department = new Department();
$departmentsObjects = [];
foreach ($allDepartments as $departmentData) {
$departmentObject = new TreeNodeAuthSource();
$departmentObject->text = htmlentities($departmentData['DEP_TITLE'], ENT_QUOTES, 'UTF-8');
$departmentDNData = $department->getDepUidIfExistsDN($departmentData['DEP_DN']);
$departmentUid = $departmentDNData['data'][0]['DEP_UID'] ?? '';
if ($departmentUid != '') {
$departmentObject->text .= ' (' . ($listDepartmentsNumberUsers[$departmentUid] ?? '0') . ')';
$departmentObject->checked = true;
} else {
$departmentObject->checked = false;
}
if ((int)$departmentData['HAS_CHILDREN'] == 1) {
$departmentObject->children = $this->getChildrenDepartments($departments, $departmentData['DEP_DN'], $listDepartmentsNumberUsers, $terminatedOu, $depth + 1);
}
$departmentObject->id = urlencode($departmentData['DEP_DN']);
$departmentsObjects[] = $departmentObject;
}
return $departmentsObjects;
} catch (Exception $exception) {
return [];
}
}
/**
* Identifies departments that should be unsynchronized
*
* This private method compares currently synchronized departments with
* the new list of departments to synchronize, returning those that should
* be removed from synchronization. Returns empty array on error.
*
* @param array $depsToCheck Array of department distinguished names to keep synchronized
*
* @return array Array of department distinguished names to unsynchronize,
* empty array on error
*/
private function getDepartmentsToUncheck($depsToCheck)
{
try {
$department = new Department();
$departmentsWithDN = $department->getDepartmentsWithDN();
$departmentsWithDN = $departmentsWithDN['data'] ?? [];
$depsToUncheck = [];
foreach ($departmentsWithDN as $departmentWithDN) {
if (!in_array($departmentWithDN['DEP_LDAP_DN'], $depsToCheck)) {
$depsToUncheck[] = $departmentWithDN['DEP_LDAP_DN'];
}
}
return $depsToUncheck;
} catch (Exception $exception) {
return [];
}
}
/**
* Identifies groups that should be unsynchronized
*
* This private method compares currently synchronized groups with
* the new list of groups to synchronize, returning those that should
* be removed from synchronization. Returns empty array on error.
*
* @param array $groupsToCheck Array of group distinguished names to keep synchronized
*
* @return array Array of group distinguished names to unsynchronize,
* empty array on error
*/
private function getGroupsToUncheck($groupsToCheck)
{
try {
$groupsWithDN = $this->getGroupsWithDN();
if (!is_array($groupsWithDN)) {
return [];
}
$groupsToUncheck = [];
foreach ($groupsWithDN as $groupWithDN) {
if (!in_array($groupWithDN['GRP_LDAP_DN'], $groupsToCheck)) {
$groupsToUncheck[] = $groupWithDN['GRP_LDAP_DN'];
}
}
return $groupsToUncheck;
} catch (Exception $e) {
return [];
}
}
/**
* Retrieves all groups that have LDAP distinguished names
*
* This private method fetches all system groups that are currently
* synchronized with LDAP (have non-empty LDAP DN values).
* Uses a high limit (100,000) which may cause performance issues with large datasets.
*
* @return array Array of group data with LDAP distinguished names,
* empty array on error
*
* @warning High limit of 100,000 records may cause memory issues
*/
private function getGroupsWithDN()
{
try {
$groupwf = new Groupwf();
$filters = [
'start' => 0, 'limit' => 100000,
'conditions' => ['GRP_LDAP_DN', '!=', '']
];
$allGroups = $groupwf->show($filters);
return $allGroups['data'] ?? [];
} catch (Exception $exception) {
return [];
}
}
}
/**
* TreeNodeAuthSource Class
*
* This class represents a node in a tree structure used for displaying
* authentication source hierarchical data (groups and departments).
* It extends stdClass to provide a flexible object structure for tree rendering.
*
* @package Lurana\Workflow\Engine\Classes
*/
class TreeNodeAuthSource extends stdClass
{
/** @var string Display text for the tree node */
public $text = '';
/** @var string CSS class for styling the tree node */
public $cls = '';
/** @var bool Indicates if this is a leaf node (no children) */
public $leaf = false;
/** @var bool Indicates if this node is selected/checked */
public $checked = false;
/** @var array Array of child TreeNodeAuthSource objects */
public $children = array();
/** @var string Unique identifier for the tree node */
public $id = '';
}