mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-28 12:13:24 +00:00
[Web][Dovecot][SOGo][DockerApi] Add mailbox groups, shared mailboxes and identity provider sync
This commit is contained in:
82 files changed
+6488
-1027
No files matched your search
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Identity\Model;
|
||||
|
||||
/**
|
||||
* A mailbox with the authsource of the configured IdP and its link (table `identity_link`).
|
||||
*/
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @param int $active mailbox.active: 1 active, 2 login disabled (still receives mail), 0 inactive
|
||||
* @param bool $idpDisabled the sync set active = 2, so it may undo it
|
||||
* @param ?string $missingSince first complete run that did not list the user
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $username,
|
||||
public readonly string $name,
|
||||
public readonly int $active,
|
||||
public readonly ?string $externalId = null,
|
||||
public readonly bool $idpDisabled = false,
|
||||
public readonly ?string $missingSince = null,
|
||||
public readonly string $attributeHash = ''
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Identity\Model;
|
||||
|
||||
/**
|
||||
* A selected IdP group with the ids of its member users.
|
||||
*/
|
||||
class DirectoryGroup
|
||||
{
|
||||
/**
|
||||
* @param ?string $mail group address in the IdP, decides the domain of an auto-created group
|
||||
* @param string[] $memberIds ids of the member users (DirectoryUser::$id)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $name,
|
||||
public readonly ?string $mail = null,
|
||||
public array $memberIds = []
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Identity\Model;
|
||||
|
||||
/**
|
||||
* A user as listed by the IdP. `id` is stable across renames (Keycloak user id, LDAP objectGUID/entryUUID/...).
|
||||
*/
|
||||
class DirectoryUser
|
||||
{
|
||||
/**
|
||||
* @param ?string $email lowercase, null if the user has none
|
||||
* @param string[] $templateValues values of the template attribute (legacy mapping)
|
||||
* @param string $dn LDAP only, lowercase, used to resolve group members
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly ?string $email,
|
||||
public readonly string $name,
|
||||
public readonly bool $enabled,
|
||||
public readonly array $templateValues = [],
|
||||
public readonly string $dn = ''
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Identity\Model;
|
||||
|
||||
/**
|
||||
* A complete listing of the IdP. Directories never return partial listings, they throw DirectoryException,
|
||||
* so a user missing here is really gone (or out of the filter).
|
||||
*/
|
||||
class Listing
|
||||
{
|
||||
/**
|
||||
* @param array<string, DirectoryUser> $users by id
|
||||
* @param ?array<string, DirectoryGroup> $groups by id, null when groups were not listed
|
||||
*/
|
||||
public function __construct(public readonly array $users, public readonly ?array $groups = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Identity\Model;
|
||||
|
||||
/**
|
||||
* Identity provider settings (table `identity_provider`, key/value, see identity_provider('get')) with defaults.
|
||||
*/
|
||||
class Settings
|
||||
{
|
||||
public const KEYCLOAK = 'keycloak';
|
||||
public const LDAP = 'ldap';
|
||||
public const GENERIC_OIDC = 'generic-oidc';
|
||||
public const MEMBER_MODES = ['memberof', 'member', 'ad_chain'];
|
||||
|
||||
public string $provider = '';
|
||||
public bool $periodicSync = false;
|
||||
public bool $importUsers = false;
|
||||
/** Minutes between scheduled runs */
|
||||
public int $syncInterval = 15;
|
||||
/** Users disabled or removed in the IdP get login disabled (mailbox.active = 2) */
|
||||
public bool $offboarding = false;
|
||||
/** Max. share (percent) of active IdP mailboxes one run may deactivate, at least 5 mailboxes */
|
||||
public int $offboardingLimit = 10;
|
||||
public bool $groupSync = false;
|
||||
public bool $groupAutocreate = true;
|
||||
public string $groupDefaultDomain = '';
|
||||
public string $defaultTemplate = '';
|
||||
/** @var string[] attribute value per mapping, keys aligned with $templates */
|
||||
public array $mappers = [];
|
||||
/** @var string[] */
|
||||
public array $templates = [];
|
||||
public string $serverUrl = '';
|
||||
public string $realm = '';
|
||||
public string $clientId = '';
|
||||
public string $clientSecret = '';
|
||||
public bool $ignoreSslError = false;
|
||||
/** @var string[] Keycloak group paths: '/path', '/path/*' (subtree) or '/*' (all) */
|
||||
public array $groupPaths = [];
|
||||
public string $basedn = '';
|
||||
public string $filter = '';
|
||||
public string $usernameField = 'mail';
|
||||
public string $attributeField = '';
|
||||
/** LDAP attribute with the stable user id, '' = objectGUID, entryUUID, nsUniqueId, ipaUniqueID, else the DN */
|
||||
public string $idAttribute = '';
|
||||
/** LDAP filter matching disabled users (besides AD userAccountControl) */
|
||||
public string $disabledFilter = '';
|
||||
public string $groupBasedn = '';
|
||||
public string $groupFilter = '';
|
||||
public string $groupNameAttribute = 'cn';
|
||||
public string $groupMemberMode = 'memberof';
|
||||
public string $groupMemberAttribute = 'member';
|
||||
|
||||
public static function fromArray(array $s): self
|
||||
{
|
||||
$settings = new self();
|
||||
$settings->provider = (string)($s['authsource'] ?? '');
|
||||
$settings->periodicSync = self::flag($s['periodic_sync'] ?? 0);
|
||||
$settings->importUsers = self::flag($s['import_users'] ?? 0);
|
||||
$settings->syncInterval = max(1, (int)($s['sync_interval'] ?? 15));
|
||||
$settings->offboarding = self::flag($s['offboarding'] ?? 0);
|
||||
$settings->offboardingLimit = min(100, max(0, (int)($s['offboarding_limit'] ?? 10)));
|
||||
$settings->groupSync = self::flag($s['group_sync'] ?? 0);
|
||||
$settings->groupAutocreate = self::flag($s['group_autocreate'] ?? 1);
|
||||
$settings->groupDefaultDomain = strtolower(trim((string)($s['group_default_domain'] ?? '')));
|
||||
$settings->defaultTemplate = (string)($s['default_template'] ?? '');
|
||||
$settings->mappers = array_map('strval', (array)($s['mappers'] ?? []));
|
||||
$settings->templates = array_map('strval', (array)($s['templates'] ?? []));
|
||||
$settings->serverUrl = rtrim((string)($s['server_url'] ?? ''), '/');
|
||||
$settings->realm = (string)($s['realm'] ?? '');
|
||||
$settings->clientId = (string)($s['client_id'] ?? '');
|
||||
$settings->clientSecret = (string)($s['client_secret'] ?? '');
|
||||
$settings->ignoreSslError = self::flag($s['ignore_ssl_error'] ?? 0);
|
||||
$settings->groupPaths = array_values(array_filter(array_map('trim', explode(',', (string)($s['group_paths'] ?? ''))), 'strlen'));
|
||||
$settings->basedn = (string)($s['basedn'] ?? '');
|
||||
$settings->filter = trim((string)($s['filter'] ?? ''));
|
||||
$settings->usernameField = strtolower((string)(($s['username_field'] ?? '') ?: 'mail'));
|
||||
$settings->attributeField = strtolower(trim((string)($s['attribute_field'] ?? '')));
|
||||
$settings->idAttribute = strtolower(trim((string)($s['id_attribute'] ?? '')));
|
||||
$settings->disabledFilter = trim((string)($s['disabled_filter'] ?? ''));
|
||||
$settings->groupBasedn = trim((string)($s['group_basedn'] ?? ''));
|
||||
$settings->groupFilter = trim((string)($s['group_filter'] ?? ''));
|
||||
$settings->groupNameAttribute = strtolower((string)(($s['group_name_attribute'] ?? '') ?: 'cn'));
|
||||
$mode = (string)($s['group_member_mode'] ?? 'memberof');
|
||||
$settings->groupMemberMode = in_array($mode, self::MEMBER_MODES, true) ? $mode : 'memberof';
|
||||
$settings->groupMemberAttribute = strtolower((string)(($s['group_member_attribute'] ?? '') ?: 'member'));
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/** Only Keycloak and LDAP can be listed by the sync */
|
||||
public function canSync(): bool
|
||||
{
|
||||
return in_array($this->provider, [self::KEYCLOAK, self::LDAP], true);
|
||||
}
|
||||
|
||||
public function syncEnabled(): bool
|
||||
{
|
||||
return $this->canSync() && ($this->periodicSync || $this->importUsers || $this->offboarding || $this->groupSync);
|
||||
}
|
||||
|
||||
/** Template of the first mapping (in configured order) matching one of the values, null if none */
|
||||
public function mappedTemplate(array $values): ?string
|
||||
{
|
||||
$values = array_map('strval', array_filter($values, 'is_scalar'));
|
||||
foreach ($this->mappers as $key => $mapper) {
|
||||
if (in_array($mapper, $values, true) && ($this->templates[$key] ?? '') !== '') {
|
||||
return $this->templates[$key];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function flag(mixed $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Identity\Model;
|
||||
|
||||
/**
|
||||
* What mailcow knows before a sync run, read by SyncService and planned against by SyncPlanner.
|
||||
*/
|
||||
class State
|
||||
{
|
||||
/**
|
||||
* @param array<string, Account> $accounts mailboxes with the provider as authsource, by username
|
||||
* @param array<string, string> $mailboxes username => authsource of every mailbox
|
||||
* @param string[] $domains mailcow domains
|
||||
* @param array[] $groups mailbox groups {id, domain, name, idp_ref, template, priority,
|
||||
* active, members: username => source}
|
||||
* @param array<string, ?string> $effectiveTemplates username => effective group template
|
||||
* @param array<string, string> $templateHashes template name => attribute hash
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $accounts,
|
||||
public readonly array $mailboxes = [],
|
||||
public readonly array $domains = [],
|
||||
public readonly array $groups = [],
|
||||
public readonly array $effectiveTemplates = [],
|
||||
public readonly array $templateHashes = []
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\Identity\Model;
|
||||
|
||||
/**
|
||||
* What one sync run changes. Built by SyncPlanner from a listing and the current state, applied by SyncService.
|
||||
*/
|
||||
class SyncPlan
|
||||
{
|
||||
/** Details per kind kept in the stored summary */
|
||||
private const DETAILS = 50;
|
||||
|
||||
/** @var array<string, string> username => external id (adopted by email or relinked) */
|
||||
public array $links = [];
|
||||
/** @var array[] {email, name, template, id}: mailboxes to create */
|
||||
public array $creates = [];
|
||||
/** @var string[] usernames set to login disabled (active = 2) */
|
||||
public array $deactivate = [];
|
||||
/** @var string[] usernames the sync had disabled and the IdP enabled again (active = 1) */
|
||||
public array $restore = [];
|
||||
/** @var string[] usernames whose idp_disabled marker is dropped, active stays (an admin changed it) */
|
||||
public array $release = [];
|
||||
/** @var string[] usernames not listed for the first time (missing_since is set) */
|
||||
public array $missing = [];
|
||||
/** @var string[] usernames listed again (missing_since is cleared) */
|
||||
public array $found = [];
|
||||
/** @var array[] {username, name, template}: existing mailboxes whose name/template is synced; template = legacy mapping */
|
||||
public array $updates = [];
|
||||
/** @var array<string, array> username => {name, template}: IdP name and legacy mapping of every listed mailbox */
|
||||
public array $profiles = [];
|
||||
/** @var array[] {ref, name, domain, mail}: groups to create for IdP groups */
|
||||
public array $groupCreates = [];
|
||||
/** @var array<int, string> group id => new name */
|
||||
public array $groupRenames = [];
|
||||
/** @var array<string, string[]> group ref => usernames of its IdP members */
|
||||
public array $memberships = [];
|
||||
/** @var int[] linked groups no longer listed; their IdP members are removed */
|
||||
public array $orphanedGroups = [];
|
||||
/** Number of IdP memberships the plan removes (for the guard) */
|
||||
public int $membershipRemovals = 0;
|
||||
/** @var string[] situations an admin has to resolve */
|
||||
public array $conflicts = [];
|
||||
/** Users without email, not in a mailcow domain or without template (not created) */
|
||||
public int $skipped = 0;
|
||||
/** The guard stopped deactivations and membership removals */
|
||||
public bool $blocked = false;
|
||||
|
||||
/** Stored with the run and shown in the UI */
|
||||
public function summary(): array
|
||||
{
|
||||
$summary = ['counts' => [], 'details' => []];
|
||||
foreach (['links' => 'links', 'creates' => 'creates', 'deactivate' => 'deactivate', 'restore' => 'restore',
|
||||
'release' => 'release', 'missing' => 'missing', 'found' => 'found', 'updates' => 'updates', 'group_creates' => 'groupCreates',
|
||||
'group_renames' => 'groupRenames', 'memberships' => 'memberships', 'orphaned_groups' => 'orphanedGroups',
|
||||
'conflicts' => 'conflicts'] as $key => $kind) {
|
||||
$summary['counts'][$key] = count($this->$kind);
|
||||
}
|
||||
$summary['counts']['membership_removals'] = $this->membershipRemovals;
|
||||
$summary['counts']['skipped'] = $this->skipped;
|
||||
$summary['details'] = [
|
||||
'creates' => array_slice(array_column($this->creates, 'email'), 0, self::DETAILS),
|
||||
'deactivate' => array_slice($this->deactivate, 0, self::DETAILS),
|
||||
'restore' => array_slice($this->restore, 0, self::DETAILS),
|
||||
'missing' => array_slice($this->missing, 0, self::DETAILS),
|
||||
'group_creates' => array_slice(array_column($this->groupCreates, 'name'), 0, self::DETAILS),
|
||||
'conflicts' => array_slice($this->conflicts, 0, self::DETAILS),
|
||||
];
|
||||
$summary['blocked'] = $this->blocked;
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user