Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117867621
D5286.1775322967.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
20 KB
Referenced Files
None
Subscribers
None
D5286.1775322967.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 5:16 PM (9 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830534
Default Alt Text
D5286.1775322967.diff (20 KB)
Attached To
Mode
D5286: LDIF import improvements/fixes
Attached
Detach File
Event Timeline