diff --git a/docker/kolab/kolab-init.sh b/docker/kolab/kolab-init.sh --- a/docker/kolab/kolab-init.sh +++ b/docker/kolab/kolab-init.sh @@ -26,5 +26,6 @@ ./18-adjust-kolab-conf.sh ./19-turn-on-vlv-in-roundcube.sh ./20-add-alias-attribute-index.sh +./21-adjust-postfix-config.sh touch /tmp/kolab-init.done diff --git a/docker/kolab/utils/15-create-hosted-domain.sh b/docker/kolab/utils/15-create-hosted-domain.sh --- a/docker/kolab/utils/15-create-hosted-domain.sh +++ b/docker/kolab/utils/15-create-hosted-domain.sh @@ -53,6 +53,18 @@ ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" ( + for role in "2fa-user" "activesync-user" "imap-user"; do + echo "cn=${role},${hosted_domain_rootdn}" + echo "cn: ${role}" + echo "description: ${role} role" + echo "objectclass: top" + echo "objectclass: ldapsubentry" + echo "objectclass: nsmanagedroledefinition" + echo "objectclass: nsroledefinition" + echo "objectclass: nssimpleroledefinition" + echo "" + done + echo "dn: ou=Groups,${hosted_domain_rootdn}" echo "ou: Groups" echo "objectClass: top" diff --git a/docker/kolab/utils/21-adjust-postfix-config.sh b/docker/kolab/utils/21-adjust-postfix-config.sh new file mode 100755 --- /dev/null +++ b/docker/kolab/utils/21-adjust-postfix-config.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# new: (inetdomainstatus:1.2.840.113556.1.4.803:=1) +# active: (inetdomainstatus:1.2.840.113556.1.4.803:=2) +# suspended: (inetdomainstatus:1.2.840.113556.1.4.803:=4) +# deleted: (inetdomainstatus:1.2.840.113556.1.4.803:=8) +# confirmed: (inetdomainstatus:1.2.840.113556.1.4.803:=16) +# verified: (inetdomainstatus:1.2.840.113556.1.4.803:=32) +# ready: (inetdomainstatus:1.2.840.113556.1.4.803:=64) + +sed -i -r \ + -e 's/^query_filter.*$/query_filter = (\&(associatedDomain=%s)(inetdomainstatus:1.2.840.113556.1.4.803:=18)(!(inetdomainstatus:1.2.840.113556.1.4.803:=4)))/g' \ + /etc/postfix/ldap/mydestination.cf + +# new: (inetuserstatus:1.2.840.113556.1.4.803:=1) +# active: (inetuserstatus:1.2.840.113556.1.4.803:=2) +# suspended: (inetuserstatus:1.2.840.113556.1.4.803:=4) +# deleted: (inetuserstatus:1.2.840.113556.1.4.803:=8) +# ldapready: (inetuserstatus:1.2.840.113556.1.4.803:=16) +# imapready: (inetuserstatus:1.2.840.113556.1.4.803:=32) + +sed -i -r \ + -e 's/^query_filter.*$/query_filter = (\&(|(mail=%s)(alias=%s))(|(objectclass=kolabinetorgperson)(|(objectclass=kolabgroupofuniquenames)(objectclass=kolabgroupofurls))(|(|(objectclass=groupofuniquenames)(objectclass=groupofurls))(objectclass=kolabsharedfolder))(objectclass=kolabsharedfolder))(inetuserstatus:1.2.840.113556.1.4.803:=50)(!(inetuserstatus:1.2.840.113556.1.4.803:=4)))/g' \ + /etc/postfix/ldap/local_recipient_maps.cf + +systemctl restart postfix diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php --- a/src/app/Backends/LDAP.php +++ b/src/app/Backends/LDAP.php @@ -19,33 +19,33 @@ $config = self::getConfig('admin'); $ldap = self::initLDAP($config); - $hosted_root_dn = \config('ldap.hosted.root_dn'); - $mgmt_root_dn = \config('ldap.admin.root_dn'); + $hostedRootDN = \config('ldap.hosted.root_dn'); + $mgmtRootDN = \config('ldap.admin.root_dn'); - $domain_base_dn = "ou={$domain->namespace},{$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,' . $mgmt_root_dn - . ' || ldap:///ou=People,' . $domain_base_dn . '??sub?(objectclass=inetorgperson)") ' - . 'AND NOT roledn = "ldap:///cn=kolab-admin,' . $mgmt_root_dn . '";)', + . '(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,' . $mgmt_root_dn - . ' || ldap:///ou=People,' . $domain_base_dn . '??sub?(objectclass=inetorgperson)");)', + . '(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,' . $domain_base_dn - . ' || ldap:///cn=kolab-admin,' . $mgmt_root_dn . '");)' + . '(roledn = "ldap:///cn=kolab-admin,' . $domainBaseDN + . ' || ldap:///cn=kolab-admin,' . $mgmtRootDN . '");)' ]; $entry = [ 'aci' => $aci, 'associateddomain' => $domain->namespace, - 'inetdomainbasedn' => $domain_base_dn, + 'inetdomainbasedn' => $domainBaseDN, 'objectclass' => [ 'top', 'domainrelatedobject', @@ -72,42 +72,42 @@ $entry['aci'] = array( '(targetattr = "*")' . '(version 3.0;acl "Deny Unauthorized"; deny (all)' - . '(userdn != "ldap:///uid=kolab-service,ou=Special Users,' . $mgmt_root_dn - . ' || ldap:///ou=People,' . $domain_base_dn . '??sub?(objectclass=inetorgperson)") ' - . 'AND NOT roledn = "ldap:///cn=kolab-admin,' . $mgmt_root_dn . '";)', + . '(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,' . $mgmt_root_dn - . ' || ldap:///ou=People,' . $domain_base_dn . '??sub?(objectclass=inetorgperson)");)', + . '(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,' . $domain_base_dn - . ' || ldap:///cn=kolab-admin,' . $mgmt_root_dn . '");)', + . '(roledn = "ldap:///cn=kolab-admin,' . $domainBaseDN + . ' || ldap:///cn=kolab-admin,' . $mgmtRootDN . '");)', - '(target = "ldap:///ou=*,' . $domain_base_dn . '")' + '(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,' . $mgmt_root_dn . '");)', + . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)', - '(target = "ldap:///uid=*,ou=People,' . $domain_base_dn . '")(targetattr="*")' + '(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,' . $mgmt_root_dn . '");)', + . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)', - '(target = "ldap:///cn=*,' . $domain_base_dn . '")(targetattr="objectclass || cn")' + '(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,' . $mgmt_root_dn . '");)', + . '(userdn = "ldap:///uid=kolab-service,ou=Special Users,' . $mgmtRootDN . '");)', ); - if (!$ldap->get_entry($domain_base_dn)) { - $ldap->add_entry($domain_base_dn, $entry); + if (!$ldap->get_entry($domainBaseDN)) { + $ldap->add_entry($domainBaseDN, $entry); } foreach (['Groups', 'People', 'Resources', 'Shared Folders'] as $item) { - if (!$ldap->get_entry("ou={$item},{$domain_base_dn}")) { + if (!$ldap->get_entry("ou={$item},{$domainBaseDN}")) { $ldap->add_entry( - "ou={$item},{$domain_base_dn}", + "ou={$item},{$domainBaseDN}", [ 'ou' => $item, 'description' => $item, @@ -120,10 +120,10 @@ } } - foreach (['kolab-admin', 'imap-user', 'activesync-user', 'billing-user'] as $item) { - if (!$ldap->get_entry("cn={$item},{$domain_base_dn}")) { + foreach (['kolab-admin', 'billing-user'] as $item) { + if (!$ldap->get_entry("cn={$item},{$domainBaseDN}")) { $ldap->add_entry( - "cn={$item},{$domain_base_dn}", + "cn={$item},{$domainBaseDN}", [ 'cn' => $item, 'description' => "{$item} role", @@ -181,12 +181,14 @@ 'objectclass' => [ 'top', 'inetorgperson', + 'inetuser', 'kolabinetorgperson', 'mailrecipient', 'person' ], 'mail' => $user->email, 'uid' => $user->email, + 'nsroledn' => [] ]; self::setUserAttributes($user, $entry); @@ -210,7 +212,19 @@ */ public static function updateDomain($domain) { - // + $config = self::getConfig('admin'); + $ldap = self::initLDAP($config); + + $ldapDomain = $ldap->find_domain($domain->namespace); + + $oldEntry = $ldap->get_entry($ldapDomain['dn']); + $newEntry = $oldEntry; + + self::setDomainAttributes($domain, $newEntry); + + $ldap->modify_entry($ldapDomain['dn'], $oldEntry, $newEntry); + + $ldap->close(); } public static function deleteDomain($domain) @@ -218,13 +232,13 @@ $config = self::getConfig('admin'); $ldap = self::initLDAP($config); - $hosted_root_dn = \config('ldap.hosted.root_dn'); - $mgmt_root_dn = \config('ldap.admin.root_dn'); + $hostedRootDN = \config('ldap.hosted.root_dn'); + $mgmtRootDN = \config('ldap.admin.root_dn'); - $domain_base_dn = "ou={$domain->namespace},{$hosted_root_dn}"; + $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}"; - if ($ldap->get_entry($domain_base_dn)) { - $ldap->delete_entry_recursive($domain_base_dn); + if ($ldap->get_entry($domainBaseDN)) { + $ldap->delete_entry_recursive($domainBaseDN); } if ($ldap_domain = $ldap->find_domain($domain->namespace)) { @@ -232,6 +246,8 @@ $ldap->delete_entry($ldap_domain['dn']); } } + + $ldap->close(); } public static function deleteUser($user) @@ -244,6 +260,7 @@ $domain = $ldap->find_domain($_domain); if (!$domain) { + $ldap->close(); return false; } @@ -251,10 +268,13 @@ $dn = "uid={$user->email},ou=People,{$base_dn}"; if (!$ldap->get_entry($dn)) { + $ldap->close(); return false; } $ldap->delete_entry($dn); + + $ldap->close(); } /** @@ -274,18 +294,29 @@ $domain = $ldap->find_domain($_domain); if (!$domain) { + $ldap->close(); return false; } $base_dn = $ldap->domain_root_dn($_domain); $dn = "uid={$user->email},ou=People,{$base_dn}"; - $old_entry = $ldap->get_entry($dn); - $new_entry = $old_entry; + $oldEntry = $ldap->get_entry($dn); - self::setUserAttributes($user, $new_entry); + if (!$oldEntry) { + $ldap->close(); + return false; + } + + if (!array_key_exists('nsroledn', $oldEntry)) { + $oldEntry['nsroledn'] = (array)$ldap->get_entry_attributes($dn, ['nsroledn']); + } + + $newEntry = $oldEntry; + + self::setUserAttributes($user, $newEntry); - $ldap->modify_entry($dn, $old_entry, $new_entry); + $ldap->modify_entry($dn, $oldEntry, $newEntry); $ldap->close(); } @@ -307,6 +338,14 @@ } /** + * Set domain attributes + */ + private static function setDomainAttributes(Domain $domain, array &$entry) + { + $entry['inetdomainstatus'] = $domain->status; + } + + /** * Set common user attributes */ private static function setUserAttributes(User $user, array &$entry) @@ -341,6 +380,61 @@ $entry['givenname'] = $firstName; $entry['sn'] = $lastName; $entry['userpassword'] = $user->password_ldap; + + $entry['inetuserstatus'] = $user->status; + + $entry['mailquota'] = 0; + + if (!array_key_exists('nsroledn', $entry)) { + $entry['nsroledn'] = []; + } else if (!is_array($entry['nsroledn'])) { + $entry['nsroledn'] = (array)$entry['nsroledn']; + } + + $roles = []; + + foreach ($user->entitlements as $entitlement) { + \Log::debug("Examining {$entitlement->sku->title}"); + + switch ($entitlement->sku->title) { + case "storage": + $entry['mailquota'] += 1048576; + break; + } + + $roles[] = $entitlement->sku->title; + } + + $hostedRootDN = \config('ldap.hosted.root_dn'); + + if (in_array("2fa", $roles)) { + $entry['nsroledn'][] = "cn=2fa-user,{$hostedRootDN}"; + } else { + $key = array_search("cn=2fa-user,{$hostedRootDN}", $entry['nsroledn']); + if ($key !== false) { + unset($entry['nsroledn'][$key]); + } + } + + if (in_array("activesync", $roles)) { + $entry['nsroledn'][] = "cn=activesync-user,{$hostedRootDN}"; + } else { + $key = array_search("cn=activesync-user,{$hostedRootDN}", $entry['nsroledn']); + if ($key !== false) { + unset($entry['nsroledn'][$key]); + } + } + + if (!in_array("groupware", $roles)) { + $entry['nsroledn'][] = "cn=imap-user,{$hostedRootDN}"; + } else { + $key = array_search("cn=imap-user,{$hostedRootDN}", $entry['nsroledn']); + if ($key !== false) { + unset($entry['nsroledn'][$key]); + } + } + + $entry['nsroledn'] = array_unique($entry['nsroledn']); } /** @@ -367,7 +461,11 @@ public static function logHook($level, $msg): void { if ( - ($level == LOG_INFO || $level == LOG_DEBUG || $level == LOG_NOTICE) + ( + $level == LOG_INFO + || $level == LOG_DEBUG + || $level == LOG_NOTICE + ) && !\config('app.debug') ) { return; diff --git a/src/app/Console/Commands/DomainList.php b/src/app/Console/Commands/DomainList.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/DomainList.php @@ -0,0 +1,47 @@ +orderBy('namespace')->each( + function ($domain) { + $this->info($domain->namespace); + } + ); + } +} diff --git a/src/app/Console/Commands/DomainStatus.php b/src/app/Console/Commands/DomainStatus.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/DomainStatus.php @@ -0,0 +1,51 @@ +argument('domain'))->first(); + + if (!$domain) { + return 1; + } + + $this->info("Found domain: {$domain->id}"); + + $this->info($domain->status); + } +} diff --git a/src/app/Console/Commands/DomainSuspend.php b/src/app/Console/Commands/DomainSuspend.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/DomainSuspend.php @@ -0,0 +1,51 @@ +argument('domain'))->first(); + + if (!$domain) { + return 1; + } + + $this->info("Found domain: {$domain->id}"); + + $domain->suspend(); + } +} diff --git a/src/app/Console/Commands/DomainUnsuspend.php b/src/app/Console/Commands/DomainUnsuspend.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/DomainUnsuspend.php @@ -0,0 +1,51 @@ +argument('domain'))->first(); + + if (!$domain) { + return 1; + } + + $this->info("Found domain {$domain->id}"); + + $domain->unsuspend(); + } +} diff --git a/src/app/Console/Commands/UserStatus.php b/src/app/Console/Commands/UserStatus.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/UserStatus.php @@ -0,0 +1,51 @@ +argument('user'))->first(); + + if (!$user) { + return 1; + } + + $this->info("Found user: {$user->id}"); + + $this->info($user->status); + } +} diff --git a/src/app/Console/Commands/UserSuspend.php b/src/app/Console/Commands/UserSuspend.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/UserSuspend.php @@ -0,0 +1,51 @@ +argument('user'))->first(); + + if (!$user) { + return 1; + } + + $this->info("Found user: {$user->id}"); + + $user->suspend(); + } +} diff --git a/src/app/Console/Commands/UserUnsuspend.php b/src/app/Console/Commands/UserUnsuspend.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/UserUnsuspend.php @@ -0,0 +1,51 @@ +argument('user'))->first(); + + if (!$user) { + return 1; + } + + $this->info("Found user {$user->id}"); + + $user->unsuspend(); + } +} diff --git a/src/app/Console/Commands/UserVerify.php b/src/app/Console/Commands/UserVerify.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/UserVerify.php @@ -0,0 +1,51 @@ +argument('user'))->first(); + + if (!$user) { + return 1; + } + + $this->info("Found user: {$user->id}"); + + $job = new \App\Jobs\UserVerify($user); + $job->handle(); + } +} diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -309,6 +309,36 @@ } /** + * Suspend this domain. + * + * @return void + */ + public function suspend(): void + { + if ($this->isSuspended()) { + return; + } + + $this->status |= Domain::STATUS_SUSPENDED; + $this->save(); + } + + /** + * Unsuspend this domain. + * + * @return void + */ + public function unsuspend(): void + { + if (!$this->isSuspended()) { + return; + } + + $this->status ^= Domain::STATUS_SUSPENDED; + $this->save(); + } + + /** * Verify if a domain exists in DNS * * @return bool True if registered, False otherwise diff --git a/src/app/Jobs/DomainUpdate.php b/src/app/Jobs/DomainUpdate.php new file mode 100644 --- /dev/null +++ b/src/app/Jobs/DomainUpdate.php @@ -0,0 +1,44 @@ +domain_id = $domain_id; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $domain = \App\Domain::find($this->domain_id); + + LDAP::updateDomain($domain); + } +} diff --git a/src/app/Observers/DomainObserver.php b/src/app/Observers/DomainObserver.php --- a/src/app/Observers/DomainObserver.php +++ b/src/app/Observers/DomainObserver.php @@ -77,10 +77,9 @@ */ public function updated(Domain $domain) { - // + \App\Jobs\DomainUpdate::dispatch($domain->id); } - /** * Handle the domain "restored" event. * diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php --- a/src/app/Observers/EntitlementObserver.php +++ b/src/app/Observers/EntitlementObserver.php @@ -54,6 +54,12 @@ } } + public function created(Entitlement $entitlement) + { + $entitlement->entitleable->updated_at = Carbon::now(); + $entitlement->entitleable->save(); + } + /** * Handle the entitlement "deleted" event. * @@ -69,5 +75,8 @@ $sf = new \App\Auth\SecondFactor($entitlement->entitleable); $sf->removeFactors(); } + + $entitlement->entitleable->updated_at = Carbon::now(); + $entitlement->entitleable->save(); } } diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -453,6 +453,36 @@ } /** + * Suspend this domain. + * + * @return void + */ + public function suspend(): void + { + if ($this->isSuspended()) { + return; + } + + $this->status |= User::STATUS_SUSPENDED; + $this->save(); + } + + /** + * Unsuspend this domain. + * + * @return void + */ + public function unsuspend(): void + { + if (!$this->isSuspended()) { + return; + } + + $this->status ^= User::STATUS_SUSPENDED; + $this->save(); + } + + /** * Return users controlled by the current user. * * Users assigned to wallets the current user controls or owns. diff --git a/src/database/seeds/UserSeeder.php b/src/database/seeds/UserSeeder.php --- a/src/database/seeds/UserSeeder.php +++ b/src/database/seeds/UserSeeder.php @@ -57,6 +57,7 @@ $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $package_kolab = \App\Package::where('title', 'kolab')->first(); + $package_lite = \App\Package::where('title', 'lite')->first(); $domain->assignPackage($package_domain, $john); $john->assignPackage($package_kolab); @@ -109,6 +110,8 @@ $john->assignPackage($package_kolab, $ned); + $ned->assignSku(\App\Sku::where('title', 'activesync')->first(), 1); + // Ned is a controller on Jack's wallet $john->wallets()->first()->addController($ned); @@ -117,6 +120,17 @@ $ned->assignSku($sku2fa); SecondFactor::seed('ned@kolab.org'); + $joe = User::create( + [ + 'name' => 'Joe Sixpack', + 'email' => 'joe@kolab.org', + 'password' => 'simple123', + 'email_verified_at' => now() + ] + ); + + $john->assignPackage($package_lite, $joe); + factory(User::class, 10)->create(); } }