Page MenuHomePhorge

D5286.1775394498.diff
No OneTemporary

Authored By
Unknown
Size
20 KB
Referenced Files
None
Subscribers
None

D5286.1775394498.diff

diff --git a/src/app/Console/Commands/Data/Import/LdifCommand.php b/src/app/Console/Commands/Data/Import/LdifCommand.php
--- a/src/app/Console/Commands/Data/Import/LdifCommand.php
+++ b/src/app/Console/Commands/Data/Import/LdifCommand.php
@@ -3,6 +3,7 @@
namespace App\Console\Commands\Data\Import;
use App\Console\Command;
+use App\Delegation;
use App\Domain;
use App\Group;
use App\Package;
@@ -11,6 +12,7 @@
use App\Sku;
use App\Tenant;
use App\User;
+use App\UserPassword;
use App\Utils;
use App\Wallet;
use Illuminate\Database\Schema\Blueprint;
@@ -36,6 +38,9 @@
/** @var array Aliases email addresses of the owner */
protected $aliases = [];
+ /** @var array Delegation information */
+ protected $delegations = [];
+
/** @var array List of imported domains */
protected $domains = [];
@@ -103,6 +108,7 @@
// Import other objects
$this->importUsers();
+ $this->importDelegations();
$this->importSharedFolders();
$this->importResources();
$this->importGroups();
@@ -179,10 +185,10 @@
[$attr, $remainder] = explode(':', $line, 2);
$attr = strtolower($attr);
- if ($remainder[0] === ':') {
+ if (isset($remainder[0]) && $remainder[0] === ':') {
$remainder = base64_decode(substr($remainder, 2));
} else {
- $remainder = ltrim($remainder);
+ $remainder = ltrim((string) $remainder);
}
if (array_key_exists($attr, $entry)) {
@@ -228,7 +234,7 @@
}
$this->wallet->owner->contacts()->create([
- 'name' => $data->name,
+ 'name' => $data->name ?? null,
'email' => $data->email,
]);
}
@@ -238,6 +244,32 @@
$this->info("DONE");
}
+ /**
+ * Import delegations
+ */
+ protected function importDelegations(): void
+ {
+ $bar = $this->createProgressBar(count($this->delegations), "Importing delegations");
+
+ foreach ($this->delegations as $user_id => $delegates) {
+ $bar->advance();
+
+ foreach ($this->resolveUserDNs($delegates, true) as $id) {
+ $delegation = new Delegation();
+ $delegation->user_id = $user_id;
+ $delegation->delegatee_id = $id;
+ // FIXME: Should we set any options? For existing delegations just the relation might be enough.
+ // We don't want to give more permissions than intended.
+ $delegation->options = [];
+ $delegation->save();
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
/**
* Import domains from the temp table
*/
@@ -562,7 +594,9 @@
return;
}
- $user = User::create(['email' => $data->email]);
+ $user = new User();
+ $user->setRawAttributes(['email' => $data->email, 'password_ldap' => $data->password]);
+ $user->save();
// Entitlements
$user->assignPackageAndWallet($this->packages['user'], $this->wallet ?: $user->wallets()->first());
@@ -588,11 +622,6 @@
DB::table('user_settings')->insert($settings);
}
- // Update password
- if ($data->password != $user->password_ldap) {
- User::where('id', $user->id)->update(['password_ldap' => $data->password]);
- }
-
// Import aliases
if (!empty($data->aliases)) {
if (!$this->wallet) {
@@ -604,6 +633,28 @@
}
}
+ // Old passwords
+ if (!empty($data->passwords)) {
+ // Note: We'll import all old passwords even if account policy has a different limit
+ $passwords = array_map(
+ function ($pass) use ($user) {
+ return [
+ 'created_at' => $pass[0],
+ 'password' => $pass[1],
+ 'user_id' => $user->id,
+ ];
+ },
+ $data->passwords
+ );
+
+ UserPassword::insert($passwords);
+ }
+
+ // Collect delegation info tobe imported later
+ if (!empty($data->delegates)) {
+ $this->delegations[$user->id] = $data->delegates;
+ }
+
return $user;
}
@@ -633,6 +684,14 @@
'Domains' => 'domain',
];
+ // Skip entries with these classes
+ $ignoreByClass = [
+ 'cossuperdefinition',
+ 'extensibleobject',
+ 'nscontainer',
+ 'nsroledefinition',
+ ];
+
// Ignore LDIF header
if (!empty($entry['version'])) {
return null;
@@ -640,15 +699,17 @@
if (!isset($entry['objectclass'])) {
$entry['objectclass'] = [];
+ } else {
+ $entry['objectclass'] = array_map('strtolower', (array) $entry['objectclass']);
}
// Skip non-importable entries
- if (
- preg_match('/uid=(cyrus-admin|kolab-service)/', $entry['dn'])
- || in_array('nsroledefinition', $entry['objectclass'])
- || in_array('organizationalUnit', $entry['objectclass'])
- || in_array('organizationalunit', $entry['objectclass'])
- ) {
+ if (count(array_intersect($entry['objectclass'], $ignoreByClass)) > 0) {
+ return null;
+ }
+
+ // Skip special entries
+ if (preg_match('/uid=(cyrus-admin|kolab-service)/', $entry['dn'])) {
return null;
}
@@ -684,7 +745,7 @@
}
// Silently ignore groups with no 'mail' attribute
- if ($type == 'group' && empty($entry['mail'])) {
+ if (empty($entry['mail']) && $type == 'group') {
return null;
}
@@ -716,7 +777,9 @@
if (empty($entry['mail'])) {
$error = "Missing 'mail' attribute";
} else {
- if (!empty($entry['cn'])) {
+ if (!empty($entry['displayname'])) {
+ $result['name'] = $this->attrStringValue($entry, 'displayname');
+ } elseif (!empty($entry['cn'])) {
$result['name'] = $this->attrStringValue($entry, 'cn');
}
@@ -745,6 +808,8 @@
}
} elseif (!empty($entry['dn']) && str_starts_with($entry['dn'], 'dc=')) {
$result['namespace'] = strtolower(str_replace(['dc=', ','], ['', '.'], $entry['dn']));
+ } elseif (!empty($entry['ou']) && preg_match('/^[a-zA-Z0-9.]+\.[a-zA-Z]+$/', $entry['ou'])) {
+ $result['namespace'] = strtolower($entry['ou']);
} else {
$error = "Missing 'associatedDomain' and 'dn' attribute";
}
@@ -873,6 +938,8 @@
$result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
$result['settings'] = [];
$result['aliases'] = [];
+ $result['delegates'] = [];
+ $result['passwords'] = [];
foreach ($settingAttrs as $attr => $setting) {
if (!empty($entry[$attr])) {
@@ -884,10 +951,27 @@
$result['aliases'] = $this->attrArrayValue($entry, 'alias');
}
+ if (!empty($entry['kolabdelegate'])) {
+ $result['delegates'] = $this->attrArrayValue($entry, 'kolabdelegate');
+ }
+
if (!empty($entry['userpassword'])) {
$result['password'] = $this->attrStringValue($entry, 'userpassword');
}
+ if (!empty($entry['passwordhistory'])) {
+ $regexp = '/^([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})Z(\{.*)$/';
+ foreach ($this->attrArrayValue($entry, 'passwordhistory') as $pass) {
+ if (preg_match($regexp, $pass, $matches)) {
+ $result['passwords'][] = [
+ $matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' '
+ . $matches[4] . ':' . $matches[5] . ':' . $matches[6],
+ $matches[7],
+ ];
+ }
+ }
+ }
+
if (!empty($entry['mailquota'])) {
$result['quota'] = $this->attrStringValue($entry, 'mailquota');
}
@@ -951,8 +1035,10 @@
/**
* Resolve a list of user DNs into email addresses. Makes sure
* the returned addresses exist in Kolab4 database.
+ *
+ * @param bool $return_ids Return user IDs instead of email addresses
*/
- protected function resolveUserDNs($user_dns): array
+ protected function resolveUserDNs($user_dns, $return_ids = false): array
{
// Get email addresses from the import data
$users = DB::table(self::$table)->whereIn('dn', $user_dns)
@@ -971,7 +1057,7 @@
// Get email addresses for existing Kolab4 users
if (!empty($users)) {
- $users = User::whereIn('email', $users)->get()->pluck('email')->all();
+ $users = User::whereIn('email', $users)->get()->pluck($return_ids ? 'id' : 'email')->all();
}
return $users;
@@ -1001,6 +1087,7 @@
$rights = $map[$rights] ?? $rights;
if (in_array($rights, $supportedRights) && ($label === 'anyone' || strpos($label, '@'))) {
+ $label = strtolower($label);
$entry = "{$label}, {$rights}";
}
@@ -1047,7 +1134,7 @@
// 'deny' rules aren't supported
if (isset($entry[0]) && $entry[0] !== '-') {
- $rule = $entry;
+ $rule = strtolower($entry);
}
$rules[$idx] = $rule;
@@ -1115,7 +1202,10 @@
}
if (!empty($aliases)) {
- $object->setAliases($aliases);
+ $class = $object::class . 'Alias';
+ $aliases = array_map(fn ($alias) => new $class(['alias' => $alias]), $aliases);
+
+ $object->aliases()->saveManyQuietly($aliases);
}
}
}
diff --git a/src/database/migrations/2025_05_30_100000_ldap_password_length_change.php b/src/database/migrations/2025_05_30_100000_ldap_password_length_change.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2025_05_30_100000_ldap_password_length_change.php
@@ -0,0 +1,35 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+ /**
+ * Run the migrations.
+ */
+ public function up()
+ {
+ Schema::table(
+ 'users',
+ static function (Blueprint $table) {
+ $table->string('password_ldap', 512)->nullable()->change();
+ }
+ );
+
+ Schema::table(
+ 'user_passwords',
+ static function (Blueprint $table) {
+ $table->string('password', 512)->change();
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down()
+ {
+ // Note: We can't set the length to a smaller value if there are already entries that are long
+ }
+};
diff --git a/src/tests/Feature/Console/Data/Import/LdifTest.php b/src/tests/Feature/Console/Data/Import/LdifTest.php
--- a/src/tests/Feature/Console/Data/Import/LdifTest.php
+++ b/src/tests/Feature/Console/Data/Import/LdifTest.php
@@ -8,6 +8,7 @@
use App\Resource;
use App\SharedFolder;
use App\User;
+use Illuminate\Support\Collection;
use Symfony\Component\Console\Input\ArrayInput;
use Tests\TestCase;
@@ -88,42 +89,61 @@
]);
// Users
- $this->assertSame(2, $owner->users(false)->count());
- /** @var User $user */
- $user = $owner->users(false)->where('email', 'user@kolab3.com')->first();
+ /** @var Collection<User> $users */
+ $users = $owner->users(false)->orderBy('email')->get()->keyBy('email');
+ $this->assertCount(4, $users);
// User settings
- $this->assertSame('Jane', $user->getSetting('first_name'));
- $this->assertSame('Doe', $user->getSetting('last_name'));
- $this->assertSame('1234567890', $user->getSetting('phone'));
- $this->assertSame('ext@gmail.com', $user->getSetting('external_email'));
- $this->assertSame('Org AG', $user->getSetting('organization'));
+ $this->assertSame('Jane', $users['user@kolab3.com']->getSetting('first_name'));
+ $this->assertSame('Doe', $users['user@kolab3.com']->getSetting('last_name'));
+ $this->assertSame('1234567890', $users['user@kolab3.com']->getSetting('phone'));
+ $this->assertSame('ext@gmail.com', $users['user@kolab3.com']->getSetting('external_email'));
+ $this->assertSame('Org AG', $users['user@kolab3.com']->getSetting('organization'));
// User aliases
- $aliases = $user->aliases()->orderBy('alias')->pluck('alias')->all();
+ $aliases = $users['user@kolab3.com']->aliases()->orderBy('alias')->pluck('alias')->all();
$this->assertSame(['alias2@kolab3.com'], $aliases);
- $this->assertEntitlements($user, [
+ $this->assertEntitlements($users['user@kolab3.com'], [
'groupware',
'mailbox',
'storage', 'storage', 'storage', 'storage', 'storage',
]);
+ // User passwords history
+ $passwords = $users['user@kolab3.com']->passwords()->orderBy('created_at')->get();
+ $this->assertCount(2, $passwords);
+ $this->assertSame('2023-07-05 14:16:28', $passwords[0]->created_at->format('Y-m-d H:i:s'));
+ $this->assertSame('{PBKDF2_SHA256}AAAIAF', $passwords[0]->password);
+ $this->assertSame('2023-10-04 08:35:39', $passwords[1]->created_at->format('Y-m-d H:i:s'));
+ $this->assertSame('{PBKDF2_SHA256}AAAIAB', $passwords[1]->password);
+
+ // User delegation
+ /** @var Collection<User> $delegates */
+ $delegates = $users['user@kolab3.com']->delegatees()->orderBy('email')->get()->keyBy('email');
+ $this->assertCount(2, $delegates);
+ $this->assertSame(['user2@kolab3.com', 'user3@kolab3.com'], $delegates->pluck('email')->all());
+ $this->assertCount(0, $users['user2@kolab3.com']->delegatees()->get());
+ $this->assertCount(0, $users['user3@kolab3.com']->delegatees()->get());
+
// Domains
- /** @var Domain[] $domains */
+ /** @var Collection<Domain> $domains */
$domains = $owner->domains(false, false)->orderBy('namespace')->get();
- $this->assertCount(2, $domains);
+ $this->assertCount(3, $domains);
$this->assertSame('kolab3-alias.com', $domains[0]->namespace);
$this->assertSame('kolab3.com', $domains[1]->namespace);
+ $this->assertSame('my.kolab3.com', $domains[2]->namespace);
$this->assertSame(Domain::TYPE_EXTERNAL, $domains[0]->type);
$this->assertSame(Domain::TYPE_EXTERNAL, $domains[1]->type);
+ $this->assertSame(Domain::TYPE_EXTERNAL, $domains[2]->type);
$this->assertEntitlements($domains[0], ['domain-hosting']);
$this->assertEntitlements($domains[1], ['domain-hosting']);
+ $this->assertEntitlements($domains[2], ['domain-hosting']);
// Shared folders
- /** @var SharedFolder[] $folders */
+ /** @var Collection<SharedFolder> $folders */
$folders = $owner->sharedFolders(false)->orderBy('email')->get();
$this->assertCount(2, $folders);
@@ -144,7 +164,7 @@
);
// Groups
- /** @var Group[] $groups */
+ /** @var Collection<Group> $groups */
$groups = $owner->groups(false)->orderBy('email')->get();
$this->assertCount(1, $groups);
@@ -154,7 +174,7 @@
$this->assertSame('["sender@gmail.com","-"]', $groups[0]->getSetting('sender_policy'));
// Resources
- /** @var Resource[] $resources */
+ /** @var Collection<Resource> $resources */
$resources = $owner->resources(false)->orderBy('email')->get();
$this->assertCount(1, $resources);
@@ -268,6 +288,11 @@
$result = $this->invokeMethod($command, 'parseLDAPContact', [$entry]);
$this->assertSame(['name' => 'Test', 'email' => 'test@test.com'], $result[0]);
$this->assertNull($result[1]);
+
+ $entry = ['mail' => ['test@test.com'], 'cn' => 'Test', 'displayname' => 'Display Name'];
+ $result = $this->invokeMethod($command, 'parseLDAPContact', [$entry]);
+ $this->assertSame(['name' => 'Display Name', 'email' => 'test@test.com'], $result[0]);
+ $this->assertNull($result[1]);
}
/**
@@ -287,6 +312,16 @@
$this->assertSame(['namespace' => 'test.com'], $result[0]);
$this->assertNull($result[1]);
+ $entry = ['ou' => 'sub.test.com'];
+ $result = $this->invokeMethod($command, 'parseLDAPDomain', [$entry]);
+ $this->assertSame(['namespace' => 'sub.test.com'], $result[0]);
+ $this->assertNull($result[1]);
+
+ $entry = ['dn' => 'dc=test,dc=kolab,dc=org'];
+ $result = $this->invokeMethod($command, 'parseLDAPDomain', [$entry]);
+ $this->assertSame(['namespace' => 'test.kolab.org'], $result[0]);
+ $this->assertNull($result[1]);
+
$entry = ['associateddomain' => 'test.com', 'inetdomainstatus' => 'deleted'];
$result = $this->invokeMethod($command, 'parseLDAPDomain', [$entry]);
$this->assertSame([], $result[0]);
@@ -454,6 +489,8 @@
'organization' => 'Org',
],
'aliases' => ['test1@domain.tld', 'test2@domain.tld'],
+ 'delegates' => [],
+ 'passwords' => [],
'password' => 'pass',
'quota' => '12345678',
];
diff --git a/src/tests/data/kolab3.ldif b/src/tests/data/kolab3.ldif
--- a/src/tests/data/kolab3.ldif
+++ b/src/tests/data/kolab3.ldif
@@ -1,3 +1,6 @@
+version: 1
+
+# entry-id: 1
dn: associateddomain=kolab3.com,ou=Domains,dc=hosted,dc=com
objectClass: top
objectClass: domainrelatedobject
@@ -6,6 +9,17 @@
associatedDomain: kolab3.com
associatedDomain: kolab3-alias.com
+# entry-id: 2
+dn: ou=my.kolab3.com,ou=Domains,dc=hosted,dc=com
+modifyTimestamp: 20220912130615Z
+modifiersName: cn=directory manager
+nsUniqueId: ed008f0d-21fe11ed-90dee5c5-e8b7dc42
+ou: my.kolab3.com
+objectClass: top
+objectClass: organizationalunit
+creatorsName: cn=directory manager
+createTimestamp: 20220822094441Z
+
dn: uid=owner@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
cn: Aleksander Machniak
displayName: Machniak, Aleksander
@@ -56,6 +70,33 @@
uid: user@kolab3.com
userPassword:: e1NTSEE1MTJ9Zzc0K1NFQ1RMc00xeDBhWWtTclRHOXNPRnpFcDh3akNmbGhzaHIyRGpFN21pMUczaU5iNENsSDNsam9yUFJsVGdaMTA1UHNRR0VwTnROcitYUmppZ2c9PQ==
nsUniqueID: 229dc20c-1b6a11f7-b7c1edc1-0e0f46c4
+kolabDelegate: uid=user2@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+kolabDelegate: uid=user3@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+passwordHistory: 20230705141628Z{PBKDF2_SHA256}AAAIAF
+passwordHistory: 20231004083539Z{PBKDF2_SHA256}AAAIAB
+
+dn: uid=user2@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+cn: Jack Rian
+objectClass: top
+objectClass: inetorgperson
+objectClass: kolabinetorgperson
+objectClass: organizationalperson
+objectClass: mailrecipient
+objectClass: person
+mail: user2@kolab3.com
+uid: user2@kolab3.com
+userPassword:: e1NTSEE1MTJ9Zzc0K1NFQ1RMc00xeDBhWWtTclRHOXNPRnpFcDh3akNmbGhzaHIyRGpFN21pMUczaU5iNENsSDNsam9yUFJsVGdaMTA1UHNRR0VwTnROcitYUmppZ2c9PQ==
+nsUniqueID: 229dc20c-1b6a11f7-b7c1edc1-0e0f46c5
+
+dn: uid=user3@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+cn: User3
+objectClass: top
+objectClass: inetorgperson
+objectClass: kolabinetorgperson
+mail: user3@kolab3.com
+uid: user3@kolab3.com
+userPassword:: e1NTSEE1MTJ9Zzc0K1NFQ1RMc00xeDBhWWtTclRHOXNPRnpFcDh3akNmbGhzaHIyRGpFN21pMUczaU5iNENsSDNsam9yUFJsVGdaMTA1UHNRR0VwTnROcitYUmppZ2c9PQ==
+nsUniqueID: 229dc20c-1b6a11f7-b7c1edc1-0e0f46c6
dn: cn=Group,ou=Groups,ou=kolab3.com,dc=hosted,dc=com
cn: Group
@@ -66,7 +107,7 @@
uniqueMember: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
uniqueMember: uid=owner@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
kolabAllowSMTPRecipient: recipient@kolab.org
-kolabAllowSMTPSender: sender@gmail.com
+kolabAllowSMTPSender: Sender@gmail.com
dn: cn=Error,ou=Groups,ou=kolab3.com,dc=hosted,dc=com
cn: Error
@@ -108,7 +149,7 @@
alias: folder-alias1@kolab3.com
alias: folder-alias2@kolab3.com
acl: anyone, read-write
-acl: owner@kolab3.com, full
+acl: Owner@kolab3.com, full
dn: cn=Folder2,ou=Shared Folders,ou=kolab3.com,dc=hosted,dc=com
cn: Folder2

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 1:08 PM (19 h, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830534
Default Alt Text
D5286.1775394498.diff (20 KB)

Event Timeline