Page MenuHomePhorge

No OneTemporary

diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php
index 3ad7fc32..63572d95 100644
--- a/src/app/Backends/LDAP.php
+++ b/src/app/Backends/LDAP.php
@@ -1,1416 +1,1414 @@
<?php
namespace App\Backends;
use App\Domain;
use App\Group;
use App\Resource;
use App\SharedFolder;
use App\User;
class LDAP
{
/** @const array Group settings used by the backend */
public const GROUP_SETTINGS = [
'sender_policy',
];
/** @const array Resource settings used by the backend */
public const RESOURCE_SETTINGS = [
'folder',
'invitation_policy',
];
/** @const array Shared folder settings used by the backend */
public const SHARED_FOLDER_SETTINGS = [
'folder',
'acl',
];
/** @const array User settings used by the backend */
public const USER_SETTINGS = [
'first_name',
'last_name',
'organization',
];
/** @var ?\Net_LDAP3 LDAP connection object */
protected static $ldap;
/**
* Starts a new LDAP connection that will be used by all methods
* until you call self::disconnect() explicitely. Normally every
* method uses a separate connection.
*
* @throws \Exception
*/
public static function connect(): void
{
if (empty(self::$ldap)) {
$config = self::getConfig('admin');
self::$ldap = self::initLDAP($config);
}
}
/**
* Close the connection created by self::connect()
*/
public static function disconnect(): void
{
if (!empty(self::$ldap)) {
self::$ldap->close();
self::$ldap = null;
}
}
/**
* Validates that ldap is available as configured.
*
* @throws \Exception
*/
public static function healthcheck(): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$mgmtRootDN = \config('ldap.admin.root_dn');
$hostedRootDN = \config('ldap.hosted.root_dn');
$result = $ldap->search($mgmtRootDN, '', 'base');
if (!$result || $result->count() != 1) {
self::throwException($ldap, "Failed to find the configured management domain $mgmtRootDN");
}
$result = $ldap->search($hostedRootDN, '', 'base');
if (!$result || $result->count() != 1) {
self::throwException($ldap, "Failed to find the configured hosted domain $hostedRootDN");
}
}
/**
* Create a domain in LDAP.
*
* @param \App\Domain $domain The domain to create.
*
* @throws \Exception
*/
public static function createDomain(Domain $domain): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$mgmtRootDN = \config('ldap.admin.root_dn');
$hostedRootDN = \config('ldap.hosted.root_dn');
$domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}";
$aci = [
'(targetattr = "*")'
. '(version 3.0; acl "Deny Unauthorized"; deny (all)'
. '(userdn != "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN
. ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)") '
. 'AND NOT roledn = "ldap:///cn=kolab-admin,' . $mgmtRootDN . '";)',
'(targetattr != "userPassword")'
. '(version 3.0;acl "Search Access";allow (read,compare,search)'
. '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN
. ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)");)',
'(targetattr = "*")'
. '(version 3.0;acl "Kolab Administrators";allow (all)'
. '(roledn = "ldap:///cn=kolab-admin,' . $domainBaseDN
. ' || ldap:///cn=kolab-admin,' . $mgmtRootDN . '");)'
];
$entry = [
'aci' => $aci,
'associateddomain' => $domain->namespace,
'inetdomainbasedn' => $domainBaseDN,
'objectclass' => [
'top',
'domainrelatedobject',
'inetdomain'
],
];
$dn = "associateddomain={$domain->namespace},{$config['domain_base_dn']}";
self::setDomainAttributes($domain, $entry);
if (!$ldap->get_entry($dn)) {
self::addEntry(
$ldap,
$dn,
$entry,
"Failed to create domain {$domain->namespace} in LDAP (" . __LINE__ . ")"
);
}
// create ou, roles, ous
$entry = [
'description' => $domain->namespace,
'objectclass' => [
'top',
'organizationalunit'
],
'ou' => $domain->namespace,
];
$entry['aci'] = array(
'(targetattr = "*")'
. '(version 3.0;acl "Deny Unauthorized"; deny (all)'
. '(userdn != "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN
. ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)") '
. 'AND NOT roledn = "ldap:///cn=kolab-admin,' . $mgmtRootDN . '";)',
'(targetattr != "userPassword")'
. '(version 3.0;acl "Search Access";allow (read,compare,search,write)'
. '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN
. ' || ldap:///ou=People,' . $domainBaseDN . '??sub?(objectclass=inetorgperson)");)',
'(targetattr = "*")'
. '(version 3.0;acl "Kolab Administrators";allow (all)'
. '(roledn = "ldap:///cn=kolab-admin,' . $domainBaseDN
. ' || ldap:///cn=kolab-admin,' . $mgmtRootDN . '");)',
'(target = "ldap:///ou=*,' . $domainBaseDN . '")'
. '(targetattr="objectclass || aci || ou")'
. '(version 3.0;acl "Allow Domain sub-OU Registration"; allow (add)'
. '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)',
'(target = "ldap:///uid=*,ou=People,' . $domainBaseDN . '")(targetattr="*")'
. '(version 3.0;acl "Allow Domain First User Registration"; allow (add)'
. '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)',
'(target = "ldap:///cn=*,' . $domainBaseDN . '")(targetattr="objectclass || cn")'
. '(version 3.0;acl "Allow Domain Role Registration"; allow (add)'
. '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)',
);
if (!$ldap->get_entry($domainBaseDN)) {
self::addEntry(
$ldap,
$domainBaseDN,
$entry,
"Failed to create domain {$domain->namespace} in LDAP (" . __LINE__ . ")"
);
}
foreach (['Groups', 'People', 'Resources', 'Shared Folders'] as $item) {
$itemDN = "ou={$item},{$domainBaseDN}";
if (!$ldap->get_entry($itemDN)) {
$itemEntry = [
'ou' => $item,
'description' => $item,
'objectclass' => [
'top',
'organizationalunit'
]
];
self::addEntry(
$ldap,
$itemDN,
$itemEntry,
"Failed to create domain {$domain->namespace} in LDAP (" . __LINE__ . ")"
);
}
}
foreach (['kolab-admin'] as $item) {
$itemDN = "cn={$item},{$domainBaseDN}";
if (!$ldap->get_entry($itemDN)) {
$itemEntry = [
'cn' => $item,
'description' => "{$item} role",
'objectclass' => [
'top',
'ldapsubentry',
'nsmanagedroledefinition',
'nsroledefinition',
'nssimpleroledefinition'
]
];
self::addEntry(
$ldap,
$itemDN,
$itemEntry,
"Failed to create domain {$domain->namespace} in LDAP (" . __LINE__ . ")"
);
}
}
// TODO: Assign kolab-admin role to the owner?
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Create a group in LDAP.
*
* @param \App\Group $group The group to create.
*
* @throws \Exception
*/
public static function createGroup(Group $group): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$domainName = explode('@', $group->email, 2)[1];
$cn = $ldap->quote_string($group->name);
$dn = "cn={$cn}," . self::baseDN($ldap, $domainName, 'Groups');
$entry = [
'mail' => $group->email,
'objectclass' => [
'top',
'groupofuniquenames',
'kolabgroupofuniquenames'
],
];
if (!self::getGroupEntry($ldap, $group->email)) {
self::setGroupAttributes($ldap, $group, $entry);
self::addEntry(
$ldap,
$dn,
$entry,
"Failed to create group {$group->email} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Create a resource in LDAP.
*
* @param \App\Resource $resource The resource to create.
*
* @throws \Exception
*/
public static function createResource(Resource $resource): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$domainName = explode('@', $resource->email, 2)[1];
$cn = $ldap->quote_string($resource->name);
$dn = "cn={$cn}," . self::baseDN($ldap, $domainName, 'Resources');
$entry = [
'mail' => $resource->email,
'objectclass' => [
'top',
'kolabresource',
'kolabsharedfolder',
'mailrecipient',
],
'kolabfoldertype' => 'event',
];
if (!self::getResourceEntry($ldap, $resource->email)) {
self::setResourceAttributes($ldap, $resource, $entry);
self::addEntry(
$ldap,
$dn,
$entry,
"Failed to create resource {$resource->email} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Create a shared folder in LDAP.
*
* @param \App\SharedFolder $folder The shared folder to create.
*
* @throws \Exception
*/
public static function createSharedFolder(SharedFolder $folder): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$domainName = explode('@', $folder->email, 2)[1];
$cn = $ldap->quote_string($folder->name);
$dn = "cn={$cn}," . self::baseDN($ldap, $domainName, 'Shared Folders');
$entry = [
'mail' => $folder->email,
'objectclass' => [
'top',
'kolabsharedfolder',
'mailrecipient',
],
];
if (!self::getSharedFolderEntry($ldap, $folder->email)) {
self::setSharedFolderAttributes($ldap, $folder, $entry);
self::addEntry(
$ldap,
$dn,
$entry,
"Failed to create shared folder {$folder->id} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Create a user in LDAP.
*
* Only need to add user if in any of the local domains? Figure that out here for now. Should
* have Context-Based Access Controls before the job is queued though, probably.
*
* Use one of three modes;
*
* 1) The authenticated user account.
*
* * Only valid if the authenticated user is a domain admin.
* * We don't know the originating user here.
* * We certainly don't have its password anymore.
*
* 2) The hosted kolab account.
*
* 3) The Directory Manager account.
*
* @param \App\User $user The user account to create.
*
* @throws \Exception
*/
public static function createUser(User $user): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$entry = [
'objectclass' => [
'top',
'inetorgperson',
'inetuser',
'kolabinetorgperson',
'mailrecipient',
'person'
],
'mail' => $user->email,
'uid' => $user->email,
'nsroledn' => []
];
if (!self::getUserEntry($ldap, $user->email, $dn)) {
if (empty($dn)) {
self::throwException($ldap, "Failed to create user {$user->email} in LDAP (" . __LINE__ . ")");
}
self::setUserAttributes($user, $entry);
self::addEntry(
$ldap,
$dn,
$entry,
"Failed to create user {$user->email} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Delete a domain from LDAP.
*
* @param \App\Domain $domain The domain to delete
*
* @throws \Exception
*/
public static function deleteDomain(Domain $domain): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$domainBaseDN = self::baseDN($ldap, $domain->namespace);
if ($ldap->get_entry($domainBaseDN)) {
$result = $ldap->delete_entry_recursive($domainBaseDN);
if (!$result) {
self::throwException(
$ldap,
"Failed to delete domain {$domain->namespace} from LDAP (" . __LINE__ . ")"
);
}
}
if ($ldap_domain = $ldap->find_domain($domain->namespace)) {
if ($ldap->get_entry($ldap_domain['dn'])) {
$result = $ldap->delete_entry($ldap_domain['dn']);
if (!$result) {
self::throwException(
$ldap,
"Failed to delete domain {$domain->namespace} from LDAP (" . __LINE__ . ")"
);
}
}
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Delete a group from LDAP.
*
* @param \App\Group $group The group to delete.
*
* @throws \Exception
*/
public static function deleteGroup(Group $group): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
if (self::getGroupEntry($ldap, $group->email, $dn)) {
$result = $ldap->delete_entry($dn);
if (!$result) {
self::throwException(
$ldap,
"Failed to delete group {$group->email} from LDAP (" . __LINE__ . ")"
);
}
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Delete a resource from LDAP.
*
* @param \App\Resource $resource The resource to delete.
*
* @throws \Exception
*/
public static function deleteResource(Resource $resource): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
if (self::getResourceEntry($ldap, $resource->email, $dn)) {
$result = $ldap->delete_entry($dn);
if (!$result) {
self::throwException(
$ldap,
"Failed to delete resource {$resource->email} from LDAP (" . __LINE__ . ")"
);
}
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Delete a shared folder from LDAP.
*
* @param \App\SharedFolder $folder The shared folder to delete.
*
* @throws \Exception
*/
public static function deleteSharedFolder(SharedFolder $folder): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
if (self::getSharedFolderEntry($ldap, $folder->email, $dn)) {
$result = $ldap->delete_entry($dn);
if (!$result) {
self::throwException(
$ldap,
"Failed to delete shared folder {$folder->id} from LDAP (" . __LINE__ . ")"
);
}
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Delete a user from LDAP.
*
* @param \App\User $user The user account to delete.
*
* @throws \Exception
*/
public static function deleteUser(User $user): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
if (self::getUserEntry($ldap, $user->email, $dn)) {
$result = $ldap->delete_entry($dn);
if (!$result) {
self::throwException(
$ldap,
"Failed to delete user {$user->email} from LDAP (" . __LINE__ . ")"
);
}
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Get a domain data from LDAP.
*
* @param string $namespace The domain name
*
* @return array|false|null
* @throws \Exception
*/
public static function getDomain(string $namespace)
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$ldapDomain = $ldap->find_domain($namespace);
if ($ldapDomain) {
$domain = $ldap->get_entry($ldapDomain['dn']);
}
if (empty(self::$ldap)) {
$ldap->close();
}
return $domain ?? null;
}
/**
* Get a group data from LDAP.
*
* @param string $email The group email.
*
* @return array|false|null
* @throws \Exception
*/
public static function getGroup(string $email)
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$group = self::getGroupEntry($ldap, $email, $dn);
if (empty(self::$ldap)) {
$ldap->close();
}
return $group;
}
/**
* Get a resource data from LDAP.
*
* @param string $email The resource email.
*
* @return array|false|null
* @throws \Exception
*/
public static function getResource(string $email)
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$resource = self::getResourceEntry($ldap, $email, $dn);
if (empty(self::$ldap)) {
$ldap->close();
}
return $resource;
}
/**
* Get a shared folder data from LDAP.
*
* @param string $email The resource email.
*
* @return array|false|null
* @throws \Exception
*/
public static function getSharedFolder(string $email)
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$folder = self::getSharedFolderEntry($ldap, $email, $dn);
if (empty(self::$ldap)) {
$ldap->close();
}
return $folder;
}
/**
* Get a user data from LDAP.
*
* @param string $email The user email.
*
* @return array|false|null
* @throws \Exception
*/
public static function getUser(string $email)
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$user = self::getUserEntry($ldap, $email, $dn, true);
if (empty(self::$ldap)) {
$ldap->close();
}
return $user;
}
/**
* Update a domain in LDAP.
*
* @param \App\Domain $domain The domain to update.
*
* @throws \Exception
*/
public static function updateDomain(Domain $domain): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$ldapDomain = $ldap->find_domain($domain->namespace);
if (!$ldapDomain) {
self::throwException(
$ldap,
"Failed to update domain {$domain->namespace} in LDAP (domain not found)"
);
}
$oldEntry = $ldap->get_entry($ldapDomain['dn']);
$newEntry = $oldEntry;
self::setDomainAttributes($domain, $newEntry);
if (array_key_exists('inetdomainstatus', $newEntry)) {
$newEntry['inetdomainstatus'] = (string) $newEntry['inetdomainstatus'];
}
$result = $ldap->modify_entry($ldapDomain['dn'], $oldEntry, $newEntry);
if (!is_array($result)) {
self::throwException(
$ldap,
"Failed to update domain {$domain->namespace} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Update a group in LDAP.
*
* @param \App\Group $group The group to update
*
* @throws \Exception
*/
public static function updateGroup(Group $group): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$newEntry = $oldEntry = self::getGroupEntry($ldap, $group->email, $dn);
if (empty($oldEntry)) {
self::throwException(
$ldap,
"Failed to update group {$group->email} in LDAP (group not found)"
);
}
self::setGroupAttributes($ldap, $group, $newEntry);
$result = $ldap->modify_entry($dn, $oldEntry, $newEntry);
if (!is_array($result)) {
self::throwException(
$ldap,
"Failed to update group {$group->email} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Update a resource in LDAP.
*
* @param \App\Resource $resource The resource to update
*
* @throws \Exception
*/
public static function updateResource(Resource $resource): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$newEntry = $oldEntry = self::getResourceEntry($ldap, $resource->email, $dn);
if (empty($oldEntry)) {
self::throwException(
$ldap,
"Failed to update resource {$resource->email} in LDAP (resource not found)"
);
}
self::setResourceAttributes($ldap, $resource, $newEntry);
$result = $ldap->modify_entry($dn, $oldEntry, $newEntry);
if (!is_array($result)) {
self::throwException(
$ldap,
"Failed to update resource {$resource->email} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Update a shared folder in LDAP.
*
* @param \App\SharedFolder $folder The shared folder to update
*
* @throws \Exception
*/
public static function updateSharedFolder(SharedFolder $folder): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$newEntry = $oldEntry = self::getSharedFolderEntry($ldap, $folder->email, $dn);
if (empty($oldEntry)) {
self::throwException(
$ldap,
"Failed to update shared folder {$folder->id} in LDAP (folder not found)"
);
}
self::setSharedFolderAttributes($ldap, $folder, $newEntry);
$result = $ldap->modify_entry($dn, $oldEntry, $newEntry);
if (!is_array($result)) {
self::throwException(
$ldap,
"Failed to update shared folder {$folder->id} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Update a user in LDAP.
*
* @param \App\User $user The user account to update.
*
* @throws \Exception
*/
public static function updateUser(User $user): void
{
$config = self::getConfig('admin');
$ldap = self::initLDAP($config);
$newEntry = $oldEntry = self::getUserEntry($ldap, $user->email, $dn, true);
if (!$oldEntry) {
self::throwException(
$ldap,
"Failed to update user {$user->email} in LDAP (user not found)"
);
}
self::setUserAttributes($user, $newEntry);
if (array_key_exists('objectclass', $newEntry)) {
if (!in_array('inetuser', $newEntry['objectclass'])) {
$newEntry['objectclass'][] = 'inetuser';
}
}
if (array_key_exists('inetuserstatus', $newEntry)) {
$newEntry['inetuserstatus'] = (string) $newEntry['inetuserstatus'];
}
if (array_key_exists('mailquota', $newEntry)) {
$newEntry['mailquota'] = (string) $newEntry['mailquota'];
}
$result = $ldap->modify_entry($dn, $oldEntry, $newEntry);
if (!is_array($result)) {
self::throwException(
$ldap,
"Failed to update user {$user->email} in LDAP (" . __LINE__ . ")"
);
}
if (empty(self::$ldap)) {
$ldap->close();
}
}
/**
* Initialize connection to LDAP
*/
private static function initLDAP(array $config, string $privilege = 'admin')
{
if (self::$ldap) {
return self::$ldap;
}
$ldap = new \Net_LDAP3($config);
$connected = $ldap->connect();
if (!$connected) {
throw new \Exception("Failed to connect to LDAP");
}
$bound = $ldap->bind(
\config("ldap.{$privilege}.bind_dn"),
\config("ldap.{$privilege}.bind_pw")
);
if (!$bound) {
throw new \Exception("Failed to bind to LDAP");
}
return $ldap;
}
/**
* Set domain attributes
*/
private static function setDomainAttributes(Domain $domain, array &$entry)
{
$entry['inetdomainstatus'] = $domain->status;
}
/**
* Convert group member addresses in to valid entries.
*/
private static function setGroupAttributes($ldap, Group $group, &$entry)
{
$settings = $group->getSettings(['sender_policy']);
$entry['kolaballowsmtpsender'] = json_decode($settings['sender_policy'] ?: '[]', true);
$entry['cn'] = $group->name;
$entry['uniquemember'] = [];
$groupDomain = explode('@', $group->email, 2)[1];
$domainBaseDN = self::baseDN($ldap, $groupDomain);
$validMembers = [];
foreach ($group->members as $member) {
list($local, $domainName) = explode('@', $member);
$memberDN = "uid={$member},ou=People,{$domainBaseDN}";
$memberEntry = $ldap->get_entry($memberDN);
// if the member is in the local domain but doesn't exist, drop it
if ($domainName == $groupDomain && !$memberEntry) {
continue;
}
// add the member if not in the local domain
if (!$memberEntry) {
$memberEntry = [
'cn' => $member,
'mail' => $member,
'objectclass' => [
'top',
'inetorgperson',
'organizationalperson',
'person'
],
'sn' => 'unknown'
];
$ldap->add_entry($memberDN, $memberEntry);
}
$entry['uniquemember'][] = $memberDN;
$validMembers[] = $member;
}
// Update members in sql (some might have been removed),
// skip model events to not invoke another update job
if ($group->members !== $validMembers) {
$group->members = $validMembers;
- Group::withoutEvents(function () use ($group) {
- $group->save();
- });
+ $group->saveQuietly();
}
}
/**
* Set common resource attributes
*/
private static function setResourceAttributes($ldap, Resource $resource, &$entry)
{
$entry['cn'] = $resource->name;
$entry['owner'] = null;
$entry['kolabinvitationpolicy'] = null;
$entry['acl'] = '';
$settings = $resource->getSettings(['invitation_policy', 'folder']);
$entry['kolabtargetfolder'] = $settings['folder'] ?? '';
// Here's how Wallace's resources module works:
// - if policy is ACT_MANUAL and owner mail specified: a tentative response is sent, event saved,
// and mail sent to the owner to accept/decline the request.
// - if policy is ACT_ACCEPT_AND_NOTIFY and owner mail specified: an accept response is sent,
// event saved, and notification (not confirmation) mail sent to the owner.
// - if there's no owner (policy irrelevant): an accept response is sent, event saved.
// - if policy is ACT_REJECT: a decline response is sent
// - note that the notification email is being send if COND_NOTIFY policy is set or saving failed.
// - all above assume there's no conflict, if there's a conflict the decline response is sent automatically
// (notification is sent if policy = ACT_ACCEPT_AND_NOTIFY).
// - the only supported policies are: 'ACT_MANUAL', 'ACT_ACCEPT' (defined but not used anywhere),
// 'ACT_REJECT', 'ACT_ACCEPT_AND_NOTIFY'.
// For now we ignore the notifications feature
if (!empty($settings['invitation_policy'])) {
if ($settings['invitation_policy'] === 'accept') {
$entry['kolabinvitationpolicy'] = 'ACT_ACCEPT';
} elseif ($settings['invitation_policy'] === 'reject') {
$entry['kolabinvitationpolicy'] = 'ACT_REJECT';
} elseif (preg_match('/^manual:(\S+@\S+)$/', $settings['invitation_policy'], $m)) {
if (self::getUserEntry($ldap, $m[1], $userDN)) {
$entry['owner'] = $userDN;
$entry['acl'] = $m[1] . ', full';
$entry['kolabinvitationpolicy'] = 'ACT_MANUAL';
} else {
$entry['kolabinvitationpolicy'] = 'ACT_ACCEPT';
}
}
}
}
/**
* Set common shared folder attributes
*/
private static function setSharedFolderAttributes($ldap, SharedFolder $folder, &$entry)
{
$settings = $folder->getSettings(['acl', 'folder']);
$entry['cn'] = $folder->name;
$entry['kolabfoldertype'] = $folder->type;
$entry['kolabtargetfolder'] = $settings['folder'] ?? '';
$entry['acl'] = !empty($settings['acl']) ? json_decode($settings['acl'], true) : '';
$entry['alias'] = $folder->aliases()->pluck('alias')->all();
}
/**
* Set common user attributes
*/
private static function setUserAttributes(User $user, array &$entry)
{
$isDegraded = $user->isDegraded(true);
$settings = $user->getSettings(['first_name', 'last_name', 'organization']);
$firstName = $settings['first_name'];
$lastName = $settings['last_name'];
$cn = "unknown";
$displayname = "";
if ($firstName) {
if ($lastName) {
$cn = "{$firstName} {$lastName}";
$displayname = "{$lastName}, {$firstName}";
} else {
$lastName = "unknown";
$cn = "{$firstName}";
$displayname = "{$firstName}";
}
} else {
$firstName = "";
if ($lastName) {
$cn = "{$lastName}";
$displayname = "{$lastName}";
} else {
$lastName = "unknown";
}
}
$entry['cn'] = $cn;
$entry['displayname'] = $displayname;
$entry['givenname'] = $firstName;
$entry['sn'] = $lastName;
$entry['userpassword'] = $user->password_ldap;
$entry['inetuserstatus'] = $user->status;
$entry['o'] = $settings['organization'];
$entry['mailquota'] = 0;
$entry['alias'] = $user->aliases()->pluck('alias')->all();
$roles = [];
foreach ($user->entitlements as $entitlement) {
\Log::debug("Examining {$entitlement->sku->title}");
switch ($entitlement->sku->title) {
case "mailbox":
break;
case "storage":
$entry['mailquota'] += 1048576;
break;
default:
$roles[] = $entitlement->sku->title;
break;
}
}
$hostedRootDN = \config('ldap.hosted.root_dn');
$entry['nsroledn'] = [];
if (in_array("2fa", $roles)) {
$entry['nsroledn'][] = "cn=2fa-user,{$hostedRootDN}";
}
if ($isDegraded) {
$entry['nsroledn'][] = "cn=degraded-user,{$hostedRootDN}";
$entry['mailquota'] = \config('app.storage.min_qty') * 1048576;
} else {
if (in_array("activesync", $roles)) {
$entry['nsroledn'][] = "cn=activesync-user,{$hostedRootDN}";
}
if (!in_array("groupware", $roles)) {
$entry['nsroledn'][] = "cn=imap-user,{$hostedRootDN}";
}
}
}
/**
* Get LDAP configuration for specified access level
*/
private static function getConfig(string $privilege)
{
$config = [
'domain_base_dn' => \config('ldap.domain_base_dn'),
'domain_filter' => \config('ldap.domain_filter'),
'domain_name_attribute' => \config('ldap.domain_name_attribute'),
'hosts' => \config('ldap.hosts'),
'sort' => false,
'vlv' => false,
'log_hook' => 'App\Backends\LDAP::logHook',
];
return $config;
}
/**
* Get group entry from LDAP.
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $email Group email (mail)
* @param string $dn Reference to group DN
*
* @return null|array Group entry, NULL if not found
*/
private static function getGroupEntry($ldap, $email, &$dn = null)
{
$domainName = explode('@', $email, 2)[1];
$base_dn = self::baseDN($ldap, $domainName, 'Groups');
$attrs = ['dn', 'cn', 'mail', 'uniquemember', 'objectclass', 'kolaballowsmtpsender'];
// For groups we're using search() instead of get_entry() because
// a group name is not constant, so e.g. on update we might have
// the new name, but not the old one. Email address is constant.
return self::searchEntry($ldap, $base_dn, "(mail=$email)", $attrs, $dn);
}
/**
* Get a resource entry from LDAP.
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $email Resource email (mail)
* @param string $dn Reference to the resource DN
*
* @return null|array Resource entry, NULL if not found
*/
private static function getResourceEntry($ldap, $email, &$dn = null)
{
$domainName = explode('@', $email, 2)[1];
$base_dn = self::baseDN($ldap, $domainName, 'Resources');
$attrs = ['dn', 'cn', 'mail', 'objectclass', 'kolabtargetfolder',
'kolabfoldertype', 'kolabinvitationpolicy', 'owner', 'acl'];
// For resources we're using search() instead of get_entry() because
// a resource name is not constant, so e.g. on update we might have
// the new name, but not the old one. Email address is constant.
return self::searchEntry($ldap, $base_dn, "(mail=$email)", $attrs, $dn);
}
/**
* Get a shared folder entry from LDAP.
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $email Resource email (mail)
* @param string $dn Reference to the shared folder DN
*
* @return null|array Shared folder entry, NULL if not found
*/
private static function getSharedFolderEntry($ldap, $email, &$dn = null)
{
$domainName = explode('@', $email, 2)[1];
$base_dn = self::baseDN($ldap, $domainName, 'Shared Folders');
$attrs = ['dn', 'cn', 'mail', 'objectclass', 'kolabtargetfolder', 'kolabfoldertype', 'acl', 'alias'];
// For shared folders we're using search() instead of get_entry() because
// a folder name is not constant, so e.g. on update we might have
// the new name, but not the old one. Email address is constant.
return self::searchEntry($ldap, $base_dn, "(mail=$email)", $attrs, $dn);
}
/**
* Get user entry from LDAP.
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $email User email (uid)
* @param string $dn Reference to user DN
* @param bool $full Get extra attributes, e.g. nsroledn
*
* @return null|array User entry, NULL if not found
*/
private static function getUserEntry($ldap, $email, &$dn = null, $full = false)
{
$domainName = explode('@', $email, 2)[1];
$dn = "uid={$email}," . self::baseDN($ldap, $domainName, 'People');
$entry = $ldap->get_entry($dn);
if ($entry && $full) {
if (!array_key_exists('nsroledn', $entry)) {
$roles = $ldap->get_entry_attributes($dn, ['nsroledn']);
if (!empty($roles)) {
$entry['nsroledn'] = (array) $roles['nsroledn'];
}
}
}
return $entry ?: null;
}
/**
* Logging callback
*/
public static function logHook($level, $msg): void
{
if (
(
$level == LOG_INFO
|| $level == LOG_DEBUG
|| $level == LOG_NOTICE
)
&& !\config('app.debug')
) {
return;
}
switch ($level) {
case LOG_CRIT:
$function = 'critical';
break;
case LOG_EMERG:
$function = 'emergency';
break;
case LOG_ERR:
$function = 'error';
break;
case LOG_ALERT:
$function = 'alert';
break;
case LOG_WARNING:
$function = 'warning';
break;
case LOG_INFO:
$function = 'info';
break;
case LOG_DEBUG:
$function = 'debug';
break;
case LOG_NOTICE:
$function = 'notice';
break;
default:
$function = 'info';
}
if (is_array($msg)) {
$msg = implode("\n", $msg);
}
$msg = '[LDAP] ' . $msg;
\Log::{$function}($msg);
}
/**
* A wrapper for Net_LDAP3::add_entry() with error handler
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $dn Entry DN
* @param array $entry Entry attributes
* @param ?string $errorMsg A message to throw as an exception on error
*
* @throws \Exception
*/
private static function addEntry($ldap, string $dn, array $entry, $errorMsg = null)
{
// try/catch because Laravel converts warnings into exceptions
// and we want more human-friendly error message than that
try {
$result = $ldap->add_entry($dn, $entry);
} catch (\Exception $e) {
$result = false;
}
if (!$result) {
if (!$errorMsg) {
$errorMsg = "LDAP Error (" . __LINE__ . ")";
}
if (isset($e)) {
$errorMsg .= ": " . $e->getMessage();
}
self::throwException($ldap, $errorMsg);
}
}
/**
* Find a single entry in LDAP by using search.
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $base_dn Base DN
* @param string $filter Search filter
* @param array $attrs Result attributes
* @param string $dn Reference to a DN of the found entry
*
* @return null|array LDAP entry, NULL if not found
*/
private static function searchEntry($ldap, $base_dn, $filter, $attrs, &$dn = null)
{
$result = $ldap->search($base_dn, $filter, 'sub', $attrs);
if ($result && $result->count() == 1) {
$entries = $result->entries(true);
$dn = key($entries);
$entry = $entries[$dn];
$entry['dn'] = $dn;
return $entry;
}
return null;
}
/**
* Throw exception and close the connection when needed
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $message Exception message
*
* @throws \Exception
*/
private static function throwException($ldap, string $message): void
{
if (empty(self::$ldap)) {
$ldap->close();
}
throw new \Exception($message);
}
/**
* Create a base DN string for a specified object.
* Note: It makes sense with an existing domain only.
*
* @param \Net_LDAP3 $ldap Ldap connection
* @param string $domainName Domain namespace
* @param ?string $ouName Optional name of the sub-tree (OU)
*
* @return string Full base DN
*/
private static function baseDN($ldap, string $domainName, string $ouName = null): string
{
$dn = $ldap->domain_root_dn($domainName);
if ($ouName) {
$dn = "ou={$ouName},{$dn}";
}
return $dn;
}
}
diff --git a/src/app/Console/ObjectDeleteCommand.php b/src/app/Console/ObjectDeleteCommand.php
index f24e9042..f9e0d2d5 100644
--- a/src/app/Console/ObjectDeleteCommand.php
+++ b/src/app/Console/ObjectDeleteCommand.php
@@ -1,109 +1,105 @@
<?php
namespace App\Console;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Schema;
/**
* This abstract class provides a means to treat objects in our model using CRUD.
*/
abstract class ObjectDeleteCommand extends ObjectCommand
{
public function __construct()
{
$this->description = "Delete a {$this->objectName}";
$this->signature = sprintf(
"%s%s:delete {%s}",
$this->commandPrefix ? $this->commandPrefix . ":" : "",
$this->objectName,
$this->objectName
);
$class = new $this->objectClass();
try {
foreach (Schema::getColumnListing($class->getTable()) as $column) {
if ($column == "id") {
continue;
}
$this->signature .= " {--{$column}=}";
}
} catch (\Exception $e) {
\Log::error("Could not extract options: {$e->getMessage()}");
}
$classes = class_uses_recursive($this->objectClass);
if (in_array(SoftDeletes::class, $classes)) {
$this->signature .= " {--with-deleted : Consider deleted {$this->objectName}s}";
}
parent::__construct();
}
public function getProperties()
{
if (!empty($this->properties)) {
return $this->properties;
}
$class = new $this->objectClass();
$this->properties = [];
foreach (Schema::getColumnListing($class->getTable()) as $column) {
if ($column == "id") {
continue;
}
if (($value = $this->option($column)) !== null) {
$this->properties[$column] = $value;
}
}
return $this->properties;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$result = parent::handle();
if (!$result) {
return 1;
}
$argument = $this->argument($this->objectName);
$object = $this->getObject($this->objectClass, $argument, $this->objectTitle);
if (!$object) {
$this->error("No such {$this->objectName} {$argument}");
return 1;
}
if ($this->commandPrefix == 'scalpel') {
- $this->objectClass::withoutEvents(
- function () use ($object) {
- if ($object->deleted_at) {
- $object->forceDelete();
- } else {
- $object->delete();
- }
- }
- );
+ if ($object->deleted_at) {
+ $object->forceDeleteQuietly();
+ } else {
+ $object->deleteQuietly();
+ }
} else {
if ($object->deleted_at) {
$object->forceDelete();
} else {
$object->delete();
}
}
}
}
diff --git a/src/app/Console/ObjectUpdateCommand.php b/src/app/Console/ObjectUpdateCommand.php
index 5d764e8d..17f12fef 100644
--- a/src/app/Console/ObjectUpdateCommand.php
+++ b/src/app/Console/ObjectUpdateCommand.php
@@ -1,105 +1,101 @@
<?php
namespace App\Console;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Schema;
/**
* This abstract class provides a means to treat objects in our model using CRUD.
*/
abstract class ObjectUpdateCommand extends ObjectCommand
{
public function __construct()
{
$this->description = "Update a {$this->objectName}";
$this->signature = sprintf(
"%s%s:update {%s}",
$this->commandPrefix ? $this->commandPrefix . ":" : "",
$this->objectName,
$this->objectName
);
$class = new $this->objectClass();
try {
foreach (Schema::getColumnListing($class->getTable()) as $column) {
if ($column == "id") {
continue;
}
$this->signature .= " {--{$column}=}";
}
} catch (\Exception $e) {
\Log::error("Could not extract options: {$e->getMessage()}");
}
$classes = class_uses_recursive($this->objectClass);
if (in_array(SoftDeletes::class, $classes)) {
$this->signature .= " {--with-deleted : Include deleted {$this->objectName}s}";
}
parent::__construct();
}
public function getProperties()
{
if (!empty($this->properties)) {
return $this->properties;
}
$class = new $this->objectClass();
$this->properties = [];
foreach (Schema::getColumnListing($class->getTable()) as $column) {
if ($column == "id") {
continue;
}
if (($value = $this->option($column)) !== null) {
$this->properties[$column] = $value;
}
}
return $this->properties;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$argument = $this->argument($this->objectName);
$object = $this->getObject($this->objectClass, $argument, $this->objectTitle);
if (!$object) {
$this->error("No such {$this->objectName} {$argument}");
return 1;
}
foreach ($this->getProperties() as $property => $value) {
if ($property == "deleted_at" && $value == "null") {
$value = null;
}
$object->{$property} = $value;
}
$object->timestamps = false;
if ($this->commandPrefix == 'scalpel') {
- $this->objectClass::withoutEvents(
- function () use ($object) {
- $object->save();
- }
- );
+ $object->saveQuietly();
} else {
$object->save();
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 23, 10:40 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
80148
Default Alt Text
(49 KB)

Event Timeline