Page MenuHomePhorge

D2020.1775392902.diff
No OneTemporary

Authored By
Unknown
Size
87 KB
Referenced Files
None
Subscribers
None

D2020.1775392902.diff

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
@@ -3,6 +3,7 @@
namespace App\Backends;
use App\Domain;
+use App\Group;
use App\User;
class LDAP
@@ -214,6 +215,64 @@
}
/**
+ * 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);
+
+ list($cn, $domainName) = explode('@', $group->email);
+
+ $domain = $group->domain();
+
+ if (empty($domain)) {
+ self::throwException(
+ $ldap,
+ "Failed to create group {$group->email} in LDAP (" . __LINE__ . ")"
+ );
+ }
+
+ $hostedRootDN = \config('ldap.hosted.root_dn');
+
+ $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}";
+
+ $groupBaseDN = "ou=Groups,{$domainBaseDN}";
+
+ $dn = "cn={$cn},{$groupBaseDN}";
+
+ $entry = [
+ 'cn' => $cn,
+ 'mail' => $group->email,
+ 'objectclass' => [
+ 'top',
+ 'groupofuniquenames',
+ 'kolabgroupofuniquenames'
+ ],
+ 'uniqueMember' => []
+ ];
+
+ self::setGroupAttributes($group, $entry);
+
+ $result = $ldap->add_entry($dn, $entry);
+
+ if (!$result) {
+ self::throwException(
+ $ldap,
+ "Failed to create group {$group->email} 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
@@ -279,7 +338,7 @@
/**
* Delete a domain from LDAP.
*
- * @param \App\Domain $domain The domain to update.
+ * @param \App\Domain $domain The domain to delete
*
* @throws \Exception
*/
@@ -323,9 +382,37 @@
}
/**
+ * 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 user from LDAP.
*
- * @param \App\User $user The user account to update.
+ * @param \App\User $user The user account to delete.
*
* @throws \Exception
*/
@@ -377,6 +464,28 @@
}
/**
+ * 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 user data from LDAP.
*
* @param string $email The user email.
@@ -443,6 +552,66 @@
}
/**
+ * 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);
+
+ list($cn, $domainName) = explode('@', $group->email);
+
+ $domain = $group->domain();
+
+ if (empty($domain)) {
+ self::throwException(
+ $ldap,
+ "Failed to update group {$group->email} in LDAP (" . __LINE__ . ")"
+ );
+ }
+
+ $hostedRootDN = \config('ldap.hosted.root_dn');
+
+ $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}";
+
+ $groupBaseDN = "ou=Groups,{$domainBaseDN}";
+
+ $dn = "cn={$cn},{$groupBaseDN}";
+
+ $entry = [
+ 'cn' => $cn,
+ 'mail' => $group->email,
+ 'objectclass' => [
+ 'top',
+ 'groupofuniquenames',
+ 'kolabgroupofuniquenames'
+ ],
+ 'uniqueMember' => []
+ ];
+
+ $oldEntry = $ldap->get_entry($dn);
+
+ self::setGroupAttributes($group, $entry);
+
+ $result = $ldap->modify_entry($dn, $oldEntry, $entry);
+
+ if (!is_array($result)) {
+ self::throwException(
+ $ldap,
+ "Failed to update group {$group->email} in LDAP (" . __LINE__ . ")"
+ );
+ }
+
+ if (empty(self::$ldap)) {
+ $ldap->close();
+ }
+ }
+
+ /**
* Update a user in LDAP.
*
* @param \App\User $user The user account to update.
@@ -531,6 +700,63 @@
}
/**
+ * Convert group member addresses in to valid entries.
+ */
+ private static function setGroupAttributes(Group $group, &$entry)
+ {
+ $config = self::getConfig('admin');
+ $ldap = self::initLDAP($config);
+
+ $validMembers = [];
+
+ $domain = $group->domain();
+
+ $hostedRootDN = \config('ldap.hosted.root_dn');
+
+ $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}";
+
+ foreach ($group->members as $member) {
+ list($local, $domainName) = explode('@', $member);
+
+ $memberDN = "uid={$member},ou=People,{$domainBaseDN}";
+
+ // if the member is in the local domain but doesn't exist, drop it
+ if ($domainName == $domain->namespace) {
+ if (!$ldap->get_entry($memberDN)) {
+ continue;
+ }
+ }
+
+ // add the member if not in the local domain
+ if (!$ldap->get_entry($memberDN)) {
+ $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
+ $group->members = $validMembers;
+ Group::withoutEvents(function () use ($group) {
+ $group->save();
+ });
+ }
+
+ /**
* Set common user attributes
*/
private static function setUserAttributes(User $user, array &$entry)
@@ -625,6 +851,33 @@
}
/**
+ * 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 false|null|array Group entry, False on error, NULL if not found
+ */
+ private static function getGroupEntry($ldap, $email, &$dn = null)
+ {
+ list($_local, $_domain) = explode('@', $email, 2);
+
+ $domain = $ldap->find_domain($_domain);
+
+ if (!$domain) {
+ return $domain;
+ }
+
+ $base_dn = $ldap->domain_root_dn($_domain);
+ $dn = "cn={$_local},ou=Groups,{$base_dn}";
+
+ $entry = $ldap->get_entry($dn);
+
+ return $entry ?: null;
+ }
+
+ /**
* Get user entry from LDAP.
*
* @param \Net_LDAP3 $ldap Ldap connection
diff --git a/src/app/Console/Commands/Group/AddMemberCommand.php b/src/app/Console/Commands/Group/AddMemberCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/AddMemberCommand.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class AddMemberCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:add-member {group} {member}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Add a member to a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $member = \strtolower($this->argument('member'));
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ if (in_array($member, $group->members)) {
+ $this->error("{$member}: Already exists in the group.");
+ return 1;
+ }
+
+ if ($error = CreateCommand::validateMemberEmail($member)) {
+ $this->error("{$member}: $error");
+ return 1;
+ }
+
+ // We can't modify the property indirectly, therefor array_merge()
+ $group->members = array_merge($group->members, [$member]);
+ $group->save();
+ }
+}
diff --git a/src/app/Console/Commands/Group/CreateCommand.php b/src/app/Console/Commands/Group/CreateCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/CreateCommand.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+use App\Domain;
+use App\Group;
+use App\User;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Validator;
+
+class CreateCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:create {email} {--member=*}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Create a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $email = $this->argument('email');
+ $members = $this->option('member');
+
+ list($local, $domainName) = explode('@', $email, 2);
+
+ $domain = $this->getDomain($domainName);
+
+ if (!$domain) {
+ $this->error("No such domain {$domainName}.");
+ return 1;
+ }
+
+ if ($domain->isPublic()) {
+ $this->error("Domain {$domainName} is public.");
+ return 1;
+ }
+
+ $owner = $domain->wallet()->owner;
+
+ // Validate group email address
+ foreach ($members as $i => $member) {
+ if ($error = $this->validateMemberEmail($member)) {
+ $this->error("{$member}: $error");
+ return 1;
+ }
+ if (\strtolower($member) === \strtolower($email)) {
+ $this->error("{$member}: Cannot be the same as the group address.");
+ return 1;
+ }
+ }
+
+ // Validate members addresses
+ if ($error = $this->validateGroupEmail($email, $owner)) {
+ $this->error("{$email}: {$error}");
+ return 1;
+ }
+
+ DB::beginTransaction();
+
+ // Create the group
+ $group = new Group();
+ $group->email = $email;
+ $group->members = $members;
+ $group->save();
+
+ $group->assignToWallet($owner->wallets->first());
+
+ DB::commit();
+
+ $this->info($group->id);
+ }
+
+ /**
+ * Validate an email address for use as a group member
+ *
+ * @param string $email Email address
+ *
+ * @return ?string Error message on validation error
+ */
+ public static function validateMemberEmail(string $email): ?string
+ {
+ $v = Validator::make(
+ ['email' => $email],
+ ['email' => [new \App\Rules\ExternalEmail()]]
+ );
+
+ if ($v->fails()) {
+ return $v->errors()->toArray()['email'][0];
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate an email address for use as a group email
+ *
+ * @param string $email Email address
+ * @param \App\User $user The group owner
+ *
+ * @return ?string Error message on validation error
+ */
+ public static function validateGroupEmail(string $email, \App\User $user): ?string
+ {
+ if (strpos($email, '@') === false) {
+ return \trans('validation.entryinvalid', ['attribute' => 'email']);
+ }
+
+ list($login, $domain) = explode('@', \strtolower($email));
+
+ if (strlen($login) === 0 || strlen($domain) === 0) {
+ return \trans('validation.entryinvalid', ['attribute' => 'email']);
+ }
+
+ // Check if domain exists
+ $domain = Domain::where('namespace', $domain)->first();
+/*
+ if (empty($domain)) {
+ return \trans('validation.domainnotavailable');
+ }
+
+ if ($domain->isPublic()) {
+ return \trans('validation.domainnotavailable');
+ }
+*/
+ // Validate login part alone
+ $v = Validator::make(
+ ['email' => $login],
+ ['email' => [new \App\Rules\UserEmailLocal(!$domain->isPublic())]]
+ );
+
+ if ($v->fails()) {
+ return $v->errors()->toArray()['email'][0];
+ }
+/*
+ // Check if it is one of domains available to the user
+ $domains = \collect($user->domains())->pluck('namespace')->all();
+
+ if (!in_array($domain->namespace, $domains)) {
+ // return \trans('validation.entryexists', ['attribute' => 'domain']);
+ return \trans('validation.domainnotavailable');
+ }
+*/
+ // Check if a user with specified address already exists
+ if (User::emailExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+
+ // Check if an alias with specified address already exists.
+ if (User::aliasExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+
+ if (Group::emailExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+
+ return null;
+ }
+}
diff --git a/src/app/Console/Commands/Group/DeleteCommand.php b/src/app/Console/Commands/Group/DeleteCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/DeleteCommand.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class DeleteCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:delete {group}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Delete a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ $group->delete();
+ }
+}
diff --git a/src/app/Console/Commands/Group/InfoCommand.php b/src/app/Console/Commands/Group/InfoCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/InfoCommand.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class InfoCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:info {group}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Print a group information.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ $this->info('Id: ' . $group->id);
+ $this->info('Email: ' . $group->email);
+ $this->info('Status: ' . $group->status);
+
+ // TODO: Print owner/wallet
+
+ foreach ($group->members as $member) {
+ $this->info('Member: ' . $member);
+ }
+ }
+}
diff --git a/src/app/Console/Commands/Group/RemoveMemberCommand.php b/src/app/Console/Commands/Group/RemoveMemberCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/RemoveMemberCommand.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class RemoveMemberCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:remove-member {group} {member}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Remove a member from a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $member = \strtolower($this->argument('member'));
+
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ $members = [];
+
+ foreach ($group->members as $m) {
+ if ($m !== $member) {
+ $members[] = $m;
+ }
+ }
+
+ if (count($members) == count($group->members)) {
+ $this->error("Member {$member} not found in the group.");
+ return 1;
+ }
+
+ $group->members = $members;
+ $group->save();
+ }
+}
diff --git a/src/app/Group.php b/src/app/Group.php
new file mode 100644
--- /dev/null
+++ b/src/app/Group.php
@@ -0,0 +1,277 @@
+<?php
+
+namespace App;
+
+use App\Wallet;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+/**
+ * The eloquent definition of a Group.
+ *
+ * @property int $id The group identifier
+ * @property string $email An email address
+ * @property string $members A comma-separated list of email addresses
+ * @property int $status The group status
+ */
+class Group extends Model
+{
+ use SoftDeletes;
+
+ // we've simply never heard of this domain
+ public const STATUS_NEW = 1 << 0;
+ // it's been activated
+ public const STATUS_ACTIVE = 1 << 1;
+ // domain has been suspended.
+ public const STATUS_SUSPENDED = 1 << 2;
+ // domain has been deleted
+ public const STATUS_DELETED = 1 << 3;
+ // domain has been created in LDAP
+ public const STATUS_LDAP_READY = 1 << 4;
+
+ public $incrementing = false;
+
+ protected $keyType = 'bigint';
+
+ protected $fillable = [
+ 'email',
+ 'status',
+ 'members'
+ ];
+
+ /**
+ * Assign the group to a wallet.
+ *
+ * @param \App\Wallet $wallet The wallet
+ *
+ * @return \App\Group Self
+ * @throws \Exception
+ */
+ public function assignToWallet(Wallet $wallet): Group
+ {
+ if (empty($this->id)) {
+ throw new \Exception("Group not yet exists");
+ }
+
+ if ($this->entitlement()->count()) {
+ throw new \Exception("Group already assigned to a wallet");
+ }
+
+ $sku = \App\Sku::where('title', 'group')->first();
+ $exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
+
+ \App\Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'entitleable_id' => $this->id,
+ 'entitleable_type' => Group::class
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Returns group domain.
+ *
+ * @return ?\App\Domain The domain group belongs to, NULL if it does not exist
+ */
+ public function domain(): ?Domain
+ {
+ list($local, $domainName) = explode('@', $this->email);
+
+ return Domain::where('namespace', $domainName)->first();
+ }
+
+ /**
+ * Find whether an email address exists as a group (including deleted groups).
+ *
+ * @param string $email Email address
+ * @param bool $return_group Return Group instance instead of boolean
+ *
+ * @return \App\Group|bool True or Group model object if found, False otherwise
+ */
+ public static function emailExists(string $email, bool $return_group = false)
+ {
+ if (strpos($email, '@') === false) {
+ return false;
+ }
+
+ $email = \strtolower($email);
+
+ $group = self::withTrashed()->where('email', $email)->first();
+
+ if ($group) {
+ return $return_group ? $group : true;
+ }
+
+ return false;
+ }
+
+ /**
+ * The group entitlement.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\MorphOne
+ */
+ public function entitlement()
+ {
+ return $this->morphOne('App\Entitlement', 'entitleable');
+ }
+
+ /**
+ * Group members propert accessor. Converts internal comma-separated list into an array
+ *
+ * @param string $members Comma-separated list of email addresses
+ *
+ * @return array Email addresses of the group members, as an array
+ */
+ public function getMembersAttribute($members): array
+ {
+ return $members ? explode(',', $members) : [];
+ }
+
+ /**
+ * Returns whether this domain is active.
+ *
+ * @return bool
+ */
+ public function isActive(): bool
+ {
+ return ($this->status & self::STATUS_ACTIVE) > 0;
+ }
+
+ /**
+ * Returns whether this domain is deleted.
+ *
+ * @return bool
+ */
+ public function isDeleted(): bool
+ {
+ return ($this->status & self::STATUS_DELETED) > 0;
+ }
+
+ /**
+ * Returns whether this domain is new.
+ *
+ * @return bool
+ */
+ public function isNew(): bool
+ {
+ return ($this->status & self::STATUS_NEW) > 0;
+ }
+
+ /**
+ * Returns whether this domain is registered in LDAP.
+ *
+ * @return bool
+ */
+ public function isLdapReady(): bool
+ {
+ return ($this->status & self::STATUS_LDAP_READY) > 0;
+ }
+
+ /**
+ * Returns whether this domain is suspended.
+ *
+ * @return bool
+ */
+ public function isSuspended(): bool
+ {
+ return ($this->status & self::STATUS_SUSPENDED) > 0;
+ }
+
+ /**
+ * Ensure the email is appropriately cased.
+ *
+ * @param string $email Group email address
+ */
+ public function setEmailAttribute(string $email)
+ {
+ $this->attributes['email'] = strtolower($email);
+ }
+
+ /**
+ * Ensure the members are appropriately formatted.
+ *
+ * @param array $members Email addresses of the group members
+ */
+ public function setMembersAttribute(array $members): void
+ {
+ $members = array_filter(array_map('strtolower', $members));
+ $this->attributes['members'] = implode(',', $members);
+ }
+
+ /**
+ * Group status mutator
+ *
+ * @throws \Exception
+ */
+ public function setStatusAttribute($status)
+ {
+ $new_status = 0;
+
+ $allowed_values = [
+ self::STATUS_NEW,
+ self::STATUS_ACTIVE,
+ self::STATUS_SUSPENDED,
+ self::STATUS_DELETED,
+ self::STATUS_LDAP_READY,
+ ];
+
+ foreach ($allowed_values as $value) {
+ if ($status & $value) {
+ $new_status |= $value;
+ $status ^= $value;
+ }
+ }
+
+ if ($status > 0) {
+ throw new \Exception("Invalid group status: {$status}");
+ }
+
+ $this->attributes['status'] = $new_status;
+ }
+
+ /**
+ * Suspend this group.
+ *
+ * @return void
+ */
+ public function suspend(): void
+ {
+ if ($this->isSuspended()) {
+ return;
+ }
+
+ $this->status |= Group::STATUS_SUSPENDED;
+ $this->save();
+ }
+
+ /**
+ * Unsuspend this group.
+ *
+ * @return void
+ */
+ public function unsuspend(): void
+ {
+ if (!$this->isSuspended()) {
+ return;
+ }
+
+ $this->status ^= Group::STATUS_SUSPENDED;
+ $this->save();
+ }
+
+ /**
+ * Returns the wallet by which the group is controlled
+ *
+ * @return \App\Wallet A wallet object
+ */
+ public function wallet(): ?Wallet
+ {
+ // Note: Not all domains have a entitlement/wallet
+ $entitlement = $this->entitlement()->withTrashed()->first();
+
+ return $entitlement ? $entitlement->wallet : null;
+ }
+}
diff --git a/src/app/Handlers/Group.php b/src/app/Handlers/Group.php
new file mode 100644
--- /dev/null
+++ b/src/app/Handlers/Group.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Handlers;
+
+class Group extends \App\Handlers\Base
+{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
+ public static function entitleableClass(): string
+ {
+ return \App\Group::class;
+ }
+}
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -371,7 +371,7 @@
// Check if user with specified login already exists
$email = $login . '@' . $domain;
- if (User::emailExists($email) || User::aliasExists($email)) {
+ if (User::emailExists($email) || User::aliasExists($email) || \App\Group::emailExists($email)) {
return ['login' => \trans('validation.loginexists')];
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Domain;
+use App\Group;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\Sku;
@@ -30,10 +31,10 @@
];
/**
- * On user create it is filled with a user object to force-delete
+ * On user create it is filled with a user or group object to force-delete
* before the creation of a new user record is possible.
*
- * @var \App\User|null
+ * @var \App\User|\App\Group|null
*/
protected $deleteBeforeCreate;
@@ -673,10 +674,10 @@
/**
* Email address validation for use as a user mailbox (login).
*
- * @param string $email Email address
- * @param \App\User $user The account owner
- * @param ?\App\User $deleted Filled with an instance of a deleted user with
- * the specified email address, if exists
+ * @param string $email Email address
+ * @param \App\User $user The account owner
+ * @param null|\App\User|\App\Group $deleted Filled with an instance of a deleted user or group
+ * with the specified email address, if exists
*
* @return ?string Error message on validation error
*/
@@ -734,6 +735,17 @@
return \trans('validation.entryexists', ['attribute' => 'email']);
}
+ // Check if a group with specified address already exists
+ if ($existing_group = Group::emailExists($email, true)) {
+ // If this is a deleted group in the same custom domain
+ // we'll force delete it before
+ if (!$domain->isPublic() && $existing_group->trashed()) {
+ $deleted = $existing_group;
+ } else {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+ }
+
return null;
}
@@ -798,6 +810,11 @@
}
}
+ // Check if a group with specified address already exists
+ if (Group::emailExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'alias']);
+ }
+
return null;
}
}
diff --git a/src/app/Jobs/DomainJob.php b/src/app/Jobs/DomainJob.php
--- a/src/app/Jobs/DomainJob.php
+++ b/src/app/Jobs/DomainJob.php
@@ -31,7 +31,7 @@
/**
* Create a new job instance.
*
- * @param int $domainId The ID for the user to create.
+ * @param int $domainId The ID for the domain to create.
*
* @return void
*/
diff --git a/src/app/Jobs/Group/CreateJob.php b/src/app/Jobs/Group/CreateJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/Group/CreateJob.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Jobs\Group;
+
+use App\Jobs\GroupJob;
+
+class CreateJob extends GroupJob
+{
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $group = $this->getGroup();
+
+ if (!$group->isLdapReady()) {
+ \App\Backends\LDAP::createGroup($group);
+
+ $group->status |= \App\Group::STATUS_LDAP_READY;
+ $group->save();
+ }
+ }
+}
diff --git a/src/app/Jobs/Group/DeleteJob.php b/src/app/Jobs/Group/DeleteJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/Group/DeleteJob.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Jobs\Group;
+
+use App\Jobs\GroupJob;
+
+class DeleteJob extends GroupJob
+{
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $group = $this->getGroup();
+
+ // sanity checks
+ if ($group->isDeleted()) {
+ $this->fail(new \Exception("Group {$this->groupId} is already marked as deleted."));
+ return;
+ }
+
+ \App\Backends\LDAP::deleteGroup($group);
+
+ $group->status |= \App\Group::STATUS_DELETED;
+
+ if ($group->isLdapReady()) {
+ $group->status ^= \App\Group::STATUS_LDAP_READY;
+ }
+
+ $group->save();
+ }
+}
diff --git a/src/app/Jobs/Group/UpdateJob.php b/src/app/Jobs/Group/UpdateJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/Group/UpdateJob.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Jobs\Group;
+
+use App\Jobs\GroupJob;
+
+class UpdateJob extends GroupJob
+{
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $group = $this->getGroup();
+
+ if (!$group->isLdapReady()) {
+ $this->delete();
+ return;
+ }
+
+ \App\Backends\LDAP::updateGroup($group);
+ }
+}
diff --git a/src/app/Jobs/GroupJob.php b/src/app/Jobs/GroupJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/GroupJob.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Jobs;
+
+/**
+ * The abstract \App\Jobs\GroupJob implements the logic needed for all dispatchable Jobs related to
+ * \App\Group objects.
+ *
+ * ```php
+ * $job = new \App\Jobs\Group\CreateJob($groupId);
+ * $job->handle();
+ * ```
+ */
+abstract class GroupJob extends CommonJob
+{
+ /**
+ * The ID for the \App\Group. This is the shortest globally unique identifier and saves Redis space
+ * compared to a serialized version of the complete \App\Group object.
+ *
+ * @var int
+ */
+ protected $groupId;
+
+ /**
+ * The \App\Group email property, for legibility in the queue management.
+ *
+ * @var string
+ */
+ protected $groupEmail;
+
+ /**
+ * Create a new job instance.
+ *
+ * @param int $groupId The ID for the group to create.
+ *
+ * @return void
+ */
+ public function __construct(int $groupId)
+ {
+ $this->groupId = $groupId;
+
+ $group = $this->getGroup();
+
+ if ($group) {
+ $this->groupEmail = $group->email;
+ }
+ }
+
+ /**
+ * Get the \App\Group entry associated with this job.
+ *
+ * @return \App\Group|null
+ *
+ * @throws \Exception
+ */
+ protected function getGroup()
+ {
+ $group = \App\Group::withTrashed()->find($this->groupId);
+
+ if (!$group) {
+ $this->fail(new \Exception("Group {$this->groupId} could not be found in the database."));
+ }
+
+ return $group;
+ }
+}
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
@@ -68,6 +68,10 @@
*/
public function deleted(Domain $domain)
{
+ if ($domain->isForceDeleting()) {
+ return;
+ }
+
\App\Jobs\Domain\DeleteJob::dispatch($domain->id);
}
diff --git a/src/app/Observers/GroupObserver.php b/src/app/Observers/GroupObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/GroupObserver.php
@@ -0,0 +1,113 @@
+<?php
+
+namespace App\Observers;
+
+use App\Group;
+use Illuminate\Support\Facades\DB;
+
+class GroupObserver
+{
+ /**
+ * Handle the group "created" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function creating(Group $group): void
+ {
+ while (true) {
+ $allegedly_unique = \App\Utils::uuidInt();
+ if (!Group::withTrashed()->find($allegedly_unique)) {
+ $group->{$group->getKeyName()} = $allegedly_unique;
+ break;
+ }
+ }
+
+ $group->status |= Group::STATUS_NEW | Group::STATUS_ACTIVE;
+ }
+
+ /**
+ * Handle the group "created" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function created(Group $group)
+ {
+ \App\Jobs\Group\CreateJob::dispatch($group->id);
+ }
+
+ /**
+ * Handle the group "deleting" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function deleting(Group $group)
+ {
+ // Entitlements do not have referential integrity on the entitled object, so this is our
+ // way of doing an onDelete('cascade') without the foreign key.
+ \App\Entitlement::where('entitleable_id', $group->id)
+ ->where('entitleable_type', Group::class)
+ ->delete();
+ }
+
+ /**
+ * Handle the group "deleted" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function deleted(Group $group)
+ {
+ if ($group->isForceDeleting()) {
+ return;
+ }
+
+ \App\Jobs\Group\DeleteJob::dispatch($group->id);
+ }
+
+ /**
+ * Handle the group "updated" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function updated(Group $group)
+ {
+ \App\Jobs\Group\UpdateJob::dispatch($group->id);
+ }
+
+ /**
+ * Handle the group "restored" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function restored(Group $group)
+ {
+ //
+ }
+
+ /**
+ * Handle the group "force deleting" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function forceDeleted(Group $group)
+ {
+ // A group can be force-deleted separately from the owner
+ // we have to force-delete entitlements
+ \App\Entitlement::where('entitleable_id', $group->id)
+ ->where('entitleable_type', Group::class)
+ ->forceDelete();
+ }
+}
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -4,6 +4,7 @@
use App\Entitlement;
use App\Domain;
+use App\Group;
use App\Transaction;
use App\User;
use App\Wallet;
@@ -97,7 +98,16 @@
*/
public function deleted(User $user)
{
- //
+ // Remove the user from existing groups
+ $wallet = $user->wallet();
+ if ($wallet && $wallet->owner) {
+ $wallet->owner->groups()->each(function ($group) use ($user) {
+ if (in_array($user->email, $group->members)) {
+ $group->members = array_diff($group->members, [$user->email]);
+ $group->save();
+ }
+ });
+ }
}
/**
@@ -132,6 +142,7 @@
$assignments = Entitlement::whereIn('wallet_id', $wallets)->get();
$users = [];
$domains = [];
+ $groups = [];
$entitlements = [];
foreach ($assignments as $entitlement) {
@@ -139,30 +150,35 @@
$domains[] = $entitlement->entitleable_id;
} elseif ($entitlement->entitleable_type == User::class && $entitlement->entitleable_id != $user->id) {
$users[] = $entitlement->entitleable_id;
+ } elseif ($entitlement->entitleable_type == Group::class) {
+ $groups[] = $entitlement->entitleable_id;
} else {
- $entitlements[] = $entitlement->id;
+ $entitlements[] = $entitlement;
}
}
- $users = array_unique($users);
- $domains = array_unique($domains);
-
// Domains/users/entitlements need to be deleted one by one to make sure
// events are fired and observers can do the proper cleanup.
if (!empty($users)) {
- foreach (User::whereIn('id', $users)->get() as $_user) {
+ foreach (User::whereIn('id', array_unique($users))->get() as $_user) {
$_user->delete();
}
}
if (!empty($domains)) {
- foreach (Domain::whereIn('id', $domains)->get() as $_domain) {
+ foreach (Domain::whereIn('id', array_unique($domains))->get() as $_domain) {
$_domain->delete();
}
}
- if (!empty($entitlements)) {
- Entitlement::whereIn('id', $entitlements)->delete();
+ if (!empty($groups)) {
+ foreach (Group::whereIn('id', array_unique($groups))->get() as $_group) {
+ $_group->delete();
+ }
+ }
+
+ foreach ($entitlements as $entitlement) {
+ $entitlement->delete();
}
// FIXME: What do we do with user wallets?
@@ -186,6 +202,7 @@
$assignments = Entitlement::withTrashed()->whereIn('wallet_id', $wallets)->get();
$entitlements = [];
$domains = [];
+ $groups = [];
$users = [];
foreach ($assignments as $entitlement) {
@@ -198,12 +215,11 @@
&& $entitlement->entitleable_id != $user->id
) {
$users[] = $entitlement->entitleable_id;
+ } elseif ($entitlement->entitleable_type == Group::class) {
+ $groups[] = $entitlement->entitleable_id;
}
}
- $users = array_unique($users);
- $domains = array_unique($domains);
-
// Remove the user "direct" entitlements explicitely, if they belong to another
// user's wallet they will not be removed by the wallets foreign key cascade
Entitlement::withTrashed()
@@ -213,14 +229,19 @@
// Users need to be deleted one by one to make sure observers can do the proper cleanup.
if (!empty($users)) {
- foreach (User::withTrashed()->whereIn('id', $users)->get() as $_user) {
+ foreach (User::withTrashed()->whereIn('id', array_unique($users))->get() as $_user) {
$_user->forceDelete();
}
}
// Domains can be just removed
if (!empty($domains)) {
- Domain::withTrashed()->whereIn('id', $domains)->forceDelete();
+ Domain::withTrashed()->whereIn('id', array_unique($domains))->forceDelete();
+ }
+
+ // Groups can be just removed
+ if (!empty($groups)) {
+ Group::withTrashed()->whereIn('id', array_unique($groups))->forceDelete();
}
// Remove transactions, they also have no foreign key constraint
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -29,6 +29,7 @@
\App\Discount::observe(\App\Observers\DiscountObserver::class);
\App\Domain::observe(\App\Observers\DomainObserver::class);
\App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
+ \App\Group::observe(\App\Observers\GroupObserver::class);
\App\Package::observe(\App\Observers\PackageObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\Plan::observe(\App\Observers\PlanObserver::class);
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -414,6 +414,23 @@
}
/**
+ * Return groups controlled by the current user.
+ *
+ * @return \Illuminate\Database\Eloquent\Builder Query builder
+ */
+ public function groups()
+ {
+ $wallets = $this->wallets()->pluck('id')->all();
+
+ $groupIds = \App\Entitlement::whereIn('entitlements.wallet_id', $wallets)
+ ->where('entitlements.entitleable_type', Group::class)
+ ->pluck('entitleable_id')
+ ->all();
+
+ return Group::whereIn('id', $groupIds);
+ }
+
+ /**
* Check if user has an entitlement for the specified SKU.
*
* @param string $title The SKU title
@@ -606,7 +623,7 @@
->distinct()
->leftJoin('entitlements', 'entitlements.entitleable_id', '=', 'users.id')
->whereIn('entitlements.wallet_id', $wallets)
- ->where('entitlements.entitleable_type', 'App\User');
+ ->where('entitlements.entitleable_type', User::class);
}
/**
@@ -622,11 +639,11 @@
/**
* Returns the wallet by which the user is controlled
*
- * @return \App\Wallet A wallet object
+ * @return ?\App\Wallet A wallet object
*/
- public function wallet(): Wallet
+ public function wallet(): ?Wallet
{
- $entitlement = $this->entitlement()->first();
+ $entitlement = $this->entitlement()->withTrashed()->first();
// TODO: No entitlement should not happen, but in tests we have
// such cases, so we fallback to the user's wallet in this case
diff --git a/src/database/migrations/2020_12_28_140000_create_groups_table.php b/src/database/migrations/2020_12_28_140000_create_groups_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_12_28_140000_create_groups_table.php
@@ -0,0 +1,55 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+// phpcs:ignore
+class CreateGroupsTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'groups',
+ function (Blueprint $table) {
+ $table->bigInteger('id');
+ $table->string('email')->unique();
+ $table->text('members')->nullable();
+ $table->smallInteger('status');
+
+ $table->timestamps();
+ $table->softDeletes();
+
+ $table->primary('id');
+ }
+ );
+
+ if (!\App\Sku::where('title', 'group')->first()) {
+ \App\Sku::create([
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ]);
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('groups');
+ }
+}
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -183,5 +183,21 @@
]
);
}
+
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'group')->first()) {
+ Sku::create(
+ [
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ]
+ );
+ }
}
}
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -183,5 +183,21 @@
]
);
}
+
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'group')->first()) {
+ Sku::create(
+ [
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ]
+ );
+ }
}
}
diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -121,6 +121,7 @@
'2fainvalid' => 'Second factor code is invalid.',
'emailinvalid' => 'The specified email address is invalid.',
'domaininvalid' => 'The specified domain is invalid.',
+ 'domainnotavailable' => 'The specified domain is not available.',
'logininvalid' => 'The specified login is invalid.',
'loginexists' => 'The specified login is not available.',
'domainexists' => 'The specified domain is not available.',
diff --git a/src/tests/Feature/Backends/LDAPTest.php b/src/tests/Feature/Backends/LDAPTest.php
--- a/src/tests/Feature/Backends/LDAPTest.php
+++ b/src/tests/Feature/Backends/LDAPTest.php
@@ -4,6 +4,7 @@
use App\Backends\LDAP;
use App\Domain;
+use App\Group;
use App\Entitlement;
use App\User;
use Illuminate\Support\Facades\Queue;
@@ -26,6 +27,8 @@
$this->deleteTestUser('user-ldap-test@' . \config('app.domain'));
$this->deleteTestDomain('testldap.com');
+ $this->deleteTestGroup('group@kolab.org');
+ // TODO: Remove group members
}
/**
@@ -37,6 +40,8 @@
$this->deleteTestUser('user-ldap-test@' . \config('app.domain'));
$this->deleteTestDomain('testldap.com');
+ $this->deleteTestGroup('group@kolab.org');
+ // TODO: Remove group members
parent::tearDown();
}
@@ -110,6 +115,88 @@
}
/**
+ * Test creating/updating/deleting a group record
+ *
+ * @group ldap
+ */
+ public function testGroup(): void
+ {
+ Queue::fake();
+
+ $root_dn = \config('ldap.hosted.root_dn');
+ $group = $this->getTestGroup('group@kolab.org', [
+ 'members' => ['member1@testldap.com', 'member2@testldap.com']
+ ]);
+
+ // Create the group
+ LDAP::createGroup($group);
+
+ $ldap_group = LDAP::getGroup($group->email);
+
+ $expected = [
+ 'cn' => 'group',
+ 'dn' => 'cn=group,ou=Groups,ou=kolab.org,' . $root_dn,
+ 'mail' => $group->email,
+ 'objectclass' => [
+ 'top',
+ 'groupofuniquenames',
+ 'kolabgroupofuniquenames'
+ ],
+ 'uniquemember' => [
+ 'uid=member1@testldap.com,ou=People,ou=kolab.org,' . $root_dn,
+ 'uid=member2@testldap.com,ou=People,ou=kolab.org,' . $root_dn,
+ ],
+ ];
+
+ foreach ($expected as $attr => $value) {
+ $this->assertEquals($value, isset($ldap_group[$attr]) ? $ldap_group[$attr] : null, "Group $attr attribute");
+ }
+
+ // Update members
+ $group->members = ['member3@testldap.com'];
+ $group->save();
+
+ LDAP::updateGroup($group);
+
+ // TODO: Should we force this to be always an array?
+ $expected['uniquemember'] = 'uid=member3@testldap.com,ou=People,ou=kolab.org,' . $root_dn;
+
+ $ldap_group = LDAP::getGroup($group->email);
+
+ foreach ($expected as $attr => $value) {
+ $this->assertEquals($value, isset($ldap_group[$attr]) ? $ldap_group[$attr] : null, "Group $attr attribute");
+ }
+
+ $this->assertSame(['member3@testldap.com'], $group->fresh()->members);
+
+ // Update members (add non-existing local member, expect it to be aot-removed from the group)
+ $group->members = ['member3@testldap.com', 'member-local@kolab.org'];
+ $group->save();
+
+ LDAP::updateGroup($group);
+
+ // TODO: Should we force this to be always an array?
+ $expected['uniquemember'] = 'uid=member3@testldap.com,ou=People,ou=kolab.org,' . $root_dn;
+
+ $ldap_group = LDAP::getGroup($group->email);
+
+ foreach ($expected as $attr => $value) {
+ $this->assertEquals($value, isset($ldap_group[$attr]) ? $ldap_group[$attr] : null, "Group $attr attribute");
+ }
+
+ $this->assertSame(['member3@testldap.com'], $group->fresh()->members);
+
+ // We called save() twice, so we expect two update obs, this is making sure
+ // that there's no job executed by the LDAP backend
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 2);
+
+ // Delete the domain
+ LDAP::deleteGroup($group);
+
+ $this->assertSame(null, LDAP::getGroup($group->email));
+ }
+
+ /**
* Test creating/editing/deleting a user record
*
* @group ldap
diff --git a/src/tests/Feature/Console/Group/AddMemberTest.php b/src/tests/Feature/Console/Group/AddMemberTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/AddMemberTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class AddMemberTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ // Non-existing group
+ $code = \Artisan::call("group:add-member test@group.com member@group.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Group test@group.com does not exist.", $output);
+
+ $group = Group::create(['email' => 'group-test@kolabnow.com']);
+
+ // Existing group, invalid member
+ $code = \Artisan::call("group:add-member {$group->email} member");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("member: The specified email address is invalid.", $output);
+
+ // Existing group
+ $code = \Artisan::call("group:add-member {$group->email} member@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame(['member@gmail.com'], $group->refresh()->members);
+
+ // Existing group
+ $code = \Artisan::call("group:add-member {$group->email} member2@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame(['member@gmail.com', 'member2@gmail.com'], $group->refresh()->members);
+
+ // Add a member that already exists
+ $code = \Artisan::call("group:add-member {$group->email} member@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("member@gmail.com: Already exists in the group.", $output);
+ $this->assertSame(['member@gmail.com', 'member2@gmail.com'], $group->refresh()->members);
+ }
+}
diff --git a/src/tests/Feature/Console/Group/CreateTest.php b/src/tests/Feature/Console/Group/CreateTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/CreateTest.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class CreateTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolab.org');
+ $this->deleteTestGroup('group-testm@kolab.org');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolab.org');
+ $this->deleteTestGroup('group-testm@kolab.org');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ $user = $this->getTestUser('john@kolab.org');
+
+ // Domain not existing
+ $code = \Artisan::call("group:create testgroup@unknown.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("No such domain unknown.org.", $output);
+
+ // Existing email
+ $code = \Artisan::call("group:create jack@kolab.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("jack@kolab.org: The specified email is not available.", $output);
+
+ // Existing email (of a user alias)
+ $code = \Artisan::call("group:create jack.daniels@kolab.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("jack.daniels@kolab.org: The specified email is not available.", $output);
+
+ // Public domain not allowed in the group email address
+ $code = \Artisan::call("group:create group-test@kolabnow.com");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("Domain kolabnow.com is public.", $output);
+
+ // Create a group without members
+ $code = \Artisan::call("group:create group-test@kolab.org");
+ $output = trim(\Artisan::output());
+ $group = Group::where('email', 'group-test@kolab.org')->first();
+
+ $this->assertSame(0, $code);
+ $this->assertEquals($group->id, $output);
+ $this->assertSame([], $group->members);
+ $this->assertSame($user->wallets->first()->id, $group->wallet()->id);
+
+ // Existing email (of a group)
+ $code = \Artisan::call("group:create group-test@kolab.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("group-test@kolab.org: The specified email is not available.", $output);
+
+ // Invalid member
+ $code = \Artisan::call("group:create group-testm@kolab.org --member=invalid");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("invalid: The specified email address is invalid.", $output);
+
+ // Valid members
+ $code = \Artisan::call(
+ "group:create group-testm@kolab.org --member=member1@kolabnow.com --member=member2@gmail.com"
+ );
+ $output = trim(\Artisan::output());
+ $group = Group::where('email', 'group-testm@kolab.org')->first();
+ $this->assertSame(0, $code);
+ $this->assertEquals($group->id, $output);
+ $this->assertSame(['member1@kolabnow.com', 'member2@gmail.com'], $group->members);
+ $this->assertSame($user->wallets->first()->id, $group->wallet()->id);
+ }
+}
diff --git a/src/tests/Feature/Console/Group/DeleteTest.php b/src/tests/Feature/Console/Group/DeleteTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/DeleteTest.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class DeleteTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestUser('group-owner@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestUser('group-owner@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ // Non-existing group
+ $code = \Artisan::call("group:delete test@group.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Group test@group.com does not exist.", $output);
+
+ $user = $this->getTestUser('group-owner@kolabnow.com');
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ // Existing group
+ $code = \Artisan::call("group:delete {$group->email}");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertTrue($group->refresh()->trashed());
+ }
+}
diff --git a/src/tests/Feature/Console/Group/InfoTest.php b/src/tests/Feature/Console/Group/InfoTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/InfoTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class InfoTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ $code = \Artisan::call("group:info unknown@unknown.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("Group unknown@unknown.org does not exist.", $output);
+
+ // A group without members
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ $expected = "Id: {$group->id}\nEmail: {$group->email}\nStatus: {$group->status}";
+
+ $code = \Artisan::call("group:info {$group->email}");
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame($expected, $output);
+
+ // Group with members
+ $group->members = ['test@member.com'];
+ $group->save();
+
+ $expected .= "\nMember: test@member.com";
+
+ $code = \Artisan::call("group:info {$group->email}");
+ $output = trim(\Artisan::output());
+ $this->assertSame(0, $code);
+ $this->assertSame($expected, $output);
+ }
+}
diff --git a/src/tests/Feature/Console/Group/RemoveMemberTest.php b/src/tests/Feature/Console/Group/RemoveMemberTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/RemoveMemberTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class RemoveMemberTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ // Non-existing group
+ $code = \Artisan::call("group:remove-member test@group.com member@group.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Group test@group.com does not exist.", $output);
+
+ $group = Group::create([
+ 'email' => 'group-test@kolabnow.com',
+ 'members' => ['member1@gmail.com', 'member2@gmail.com'],
+ ]);
+
+ // Existing group, non-existing member
+ $code = \Artisan::call("group:remove-member {$group->email} nonexisting@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Member nonexisting@gmail.com not found in the group.", $output);
+
+ // Existing group, existing member
+ $code = \Artisan::call("group:remove-member {$group->email} member1@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame(['member2@gmail.com'], $group->refresh()->members);
+
+ // Existing group, the last existing member
+ $code = \Artisan::call("group:remove-member {$group->email} member2@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame([], $group->refresh()->members);
+ }
+}
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -31,6 +31,8 @@
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
}
/**
@@ -45,6 +47,8 @@
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
parent::tearDown();
}
@@ -686,4 +690,21 @@
$this->assertSame($expected_result, $result);
}
+
+ /**
+ * Signup login/domain validation, more cases
+ *
+ * Note: Technically these include unit tests, but let's keep it here for now.
+ */
+ public function testValidateLoginMore(): void
+ {
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+ $login = 'group-test';
+ $domain = 'kolabnow.com';
+ $external = false;
+
+ $result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
+
+ $this->assertSame(['login' => 'The specified login is not available.'], $result);
+ }
}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -49,7 +49,7 @@
$json = $response->json();
- $this->assertCount(7, $json);
+ $this->assertCount(8, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
@@ -80,7 +80,7 @@
$json = $response->json();
- $this->assertCount(6, $json);
+ $this->assertCount(7, $json);
$this->assertSkuElement('mailbox', $json[0], [
'prio' => 100,
@@ -137,6 +137,14 @@
'readonly' => false,
]);
+ $this->assertSkuElement('group', $json[6], [
+ 'prio' => 0,
+ 'type' => 'group',
+ 'handler' => 'group',
+ 'enabled' => false,
+ 'readonly' => false,
+ ]);
+
// Test filter by type
$response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus?type=domain");
$response->assertStatus(200);
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -32,6 +32,8 @@
$this->deleteTestUser('deleted@kolab.org');
$this->deleteTestUser('deleted@kolabnow.com');
$this->deleteTestDomain('userscontroller.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestGroup('group-test@kolab.org');
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
@@ -56,6 +58,8 @@
$this->deleteTestUser('deleted@kolab.org');
$this->deleteTestUser('deleted@kolabnow.com');
$this->deleteTestDomain('userscontroller.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestGroup('group-test@kolab.org');
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
@@ -1126,6 +1130,45 @@
}
/**
+ * User email validation - tests for an address being a group email address
+ *
+ * Note: Technically these include unit tests, but let's keep it here for now.
+ * FIXME: Shall we do a http request for each case?
+ */
+ public function testValidateEmailGroup(): void
+ {
+ Queue::fake();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $pub_group = $this->getTestGroup('group-test@kolabnow.com');
+ $priv_group = $this->getTestGroup('group-test@kolab.org');
+
+ // A group in a public domain, existing
+ $result = UsersController::validateEmail($pub_group->email, $john, $deleted);
+ $this->assertSame('The specified email is not available.', $result);
+ $this->assertNull($deleted);
+
+ $pub_group->delete();
+
+ // A group in a public domain, deleted
+ $result = UsersController::validateEmail($pub_group->email, $john, $deleted);
+ $this->assertSame('The specified email is not available.', $result);
+ $this->assertNull($deleted);
+
+ // A group in a private domain, existing
+ $result = UsersController::validateEmail($priv_group->email, $john, $deleted);
+ $this->assertSame('The specified email is not available.', $result);
+ $this->assertNull($deleted);
+
+ $priv_group->delete();
+
+ // A group in a private domain, deleted
+ $result = UsersController::validateEmail($priv_group->email, $john, $deleted);
+ $this->assertSame(null, $result);
+ $this->assertSame($priv_group->id, $deleted->id);
+ }
+
+ /**
* List of alias validation cases for testValidateAlias()
*
* @return array Arguments for testValidateAlias()
@@ -1204,6 +1247,7 @@
$deleted_pub = $this->getTestUser('deleted@kolabnow.com');
$deleted_pub->setAliases(['deleted-alias@kolabnow.com']);
$deleted_pub->delete();
+ $group = $this->getTestGroup('group-test@kolabnow.com');
// An alias that was a user email before is allowed, but only for custom domains
$result = UsersController::validateAlias('deleted@kolab.org', $john);
@@ -1217,5 +1261,9 @@
$result = UsersController::validateAlias('deleted-alias@kolabnow.com', $john);
$this->assertSame('The specified alias is not available.', $result);
+
+ // A grpoup with the same email address exists
+ $result = UsersController::validateAlias($group->email, $john);
+ $this->assertSame('The specified alias is not available.', $result);
}
}
diff --git a/src/tests/Feature/GroupTest.php b/src/tests/Feature/GroupTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/GroupTest.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class GroupTest extends TestCase
+{
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('user-test@kolabnow.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ }
+
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('user-test@kolabnow.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Tests for Group::assignToWallet()
+ */
+ public function testAssignToWallet(): void
+ {
+ $user = $this->getTestUser('user-test@kolabnow.com');
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ $result = $group->assignToWallet($user->wallets->first());
+
+ $this->assertSame($group, $result);
+ $this->assertSame(1, $group->entitlement()->count());
+
+ // Can't be done twice on the same group
+ $this->expectException(\Exception::class);
+ $result->assignToWallet($user->wallets->first());
+ }
+
+ /**
+ * Test group status assignment and is*() methods
+ */
+ public function testStatus(): void
+ {
+ $group = new Group();
+
+ $this->assertSame(false, $group->isNew());
+ $this->assertSame(false, $group->isActive());
+ $this->assertSame(false, $group->isDeleted());
+ $this->assertSame(false, $group->isLdapReady());
+ $this->assertSame(false, $group->isSuspended());
+
+ $group->status = Group::STATUS_NEW;
+
+ $this->assertSame(true, $group->isNew());
+ $this->assertSame(false, $group->isActive());
+ $this->assertSame(false, $group->isDeleted());
+ $this->assertSame(false, $group->isLdapReady());
+ $this->assertSame(false, $group->isSuspended());
+
+ $group->status |= Group::STATUS_ACTIVE;
+
+ $this->assertSame(true, $group->isNew());
+ $this->assertSame(true, $group->isActive());
+ $this->assertSame(false, $group->isDeleted());
+ $this->assertSame(false, $group->isLdapReady());
+ $this->assertSame(false, $group->isSuspended());
+
+ $group->status |= Group::STATUS_LDAP_READY;
+
+ $this->assertSame(true, $group->isNew());
+ $this->assertSame(true, $group->isActive());
+ $this->assertSame(false, $group->isDeleted());
+ $this->assertSame(true, $group->isLdapReady());
+ $this->assertSame(false, $group->isSuspended());
+
+ $group->status |= Group::STATUS_DELETED;
+
+ $this->assertSame(true, $group->isNew());
+ $this->assertSame(true, $group->isActive());
+ $this->assertSame(true, $group->isDeleted());
+ $this->assertSame(true, $group->isLdapReady());
+ $this->assertSame(false, $group->isSuspended());
+
+ $group->status |= Group::STATUS_SUSPENDED;
+
+ $this->assertSame(true, $group->isNew());
+ $this->assertSame(true, $group->isActive());
+ $this->assertSame(true, $group->isDeleted());
+ $this->assertSame(true, $group->isLdapReady());
+ $this->assertSame(true, $group->isSuspended());
+
+ // Unknown status value
+ $this->expectException(\Exception::class);
+ $group->status = 111;
+ }
+
+ /**
+ * Test creating a group
+ */
+ public function testCreate(): void
+ {
+ Queue::fake();
+
+ $group = Group::create(['email' => 'GROUP-test@kolabnow.com']);
+
+ $this->assertSame('group-test@kolabnow.com', $group->email);
+ $this->assertRegExp('/^[0-9]{1,20}$/', $group->id);
+ $this->assertSame([], $group->members);
+ $this->assertTrue($group->isNew());
+ $this->assertTrue($group->isActive());
+
+ Queue::assertPushed(
+ \App\Jobs\Group\CreateJob::class,
+ function ($job) use ($group) {
+ $groupEmail = TestCase::getObjectProperty($job, 'groupEmail');
+ $groupId = TestCase::getObjectProperty($job, 'groupId');
+
+ return $groupEmail === $group->email
+ && $groupId === $group->id;
+ }
+ );
+ }
+
+ /**
+ * Test group deletion and force-deletion
+ */
+ public function testDelete(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('user-test@kolabnow.com');
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+ $group->assignToWallet($user->wallets->first());
+
+ $entitlements = \App\Entitlement::where('entitleable_id', $group->id);
+
+ $this->assertSame(1, $entitlements->count());
+
+ $group->delete();
+
+ $this->assertTrue($group->fresh()->trashed());
+ $this->assertSame(0, $entitlements->count());
+ $this->assertSame(1, $entitlements->withTrashed()->count());
+
+ $group->forceDelete();
+
+ $this->assertSame(0, $entitlements->withTrashed()->count());
+ $this->assertCount(0, Group::withTrashed()->where('id', $group->id)->get());
+
+ Queue::assertPushed(\App\Jobs\Group\DeleteJob::class, 1);
+ Queue::assertPushed(
+ \App\Jobs\Group\DeleteJob::class,
+ function ($job) use ($group) {
+ $groupEmail = TestCase::getObjectProperty($job, 'groupEmail');
+ $groupId = TestCase::getObjectProperty($job, 'groupId');
+
+ return $groupEmail === $group->email
+ && $groupId === $group->id;
+ }
+ );
+ }
+
+ /**
+ * Tests for Group::emailExists()
+ */
+ public function testEmailExists(): void
+ {
+ Queue::fake();
+
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ $this->assertFalse(Group::emailExists('unknown@domain.tld'));
+ $this->assertTrue(Group::emailExists($group->email));
+
+ $result = Group::emailExists($group->email, true);
+ $this->assertSame($result->id, $group->id);
+
+ $group->delete();
+
+ $this->assertTrue(Group::emailExists($group->email));
+
+ $result = Group::emailExists($group->email, true);
+ $this->assertSame($result->id, $group->id);
+ }
+
+ /**
+ * Tests for Group::suspend()
+ */
+ public function testSuspend(): void
+ {
+ Queue::fake();
+
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+ $group->suspend();
+
+ $this->assertTrue($group->isSuspended());
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1);
+ Queue::assertPushed(
+ \App\Jobs\Group\UpdateJob::class,
+ function ($job) use ($group) {
+ $groupEmail = TestCase::getObjectProperty($job, 'groupEmail');
+ $groupId = TestCase::getObjectProperty($job, 'groupId');
+
+ return $groupEmail === $group->email
+ && $groupId === $group->id;
+ }
+ );
+ }
+
+ /**
+ * Test updating a group
+ */
+ public function testUpdate(): void
+ {
+ Queue::fake();
+
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+ $group->status |= Group::STATUS_DELETED;
+ $group->save();
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1);
+ Queue::assertPushed(
+ \App\Jobs\Group\UpdateJob::class,
+ function ($job) use ($group) {
+ $groupEmail = TestCase::getObjectProperty($job, 'groupEmail');
+ $groupId = TestCase::getObjectProperty($job, 'groupId');
+
+ return $groupEmail === $group->email
+ && $groupId === $group->id;
+ }
+ );
+ }
+
+ /**
+ * Tests for Group::unsuspend()
+ */
+ public function testUnsuspend(): void
+ {
+ Queue::fake();
+
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+ $group->status = Group::STATUS_SUSPENDED;
+ $group->unsuspend();
+
+ $this->assertFalse($group->isSuspended());
+
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 1);
+ Queue::assertPushed(
+ \App\Jobs\Group\UpdateJob::class,
+ function ($job) use ($group) {
+ $groupEmail = TestCase::getObjectProperty($job, 'groupEmail');
+ $groupId = TestCase::getObjectProperty($job, 'groupId');
+
+ return $groupEmail === $group->email
+ && $groupId === $group->id;
+ }
+ );
+ }
+}
diff --git a/src/tests/Feature/Jobs/DomainCreateTest.php b/src/tests/Feature/Jobs/DomainCreateTest.php
--- a/src/tests/Feature/Jobs/DomainCreateTest.php
+++ b/src/tests/Feature/Jobs/DomainCreateTest.php
@@ -3,7 +3,6 @@
namespace Tests\Feature\Jobs;
use App\Domain;
-use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
diff --git a/src/tests/Feature/Jobs/DomainVerifyTest.php b/src/tests/Feature/Jobs/DomainVerifyTest.php
--- a/src/tests/Feature/Jobs/DomainVerifyTest.php
+++ b/src/tests/Feature/Jobs/DomainVerifyTest.php
@@ -3,7 +3,6 @@
namespace Tests\Feature\Jobs;
use App\Domain;
-use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class DomainVerifyTest extends TestCase
diff --git a/src/tests/Feature/Jobs/Group/CreateTest.php b/src/tests/Feature/Jobs/Group/CreateTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Jobs/Group/CreateTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Tests\Feature\Jobs\Group;
+
+use App\Group;
+use Tests\TestCase;
+
+class CreateTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group@kolab.org');
+ }
+
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group@kolab.org');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test job handle
+ *
+ * @group ldap
+ */
+ public function testHandle(): void
+ {
+ $group = $this->getTestGroup('group@kolab.org', ['members' => []]);
+
+ $this->assertFalse($group->isLdapReady());
+
+ $job = new \App\Jobs\Group\CreateJob($group->id);
+ $job->handle();
+
+ $this->assertTrue($group->fresh()->isLdapReady());
+ }
+}
diff --git a/src/tests/Feature/Jobs/Group/DeleteTest.php b/src/tests/Feature/Jobs/Group/DeleteTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Jobs/Group/DeleteTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Tests\Feature\Jobs\Group;
+
+use App\Group;
+use Tests\TestCase;
+
+class DeleteTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group@kolab.org');
+ }
+
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group@kolab.org');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test job handle
+ *
+ * @group ldap
+ */
+ public function testHandle(): void
+ {
+ $group = $this->getTestGroup('group@kolab.org', [
+ 'members' => [],
+ 'status' => Group::STATUS_NEW
+ ]);
+
+ // create to domain first
+ $job = new \App\Jobs\Group\CreateJob($group->id);
+ $job->handle();
+
+ $this->assertTrue($group->fresh()->isLdapReady());
+
+ $job = new \App\Jobs\Group\DeleteJob($group->id);
+ $job->handle();
+
+ $group->refresh();
+
+ $this->assertFalse($group->isLdapReady());
+ $this->assertTrue($group->isDeleted());
+ }
+}
diff --git a/src/tests/Feature/Jobs/Group/UpdateTest.php b/src/tests/Feature/Jobs/Group/UpdateTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Jobs/Group/UpdateTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Tests\Feature\Jobs\Group;
+
+use App\Group;
+use Tests\TestCase;
+
+class UpdateTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group@kolab.org');
+ }
+
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group@kolab.org');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test job handle
+ *
+ * @group ldap
+ */
+ public function testHandle(): void
+ {
+ $group = $this->getTestGroup('group@kolab.org', ['members' => []]);
+
+ $job = new \App\Jobs\Group\UpdateJob($group->id);
+ $job->handle();
+
+ // TODO: Test if group properties (members) actually changed in LDAP
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -3,6 +3,7 @@
namespace Tests\Feature;
use App\Domain;
+use App\Group;
use App\User;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
@@ -17,6 +18,7 @@
$this->deleteTestUser('UserAccountA@UserAccount.com');
$this->deleteTestUser('UserAccountB@UserAccount.com');
$this->deleteTestUser('UserAccountC@UserAccount.com');
+ $this->deleteTestGroup('test-group@UserAccount.com');
$this->deleteTestDomain('UserAccount.com');
}
@@ -26,6 +28,7 @@
$this->deleteTestUser('UserAccountA@UserAccount.com');
$this->deleteTestUser('UserAccountB@UserAccount.com');
$this->deleteTestUser('UserAccountC@UserAccount.com');
+ $this->deleteTestGroup('test-group@UserAccount.com');
$this->deleteTestDomain('UserAccount.com');
parent::tearDown();
@@ -235,7 +238,7 @@
$this->assertCount(0, User::withTrashed()->where('id', $id)->get());
- // Test an account with users
+ // Test an account with users, domain, and group
$userA = $this->getTestUser('UserAccountA@UserAccount.com');
$userB = $this->getTestUser('UserAccountB@UserAccount.com');
$userC = $this->getTestUser('UserAccountC@UserAccount.com');
@@ -249,15 +252,20 @@
$domain->assignPackage($package_domain, $userA);
$userA->assignPackage($package_kolab, $userB);
$userA->assignPackage($package_kolab, $userC);
+ $group = $this->getTestGroup('test-group@UserAccount.com');
+ $group->assignToWallet($userA->wallets->first());
$entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id);
$entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id);
$entitlementsC = \App\Entitlement::where('entitleable_id', $userC->id);
$entitlementsDomain = \App\Entitlement::where('entitleable_id', $domain->id);
+ $entitlementsGroup = \App\Entitlement::where('entitleable_id', $group->id);
+
$this->assertSame(4, $entitlementsA->count());
$this->assertSame(4, $entitlementsB->count());
$this->assertSame(4, $entitlementsC->count());
$this->assertSame(1, $entitlementsDomain->count());
+ $this->assertSame(1, $entitlementsGroup->count());
// Delete non-controller user
$userC->delete();
@@ -272,12 +280,54 @@
$this->assertSame(0, $entitlementsA->count());
$this->assertSame(0, $entitlementsB->count());
$this->assertSame(0, $entitlementsDomain->count());
+ $this->assertSame(0, $entitlementsGroup->count());
$this->assertTrue($userA->fresh()->trashed());
$this->assertTrue($userB->fresh()->trashed());
$this->assertTrue($domain->fresh()->trashed());
+ $this->assertTrue($group->fresh()->trashed());
$this->assertFalse($userA->isDeleted());
$this->assertFalse($userB->isDeleted());
$this->assertFalse($domain->isDeleted());
+ $this->assertFalse($group->isDeleted());
+
+ $userA->forceDelete();
+
+ $all_entitlements = \App\Entitlement::where('wallet_id', $userA->wallets->first()->id);
+
+ $this->assertSame(0, $all_entitlements->withTrashed()->count());
+ $this->assertCount(0, User::withTrashed()->where('id', $userA->id)->get());
+ $this->assertCount(0, User::withTrashed()->where('id', $userB->id)->get());
+ $this->assertCount(0, User::withTrashed()->where('id', $userC->id)->get());
+ $this->assertCount(0, Domain::withTrashed()->where('id', $domain->id)->get());
+ $this->assertCount(0, Group::withTrashed()->where('id', $group->id)->get());
+ }
+
+ /**
+ * Test user deletion vs. group membership
+ */
+ public function testDeleteAandGroups(): void
+ {
+ Queue::fake();
+
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $userA = $this->getTestUser('UserAccountA@UserAccount.com');
+ $userB = $this->getTestUser('UserAccountB@UserAccount.com');
+ $userA->assignPackage($package_kolab, $userB);
+ $group = $this->getTestGroup('test-group@UserAccount.com');
+ $group->members = ['test@gmail.com', $userB->email];
+ $group->assignToWallet($userA->wallets->first());
+ $group->save();
+
+ $userGroups = $userA->groups()->get();
+ $this->assertSame(1, $userGroups->count());
+ $this->assertSame($group->id, $userGroups->first()->id);
+
+ $userB->delete();
+
+ $this->assertSame(['test@gmail.com'], $group->fresh()->members);
+
+ // Twice, one for save() and one for delete() above
+ Queue::assertPushed(\App\Jobs\Group\UpdateJob::class, 2);
}
/**
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -3,6 +3,7 @@
namespace Tests;
use App\Domain;
+use App\Group;
use App\Transaction;
use App\User;
use Carbon\Carbon;
@@ -149,6 +150,22 @@
$domain->forceDelete();
}
+ protected function deleteTestGroup($email)
+ {
+ Queue::fake();
+
+ $group = Group::withTrashed()->where('email', $email)->first();
+
+ if (!$group) {
+ return;
+ }
+
+ $job = new \App\Jobs\Group\DeleteJob($group->id);
+ $job->handle();
+
+ $group->forceDelete();
+ }
+
protected function deleteTestUser($email)
{
Queue::fake();
@@ -177,6 +194,17 @@
}
/**
+ * Get Group object by email, create it if needed.
+ * Skip LDAP jobs.
+ */
+ protected function getTestGroup($email, $attrib = [])
+ {
+ // Disable jobs (i.e. skip LDAP oprations)
+ Queue::fake();
+ return Group::firstOrCreate(['email' => $email], $attrib);
+ }
+
+ /**
* Get User object by email, create it if needed.
* Skip LDAP jobs.
*/

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 12:41 PM (16 h, 36 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828248
Default Alt Text
D2020.1775392902.diff (87 KB)

Event Timeline