mindoodb 0.0.72 → 0.0.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/index.mjs +2 -0
- package/dist/core/BackgroundTaskTracker.d.ts +18 -0
- package/dist/core/BackgroundTaskTracker.d.ts.map +1 -0
- package/dist/core/BackgroundTaskTracker.js +37 -0
- package/dist/core/BackgroundTaskTracker.js.map +1 -0
- package/dist/core/BaseMindooDB.d.ts +60 -7
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +280 -66
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +46 -12
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +133 -65
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +57 -14
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +319 -164
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +11 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +44 -18
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/purgeDocumentHistory.d.ts +52 -0
- package/dist/core/appendonlystores/purgeDocumentHistory.d.ts.map +1 -0
- package/dist/core/appendonlystores/purgeDocumentHistory.js +107 -0
- package/dist/core/appendonlystores/purgeDocumentHistory.js.map +1 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.d.ts +33 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.d.ts.map +1 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.js +78 -0
- package/dist/core/appendonlystores/purgedDocumentGuard.js.map +1 -0
- package/dist/core/cache/CacheManager.d.ts +4 -1
- package/dist/core/cache/CacheManager.d.ts.map +1 -1
- package/dist/core/cache/CacheManager.js +47 -19
- package/dist/core/cache/CacheManager.js.map +1 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts +16 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.d.ts.map +1 -1
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js +86 -14
- package/dist/core/indexing/fulltext/DocumentFullTextIndex.js.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts +2 -0
- package/dist/core/indexing/summary/DocumentSummaryStore.d.ts.map +1 -1
- package/dist/core/indexing/summary/DocumentSummaryStore.js +19 -5
- package/dist/core/indexing/summary/DocumentSummaryStore.js.map +1 -1
- package/dist/core/types.d.ts +57 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +22 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils/singleFlight.d.ts +21 -0
- package/dist/core/utils/singleFlight.d.ts.map +1 -0
- package/dist/core/utils/singleFlight.js +47 -0
- package/dist/core/utils/singleFlight.js.map +1 -0
- package/dist/core/utils/timeSource.d.ts +3 -1
- package/dist/core/utils/timeSource.d.ts.map +1 -1
- package/dist/core/utils/timeSource.js +4 -0
- package/dist/core/utils/timeSource.js.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +13 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +26 -2
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +2 -0
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +11 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +40 -9
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/peer/ClusterManager.d.ts +4 -0
- package/dist/node/server/peer/ClusterManager.d.ts.map +1 -1
- package/dist/node/server/peer/ClusterManager.js +2 -0
- package/dist/node/server/peer/ClusterManager.js.map +1 -1
- package/dist/node/server/peer/PeerReplicator.d.ts +12 -0
- package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -1
- package/dist/node/server/peer/PeerReplicator.js +19 -1
- package/dist/node/server/peer/PeerReplicator.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BaseMindooTenantDirectory = void 0;
|
|
4
4
|
const types_1 = require("./types");
|
|
5
5
|
const timeSource_1 = require("./utils/timeSource");
|
|
6
|
+
const singleFlight_1 = require("./utils/singleFlight");
|
|
6
7
|
const logging_1 = require("./logging");
|
|
7
8
|
const grantKeys_1 = require("./accesscontrol/grantKeys");
|
|
8
9
|
const types_2 = require("./accesscontrol/types");
|
|
@@ -14,10 +15,7 @@ const evaluate_1 = require("./accesscontrol/evaluate");
|
|
|
14
15
|
const RSAEncryption_1 = require("./crypto/RSAEncryption");
|
|
15
16
|
const encryptedFields_1 = require("./crypto/encryptedFields");
|
|
16
17
|
const DIRECTORY_SYNC_INTERVAL_MS = 5 * 60 * 1000;
|
|
17
|
-
|
|
18
|
-
// is reused before re-reading the directory head. Short, because it is consulted
|
|
19
|
-
// on the entry-verification hot path; the cutoff itself changes very rarely.
|
|
20
|
-
const METADATA_SIGNATURE_CUTOFF_TTL_MS = 30 * 1000;
|
|
18
|
+
const MAX_UNKNOWN_KEYS_TRACKED = 1000;
|
|
21
19
|
/** De-duplicated union of two string lists, preserving first-seen order. */
|
|
22
20
|
function unionStrings(existing, added) {
|
|
23
21
|
return Array.from(new Set([...existing, ...added]));
|
|
@@ -49,6 +47,13 @@ class BaseMindooTenantDirectory {
|
|
|
49
47
|
// Cache for groups: key -> merged group data (key: lowercase groupName)
|
|
50
48
|
// We store merged data separately to avoid mutating MindooDoc objects.
|
|
51
49
|
this.groupsCache = new Map();
|
|
50
|
+
// Every group document seen, by doc id. Several documents can carry the same
|
|
51
|
+
// group name (offline creation on two devices); groupsCache is their merge,
|
|
52
|
+
// recomputed per name from this map so an incremental pass that sees only
|
|
53
|
+
// one of them does not drop the others' members.
|
|
54
|
+
this.groupDocsById = new Map();
|
|
55
|
+
// Monotonic counter giving group documents their "latest seen" order.
|
|
56
|
+
this.groupDocSeenCounter = 0;
|
|
52
57
|
// Cache for key-distribution documents (acl_keydistribution_<keyId>), keyed by
|
|
53
58
|
// keyId. Folded incrementally in updateUnifiedCache (singleton per keyId,
|
|
54
59
|
// last-seen-wins; a deleted doc removes its entry). Powers the revoked-key
|
|
@@ -81,11 +86,33 @@ class BaseMindooTenantDirectory {
|
|
|
81
86
|
this.lastTimeTravelChangeSeq = null;
|
|
82
87
|
// Unified cache cursor for all document types
|
|
83
88
|
this.unifiedCacheLastCursor = null;
|
|
89
|
+
// Directory index `changeSeq` the last completed updateUnifiedCache pass
|
|
90
|
+
// covered. Compared against the live index instead of the cursor, because
|
|
91
|
+
// the cursor can lag behind entries that iterateChangesSince skips.
|
|
92
|
+
this.unifiedCacheIndexChangeSeq = null;
|
|
93
|
+
this.unifiedCachePass = new singleFlight_1.SingleFlight(() => this.runUnifiedCachePass());
|
|
94
|
+
this.timeTravelPass = new singleFlight_1.SingleFlight(() => this.runTimeTravelPass());
|
|
95
|
+
// Set when the feed changed stored revisions; cleared only once the chain
|
|
96
|
+
// was rebuilt from them, so a pass that fails in between is redone.
|
|
97
|
+
this.timeTravelNeedsRebuild = false;
|
|
98
|
+
// Set by invalidateUnifiedCache; the next pass starts from scratch. A flag
|
|
99
|
+
// rather than resetting the cursor directly, which a running pass would
|
|
100
|
+
// overwrite with its own progress.
|
|
101
|
+
this.unifiedCacheResetRequested = false;
|
|
102
|
+
// Last store poll (syncStoreChanges) for trust validation. Polling only
|
|
103
|
+
// discovers entries this process has not seen yet; entries already in the
|
|
104
|
+
// directory index are applied via the changeSeq check, independent of time.
|
|
84
105
|
this.lastDirectorySyncTimestamp = 0;
|
|
85
|
-
//
|
|
86
|
-
//
|
|
106
|
+
// Unknown signing keys -> time their cache miss last polled the store. Keeps
|
|
107
|
+
// repeated lookups of a key that is not granted from polling the store (and,
|
|
108
|
+
// for remote stores, the server) on every call.
|
|
109
|
+
this.unknownKeyPolledAt = new Map();
|
|
110
|
+
// Cached storage-format floor (`requireMetadataSignatureSince`), keyed by the
|
|
111
|
+
// directory changefeed `changeSeq` it was resolved at, with re-entrancy guard;
|
|
112
|
+
// see getRequireMetadataSignatureSince.
|
|
87
113
|
this.metadataSignatureCutoffCache = undefined;
|
|
88
|
-
this.
|
|
114
|
+
this.metadataSignatureCutoffResolved = false;
|
|
115
|
+
this.metadataSignatureCutoffChangeSeq = null;
|
|
89
116
|
this.resolvingMetadataSignatureCutoff = false;
|
|
90
117
|
this.tenant = tenant;
|
|
91
118
|
// Create logger if not provided (for backward compatibility)
|
|
@@ -110,7 +137,7 @@ class BaseMindooTenantDirectory {
|
|
|
110
137
|
// would otherwise go un-reconciled until the next network sync. Runs only
|
|
111
138
|
// once per directory object (the creation branch); the driver's own
|
|
112
139
|
// getDirectoryDB calls reuse the now-set instance and do not re-trigger.
|
|
113
|
-
|
|
140
|
+
this.tenant.trackBackgroundTask(this.tenant.reconcileKeyDistributionsForCurrentUserSafe());
|
|
114
141
|
}
|
|
115
142
|
return this.directoryDB;
|
|
116
143
|
}
|
|
@@ -436,31 +463,35 @@ class BaseMindooTenantDirectory {
|
|
|
436
463
|
return additionalResult;
|
|
437
464
|
}
|
|
438
465
|
}
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
// pushing entries.
|
|
466
|
+
// Store poll: discovers entries this process has not seen yet, so it is
|
|
467
|
+
// throttled to once per interval — unless the caller forces it (audit #4):
|
|
468
|
+
// the server push path must observe a freshly pushed revocation
|
|
469
|
+
// immediately, which would otherwise let a just-revoked key keep pushing.
|
|
444
470
|
const now = Date.now();
|
|
445
|
-
let
|
|
471
|
+
let didPoll = false;
|
|
446
472
|
if (opts?.forceRefresh ||
|
|
447
473
|
now - this.lastDirectorySyncTimestamp >= DIRECTORY_SYNC_INTERVAL_MS) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
await this.updateUnifiedCache();
|
|
451
|
-
this.lastDirectorySyncTimestamp = now;
|
|
452
|
-
didSync = true;
|
|
474
|
+
await this.pollDirectoryStore();
|
|
475
|
+
didPoll = true;
|
|
453
476
|
}
|
|
477
|
+
// Apply whatever the directory index already holds (local writes, pulls,
|
|
478
|
+
// the poll above). Cheap and time-independent: a grant or revocation this
|
|
479
|
+
// process knows about takes effect on the next validation.
|
|
480
|
+
await this.applyDirectoryIndexChanges();
|
|
454
481
|
let cachedResult = this.trustedKeysCache.get(publicKey);
|
|
455
|
-
if (cachedResult === undefined && !
|
|
456
|
-
// Cache miss
|
|
457
|
-
|
|
458
|
-
await
|
|
459
|
-
await this.
|
|
460
|
-
this.lastDirectorySyncTimestamp = Date.now();
|
|
482
|
+
if (cachedResult === undefined && !didPoll && this.mayPollForUnknownKey(publicKey, now)) {
|
|
483
|
+
// Cache miss: the grant may sit in the store without this process
|
|
484
|
+
// having seen it yet, so poll once and re-check.
|
|
485
|
+
await this.pollDirectoryStore();
|
|
486
|
+
await this.applyDirectoryIndexChanges();
|
|
461
487
|
cachedResult = this.trustedKeysCache.get(publicKey);
|
|
488
|
+
didPoll = true;
|
|
489
|
+
}
|
|
490
|
+
if (cachedResult === undefined && didPoll) {
|
|
491
|
+
this.recordUnknownKeyPoll(publicKey, now);
|
|
462
492
|
}
|
|
463
493
|
if (cachedResult !== undefined) {
|
|
494
|
+
this.unknownKeyPolledAt.delete(publicKey);
|
|
464
495
|
this.logger.debug(`Public key validation result (from cache): ${cachedResult}`);
|
|
465
496
|
return cachedResult;
|
|
466
497
|
}
|
|
@@ -469,26 +500,84 @@ class BaseMindooTenantDirectory {
|
|
|
469
500
|
return false;
|
|
470
501
|
}
|
|
471
502
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* newly arrived grantaccess documents are diffed immediately and the
|
|
476
|
-
* author-trust reconcile of open databases fires without waiting up to
|
|
477
|
-
* DIRECTORY_SYNC_INTERVAL_MS for the next cache-miss validation.
|
|
503
|
+
* Whether a cache miss for `publicKey` may poll the store. A key that missed
|
|
504
|
+
* within the last DIRECTORY_SYNC_INTERVAL_MS does not poll again; it still
|
|
505
|
+
* becomes trusted as soon as its grant reaches the directory index.
|
|
478
506
|
*/
|
|
479
|
-
|
|
507
|
+
mayPollForUnknownKey(publicKey, now) {
|
|
508
|
+
const polledAt = this.unknownKeyPolledAt.get(publicKey);
|
|
509
|
+
return polledAt === undefined || now - polledAt >= DIRECTORY_SYNC_INTERVAL_MS;
|
|
510
|
+
}
|
|
511
|
+
/** Remember that the store was polled and `publicKey` was still unknown. */
|
|
512
|
+
recordUnknownKeyPoll(publicKey, now) {
|
|
513
|
+
this.unknownKeyPolledAt.delete(publicKey);
|
|
514
|
+
if (this.unknownKeyPolledAt.size >= MAX_UNKNOWN_KEYS_TRACKED) {
|
|
515
|
+
const oldest = this.unknownKeyPolledAt.keys().next().value;
|
|
516
|
+
if (oldest !== undefined)
|
|
517
|
+
this.unknownKeyPolledAt.delete(oldest);
|
|
518
|
+
}
|
|
519
|
+
this.unknownKeyPolledAt.set(publicKey, now);
|
|
520
|
+
}
|
|
521
|
+
/** Read new directory entries from the store into the directory index. */
|
|
522
|
+
async pollDirectoryStore() {
|
|
480
523
|
const directoryDB = await this.getDirectoryDB();
|
|
481
524
|
await directoryDB.syncStoreChanges();
|
|
482
|
-
await this.updateUnifiedCache();
|
|
483
525
|
this.lastDirectorySyncTimestamp = Date.now();
|
|
484
526
|
}
|
|
485
527
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
|
|
528
|
+
* Bring the unified cache up to the directory index when the index has
|
|
529
|
+
* advanced since the last pass. No store access.
|
|
530
|
+
*/
|
|
531
|
+
async applyDirectoryIndexChanges() {
|
|
532
|
+
const directoryDB = await this.getDirectoryDB();
|
|
533
|
+
const latestSeq = directoryDB.getLatestChangeCursor?.()?.changeSeq ?? null;
|
|
534
|
+
if (this.unifiedCacheLastCursor === null ||
|
|
535
|
+
this.unifiedCacheResetRequested ||
|
|
536
|
+
latestSeq !== this.unifiedCacheIndexChangeSeq) {
|
|
537
|
+
await this.updateUnifiedCache();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Refresh the directory trust caches from the directory index right after a
|
|
542
|
+
* directory pull (which already read the pulled entries into the index), so
|
|
543
|
+
* newly arrived grantaccess documents are diffed immediately and the
|
|
544
|
+
* author-trust reconcile of open databases fires.
|
|
545
|
+
*/
|
|
546
|
+
async refreshTrustCaches() {
|
|
547
|
+
await this.applyDirectoryIndexChanges();
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Bring all caches (trusted keys, groups, settings) up to date with the
|
|
551
|
+
* directory index.
|
|
552
|
+
*
|
|
553
|
+
* Single-flight: passes mutate shared caches across awaits, so overlapping
|
|
554
|
+
* passes would read each other's half-built state and multiply the work
|
|
555
|
+
* under load. A caller arriving while a pass runs waits for one follow-up
|
|
556
|
+
* pass that starts after it arrived (so it observes its own preceding
|
|
557
|
+
* writes); all such callers share that follow-up.
|
|
558
|
+
*/
|
|
559
|
+
updateUnifiedCache() {
|
|
560
|
+
return this.unifiedCachePass.run();
|
|
561
|
+
}
|
|
562
|
+
/** Make the next unified-cache pass rebuild every cache from scratch. */
|
|
563
|
+
invalidateUnifiedCache() {
|
|
564
|
+
this.unifiedCacheResetRequested = true;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* One pass over the directory changes since the last cursor. Processes all
|
|
568
|
+
* document types in a single loop. No signature verification is needed
|
|
569
|
+
* since the DB already enforces admin-only access. Only called through
|
|
570
|
+
* {@link updateUnifiedCache}.
|
|
489
571
|
*/
|
|
490
|
-
async
|
|
572
|
+
async runUnifiedCachePass() {
|
|
491
573
|
const directoryDB = await this.getDirectoryDB();
|
|
574
|
+
// Captured before iterating: entries added during the pass carry a higher
|
|
575
|
+
// changeSeq, so the next applyDirectoryIndexChanges picks them up.
|
|
576
|
+
const indexChangeSeqAtStart = directoryDB.getLatestChangeCursor?.()?.changeSeq ?? null;
|
|
577
|
+
if (this.unifiedCacheResetRequested) {
|
|
578
|
+
this.unifiedCacheResetRequested = false;
|
|
579
|
+
this.unifiedCacheLastCursor = null;
|
|
580
|
+
}
|
|
492
581
|
// Determine starting cursor (null = process all, otherwise incremental)
|
|
493
582
|
const startCursor = this.unifiedCacheLastCursor;
|
|
494
583
|
// Snapshot the trusted signing keys BEFORE any rebuild so newly granted
|
|
@@ -496,19 +585,33 @@ class BaseMindooTenantDirectory {
|
|
|
496
585
|
// author-trust reconcile of open databases (entries previously skipped
|
|
497
586
|
// because their author was unknown get re-materialized).
|
|
498
587
|
const previouslyTrustedKeys = new Set(this.trustedKeysCache.keys());
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const
|
|
588
|
+
// Build and swap. The pass awaits between documents, and readers (also
|
|
589
|
+
// ones iterating a cache across their own awaits) must never see a cache
|
|
590
|
+
// that a full rebuild has cleared but not refilled. A full rebuild fills
|
|
591
|
+
// fresh maps and installs them at the end, synchronously; an incremental
|
|
592
|
+
// pass only adds/overwrites entries in place. The cursor is committed at
|
|
593
|
+
// the end too, so a pass that throws midway is simply redone (all updates
|
|
594
|
+
// are idempotent) instead of leaving its documents half-applied.
|
|
595
|
+
const fullRebuild = startCursor === null;
|
|
596
|
+
const grantDocIdToSigningKeys = fullRebuild ? new Map() : this.grantDocIdToSigningKeys;
|
|
597
|
+
const userLookupCache = fullRebuild ? new Map() : this.userLookupCache;
|
|
598
|
+
const wipeKeyToGrantDocId = fullRebuild ? new Map() : this.wipeKeyToGrantDocId;
|
|
599
|
+
let tenantSettingsCache = fullRebuild ? null : this.tenantSettingsCache;
|
|
600
|
+
const dbSettingsCache = fullRebuild ? new Map() : this.dbSettingsCache;
|
|
601
|
+
const groupDocsById = fullRebuild ? new Map() : this.groupDocsById;
|
|
602
|
+
const groupsCache = fullRebuild ? new Map() : this.groupsCache;
|
|
603
|
+
const keyDistributionCache = fullRebuild
|
|
604
|
+
? new Map()
|
|
605
|
+
: this.keyDistributionCache;
|
|
606
|
+
const appDistributionCache = fullRebuild
|
|
607
|
+
? new Map()
|
|
608
|
+
: this.appDistributionCache;
|
|
609
|
+
const syncSetupPolicyCache = fullRebuild
|
|
610
|
+
? new Map()
|
|
611
|
+
: this.syncSetupPolicyCache;
|
|
612
|
+
let lastCursor = startCursor;
|
|
613
|
+
// Group names whose merged entry must be recomputed at the end of the pass.
|
|
614
|
+
const dirtyGroupNames = new Set();
|
|
512
615
|
let sawGrantAccessChange = false;
|
|
513
616
|
// Process changes (documents are returned in order by lastModified, oldest to newest)
|
|
514
617
|
for await (const { doc, cursor } of directoryDB.iterateChangesSince(startCursor)) {
|
|
@@ -525,7 +628,7 @@ class BaseMindooTenantDirectory {
|
|
|
525
628
|
// to wipe. Maps each wipe-targeted signing key to this grant doc's id.
|
|
526
629
|
if (data.type === "grantaccess") {
|
|
527
630
|
for (const wipeKey of (0, grantKeys_1.extractWipeRequestedSigningKeys)(data)) {
|
|
528
|
-
|
|
631
|
+
wipeKeyToGrantDocId.set(wipeKey, doc.getId());
|
|
529
632
|
}
|
|
530
633
|
}
|
|
531
634
|
if (data.type === "grantaccess") {
|
|
@@ -537,7 +640,7 @@ class BaseMindooTenantDirectory {
|
|
|
537
640
|
// all grant documents once the change loop completes, so a key
|
|
538
641
|
// dropped from this document is no longer trusted unless another
|
|
539
642
|
// grant still lists it.
|
|
540
|
-
|
|
643
|
+
grantDocIdToSigningKeys.set(doc.getId(), grantedSigningKeys);
|
|
541
644
|
if (grantedSigningKeys.length > 0) {
|
|
542
645
|
// One cache entry per device key pair (§6.5). Reusing a single
|
|
543
646
|
// lookup built from keys[0] for every signing key would make the
|
|
@@ -546,32 +649,45 @@ class BaseMindooTenantDirectory {
|
|
|
546
649
|
// then fails with a WebCrypto OperationError on the iPad.
|
|
547
650
|
const lookups = await this.buildUserLookups(data);
|
|
548
651
|
for (const lookup of lookups) {
|
|
549
|
-
|
|
652
|
+
userLookupCache.set(lookup.signingPublicKey, lookup);
|
|
550
653
|
}
|
|
551
654
|
}
|
|
552
655
|
}
|
|
553
656
|
}
|
|
554
|
-
// Process group documents
|
|
657
|
+
// Process group documents. The previous name of a re-seen doc is marked
|
|
658
|
+
// dirty too, so a renamed or no-longer-group doc leaves its old merge.
|
|
659
|
+
const previousGroupEntry = groupDocsById.get(doc.getId());
|
|
660
|
+
if (previousGroupEntry) {
|
|
661
|
+
dirtyGroupNames.add(previousGroupEntry.normalizedName);
|
|
662
|
+
}
|
|
555
663
|
if (data.form === "group" &&
|
|
556
664
|
data.type === "group" &&
|
|
557
665
|
data.groupName &&
|
|
558
666
|
typeof data.groupName === "string") {
|
|
559
667
|
const normalizedGroupName = this.normalizeGroupName(data.groupName);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
668
|
+
const stringArray = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
669
|
+
groupDocsById.set(doc.getId(), {
|
|
670
|
+
normalizedName: normalizedGroupName,
|
|
671
|
+
membersHashes: stringArray(data.members_hashes),
|
|
672
|
+
membersEncrypted: stringArray(data.members_encrypted),
|
|
673
|
+
deleted: doc.isDeleted(),
|
|
674
|
+
seenOrder: ++this.groupDocSeenCounter,
|
|
675
|
+
});
|
|
676
|
+
dirtyGroupNames.add(normalizedGroupName);
|
|
677
|
+
}
|
|
678
|
+
else if (previousGroupEntry) {
|
|
679
|
+
groupDocsById.delete(doc.getId());
|
|
564
680
|
}
|
|
565
681
|
// Process tenant settings (no signature verification needed - Automerge handles merging)
|
|
566
682
|
if (data.form === "tenantsettings") {
|
|
567
683
|
// Just overwrite - last one seen will be the latest
|
|
568
|
-
|
|
684
|
+
tenantSettingsCache = doc;
|
|
569
685
|
}
|
|
570
686
|
// Process DB settings (no signature verification needed - Automerge handles merging)
|
|
571
687
|
if (data.form === "dbsettings" && data.dbid && typeof data.dbid === "string") {
|
|
572
688
|
const dbId = data.dbid;
|
|
573
689
|
// Just overwrite - last one seen will be the latest
|
|
574
|
-
|
|
690
|
+
dbSettingsCache.set(dbId, doc);
|
|
575
691
|
}
|
|
576
692
|
// Process key-distribution documents (acl_keydistribution_<keyId>); a
|
|
577
693
|
// singleton per keyId. Cache head state here so the revoked-key resolver
|
|
@@ -580,10 +696,10 @@ class BaseMindooTenantDirectory {
|
|
|
580
696
|
if (data.type === types_3.KEY_DISTRIBUTION_TYPE && typeof data.keyId === "string") {
|
|
581
697
|
const keyId = data.keyId;
|
|
582
698
|
if (doc.isDeleted()) {
|
|
583
|
-
|
|
699
|
+
keyDistributionCache.delete(keyId);
|
|
584
700
|
}
|
|
585
701
|
else {
|
|
586
|
-
|
|
702
|
+
keyDistributionCache.set(keyId, {
|
|
587
703
|
keyVersions: Array.isArray(data.keyVersions)
|
|
588
704
|
? data.keyVersions
|
|
589
705
|
.filter((v) => !!v && typeof v.fingerprint === "string")
|
|
@@ -608,11 +724,11 @@ class BaseMindooTenantDirectory {
|
|
|
608
724
|
if (data.type === types_3.APP_DISTRIBUTION_TYPE && typeof data.appId === "string") {
|
|
609
725
|
const appId = data.appId;
|
|
610
726
|
if (doc.isDeleted()) {
|
|
611
|
-
|
|
727
|
+
appDistributionCache.delete(appId);
|
|
612
728
|
}
|
|
613
729
|
else {
|
|
614
730
|
const stringArray = (value) => Array.isArray(value) ? value.filter((h) => typeof h === "string") : [];
|
|
615
|
-
|
|
731
|
+
appDistributionCache.set(appId, {
|
|
616
732
|
pushto_users_hashes: stringArray(data.pushto_users_hashes),
|
|
617
733
|
pushto_groups_hashes: stringArray(data.pushto_groups_hashes),
|
|
618
734
|
pullfrom_users_hashes: stringArray(data.pullfrom_users_hashes),
|
|
@@ -627,11 +743,11 @@ class BaseMindooTenantDirectory {
|
|
|
627
743
|
if (data.type === types_3.SYNC_SETUP_POLICY_TYPE && typeof data.policyId === "string") {
|
|
628
744
|
const policyId = data.policyId;
|
|
629
745
|
if (doc.isDeleted()) {
|
|
630
|
-
|
|
746
|
+
syncSetupPolicyCache.delete(policyId);
|
|
631
747
|
}
|
|
632
748
|
else {
|
|
633
749
|
const stringArray = (value) => Array.isArray(value) ? value.filter((h) => typeof h === "string") : [];
|
|
634
|
-
|
|
750
|
+
syncSetupPolicyCache.set(policyId, {
|
|
635
751
|
mode: data.mode === "permanent" ? "permanent" : "initial",
|
|
636
752
|
pushto_users_hashes: stringArray(data.pushto_users_hashes),
|
|
637
753
|
pushto_groups_hashes: stringArray(data.pushto_groups_hashes),
|
|
@@ -640,21 +756,74 @@ class BaseMindooTenantDirectory {
|
|
|
640
756
|
});
|
|
641
757
|
}
|
|
642
758
|
}
|
|
643
|
-
|
|
644
|
-
this.unifiedCacheLastCursor = cursor;
|
|
759
|
+
lastCursor = cursor;
|
|
645
760
|
}
|
|
761
|
+
// From here to the end of the method there is no await: readers see
|
|
762
|
+
// either the previous state or this pass's complete state.
|
|
646
763
|
// Rebuild the trusted signing-key set as the union of every grant
|
|
647
764
|
// document's current keys (docs/accesscontrol.md §6.5). Revocation removes
|
|
648
765
|
// keys from a grant, so a key absent from this union is no longer trusted;
|
|
649
766
|
// validatePublicSigningKey treats "not present" as untrusted. This is
|
|
650
767
|
// recomputed on every pass (full or incremental) because the per-grant map
|
|
651
768
|
// is the source of truth and persists across incremental updates.
|
|
652
|
-
|
|
653
|
-
for (const signingKeys of
|
|
769
|
+
const trustedKeysCache = new Map();
|
|
770
|
+
for (const signingKeys of grantDocIdToSigningKeys.values()) {
|
|
654
771
|
for (const key of signingKeys) {
|
|
655
|
-
|
|
772
|
+
trustedKeysCache.set(key, true);
|
|
656
773
|
}
|
|
657
774
|
}
|
|
775
|
+
// Merge the group documents of every touched name (handles the same group
|
|
776
|
+
// created offline on several devices). A name is deleted as soon as any of
|
|
777
|
+
// its documents is deleted; docId points at the latest-seen document.
|
|
778
|
+
if (dirtyGroupNames.size > 0) {
|
|
779
|
+
const entriesByName = new Map();
|
|
780
|
+
for (const [docId, entry] of groupDocsById) {
|
|
781
|
+
if (!dirtyGroupNames.has(entry.normalizedName))
|
|
782
|
+
continue;
|
|
783
|
+
let list = entriesByName.get(entry.normalizedName);
|
|
784
|
+
if (!list) {
|
|
785
|
+
list = [];
|
|
786
|
+
entriesByName.set(entry.normalizedName, list);
|
|
787
|
+
}
|
|
788
|
+
list.push([docId, entry]);
|
|
789
|
+
}
|
|
790
|
+
for (const normalizedGroupName of dirtyGroupNames) {
|
|
791
|
+
const entries = entriesByName.get(normalizedGroupName);
|
|
792
|
+
if (!entries || entries.some(([, entry]) => entry.deleted)) {
|
|
793
|
+
groupsCache.delete(normalizedGroupName);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
let latest = entries[0];
|
|
797
|
+
const allMembersHashes = new Set();
|
|
798
|
+
const allMembersEncrypted = new Set();
|
|
799
|
+
for (const item of entries) {
|
|
800
|
+
if (item[1].seenOrder > latest[1].seenOrder)
|
|
801
|
+
latest = item;
|
|
802
|
+
for (const h of item[1].membersHashes)
|
|
803
|
+
allMembersHashes.add(h);
|
|
804
|
+
for (const e of item[1].membersEncrypted)
|
|
805
|
+
allMembersEncrypted.add(e);
|
|
806
|
+
}
|
|
807
|
+
groupsCache.set(normalizedGroupName, {
|
|
808
|
+
docId: latest[0],
|
|
809
|
+
members_hashes: Array.from(allMembersHashes),
|
|
810
|
+
members_encrypted: Array.from(allMembersEncrypted),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
this.trustedKeysCache = trustedKeysCache;
|
|
815
|
+
this.grantDocIdToSigningKeys = grantDocIdToSigningKeys;
|
|
816
|
+
this.userLookupCache = userLookupCache;
|
|
817
|
+
this.wipeKeyToGrantDocId = wipeKeyToGrantDocId;
|
|
818
|
+
this.tenantSettingsCache = tenantSettingsCache;
|
|
819
|
+
this.dbSettingsCache = dbSettingsCache;
|
|
820
|
+
this.groupDocsById = groupDocsById;
|
|
821
|
+
this.groupsCache = groupsCache;
|
|
822
|
+
this.keyDistributionCache = keyDistributionCache;
|
|
823
|
+
this.appDistributionCache = appDistributionCache;
|
|
824
|
+
this.syncSetupPolicyCache = syncSetupPolicyCache;
|
|
825
|
+
this.unifiedCacheLastCursor = lastCursor;
|
|
826
|
+
this.unifiedCacheIndexChangeSeq = indexChangeSeqAtStart;
|
|
658
827
|
// Author-trust hook: keys that just became trusted (new grantaccess docs)
|
|
659
828
|
// may unblock entries that open databases skipped earlier because the
|
|
660
829
|
// author was unknown. Notify the tenant so those documents get purged and
|
|
@@ -668,56 +837,11 @@ class BaseMindooTenantDirectory {
|
|
|
668
837
|
}
|
|
669
838
|
}
|
|
670
839
|
if (newlyTrustedKeys.size > 0) {
|
|
671
|
-
|
|
840
|
+
this.tenant.trackBackgroundTask(this.tenant.reconcileAuthorTrustChangesSafe(newlyTrustedKeys));
|
|
672
841
|
}
|
|
673
842
|
if (sawGrantAccessChange) {
|
|
674
843
|
this.tenant.scheduleUserKeyReconcile?.();
|
|
675
844
|
}
|
|
676
|
-
// Merge group documents with the same name
|
|
677
|
-
for (const [normalizedGroupName, docs] of groupDocsByName.entries()) {
|
|
678
|
-
// Collect all member hashes and encrypted values from all documents with this group name
|
|
679
|
-
const allMembersHashes = new Set();
|
|
680
|
-
const allMembersEncrypted = new Set();
|
|
681
|
-
let latestDoc = null;
|
|
682
|
-
let isDeleted = false;
|
|
683
|
-
for (const doc of docs) {
|
|
684
|
-
const data = doc.getData();
|
|
685
|
-
// Track the latest document (by iteration order, which is by lastModified)
|
|
686
|
-
latestDoc = doc;
|
|
687
|
-
// Check if document is deleted via lifecycle metadata.
|
|
688
|
-
if (doc.isDeleted()) {
|
|
689
|
-
isDeleted = true;
|
|
690
|
-
}
|
|
691
|
-
// Use members_hashes for membership lookups.
|
|
692
|
-
if (data.members_hashes && Array.isArray(data.members_hashes)) {
|
|
693
|
-
for (const memberHash of data.members_hashes) {
|
|
694
|
-
if (typeof memberHash === "string") {
|
|
695
|
-
allMembersHashes.add(memberHash);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
if (data.members_encrypted && Array.isArray(data.members_encrypted)) {
|
|
700
|
-
for (const encryptedMember of data.members_encrypted) {
|
|
701
|
-
if (typeof encryptedMember === "string") {
|
|
702
|
-
allMembersEncrypted.add(encryptedMember);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
// If latest version is deleted, remove from cache
|
|
708
|
-
if (isDeleted) {
|
|
709
|
-
this.groupsCache.delete(normalizedGroupName);
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
// Store merged group data in cache (without mutating the MindooDoc)
|
|
713
|
-
if (latestDoc) {
|
|
714
|
-
this.groupsCache.set(normalizedGroupName, {
|
|
715
|
-
docId: latestDoc.getId(),
|
|
716
|
-
members_hashes: Array.from(allMembersHashes),
|
|
717
|
-
members_encrypted: Array.from(allMembersEncrypted),
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
845
|
}
|
|
722
846
|
/**
|
|
723
847
|
* The head time-travel directory-state node ("now"), after ensuring the chain
|
|
@@ -745,8 +869,15 @@ class BaseMindooTenantDirectory {
|
|
|
745
869
|
* The (potentially expensive) feed rebuild is skipped when the directory's
|
|
746
870
|
* changefeed cursor has not advanced since the previous build and no
|
|
747
871
|
* un-witnessed revisions are pending a possible trusted-time re-stamp.
|
|
872
|
+
*
|
|
873
|
+
* Single-flight: the pass creates the index across an await and advances
|
|
874
|
+
* the shared index cursor per revision, so overlapping passes would create
|
|
875
|
+
* and register two indexes or interleave their feed reads.
|
|
748
876
|
*/
|
|
749
|
-
|
|
877
|
+
ensureTimeTravelCurrent() {
|
|
878
|
+
return this.timeTravelPass.run();
|
|
879
|
+
}
|
|
880
|
+
async runTimeTravelPass() {
|
|
750
881
|
const directoryDB = await this.getDirectoryDB();
|
|
751
882
|
// Projection used to (re)build the chain from stored revisions in
|
|
752
883
|
// trusted-time order. Captures this directory's group-name normalization.
|
|
@@ -789,13 +920,14 @@ class BaseMindooTenantDirectory {
|
|
|
789
920
|
// (notably when a re-stamped entry is re-discovered with a `receivedAt`), so
|
|
790
921
|
// we do not need to re-run the feed on every call just to refresh the
|
|
791
922
|
// provisional `now` of un-witnessed entries.
|
|
792
|
-
if (this.
|
|
923
|
+
if (!this.timeTravelNeedsRebuild &&
|
|
924
|
+
this.lastTimeTravelChangeSeq !== null &&
|
|
925
|
+
latestSeq === this.lastTimeTravelChangeSeq) {
|
|
793
926
|
return index;
|
|
794
927
|
}
|
|
795
928
|
// Incrementally advance the feed from the persisted cursor; the feed parks
|
|
796
929
|
// the cursor before the earliest un-witnessed entry so those revisions are
|
|
797
930
|
// re-discovered (and re-stamped/superseded as needed) on this resume.
|
|
798
|
-
let changed = false;
|
|
799
931
|
for await (const rev of directoryDB.iterateChangeRevisionsSince(index.cursor)) {
|
|
800
932
|
const revChanged = index.upsertRevision({
|
|
801
933
|
entryId: rev.entryId,
|
|
@@ -805,13 +937,16 @@ class BaseMindooTenantDirectory {
|
|
|
805
937
|
trustedTime: rev.trustedTime,
|
|
806
938
|
witnessed: rev.witnessed,
|
|
807
939
|
}, rev.cursor);
|
|
808
|
-
|
|
940
|
+
if (revChanged) {
|
|
941
|
+
this.timeTravelNeedsRebuild = true;
|
|
942
|
+
}
|
|
809
943
|
}
|
|
810
944
|
// Replay all revisions into the chain in trusted-time order. This absorbs
|
|
811
945
|
// out-of-order arrivals and supersessions (a re-emitted revision replaced its
|
|
812
946
|
// prior record above) without any special-casing in the chain builder.
|
|
813
|
-
if (
|
|
947
|
+
if (this.timeTravelNeedsRebuild) {
|
|
814
948
|
index.rebuild(project);
|
|
949
|
+
this.timeTravelNeedsRebuild = false;
|
|
815
950
|
}
|
|
816
951
|
this.lastTimeTravelChangeSeq = latestSeq;
|
|
817
952
|
// Persist the observed changeSeq with the chain so the gate survives restart.
|
|
@@ -1002,28 +1137,34 @@ class BaseMindooTenantDirectory {
|
|
|
1002
1137
|
* signature, or `undefined` when no floor is configured (fully backward
|
|
1003
1138
|
* compatible). Tenant-level only, read from the `acl_defaultpolicy` head.
|
|
1004
1139
|
*
|
|
1005
|
-
* Cached
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1140
|
+
* Cached until the directory changefeed `changeSeq` advances, so a newly set
|
|
1141
|
+
* floor applies to the very next verification, and guarded against
|
|
1142
|
+
* re-entrancy: resolving the cutoff loads the directory head, and directory
|
|
1143
|
+
* materialization itself runs signature verification — without the guard a
|
|
1144
|
+
* verify→resolve→materialize→verify cycle could recurse. During such re-entry
|
|
1145
|
+
* we return the last known value (the directory store is anyway exempt from
|
|
1146
|
+
* the floor by its callers).
|
|
1010
1147
|
*/
|
|
1011
1148
|
async getRequireMetadataSignatureSince() {
|
|
1012
|
-
const now = Date.now();
|
|
1013
1149
|
if (this.resolvingMetadataSignatureCutoff) {
|
|
1014
1150
|
return this.metadataSignatureCutoffCache;
|
|
1015
1151
|
}
|
|
1016
|
-
if (this.
|
|
1017
|
-
|
|
1152
|
+
if (this.metadataSignatureCutoffResolved &&
|
|
1153
|
+
this.directoryDB &&
|
|
1154
|
+
(this.directoryDB.getLatestChangeCursor?.()?.changeSeq ?? null) ===
|
|
1155
|
+
this.metadataSignatureCutoffChangeSeq) {
|
|
1018
1156
|
return this.metadataSignatureCutoffCache;
|
|
1019
1157
|
}
|
|
1020
1158
|
this.resolvingMetadataSignatureCutoff = true;
|
|
1021
1159
|
try {
|
|
1160
|
+
const directoryDB = await this.getDirectoryDB();
|
|
1161
|
+
const changeSeq = directoryDB.getLatestChangeCursor?.()?.changeSeq ?? null;
|
|
1022
1162
|
const head = await this.getDirectoryStateHead();
|
|
1023
1163
|
const since = head.defaultPolicy?.requireMetadataSignatureSince;
|
|
1024
1164
|
this.metadataSignatureCutoffCache =
|
|
1025
1165
|
typeof since === "number" && Number.isFinite(since) ? since : undefined;
|
|
1026
|
-
this.
|
|
1166
|
+
this.metadataSignatureCutoffChangeSeq = changeSeq;
|
|
1167
|
+
this.metadataSignatureCutoffResolved = true;
|
|
1027
1168
|
}
|
|
1028
1169
|
finally {
|
|
1029
1170
|
this.resolvingMetadataSignatureCutoff = false;
|
|
@@ -3237,7 +3378,7 @@ class BaseMindooTenantDirectory {
|
|
|
3237
3378
|
});
|
|
3238
3379
|
// Invalidate cache
|
|
3239
3380
|
this.tenantSettingsCache = null;
|
|
3240
|
-
this.
|
|
3381
|
+
this.invalidateUnifiedCache();
|
|
3241
3382
|
}
|
|
3242
3383
|
async changeDBSettings(dbId, changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
3243
3384
|
const baseTenant = this.tenant;
|
|
@@ -3266,7 +3407,7 @@ class BaseMindooTenantDirectory {
|
|
|
3266
3407
|
});
|
|
3267
3408
|
// Invalidate cache for this dbId
|
|
3268
3409
|
this.dbSettingsCache.delete(dbId);
|
|
3269
|
-
this.
|
|
3410
|
+
this.invalidateUnifiedCache();
|
|
3270
3411
|
}
|
|
3271
3412
|
async getGroups() {
|
|
3272
3413
|
const directoryDB = await this.getDirectoryDB();
|
|
@@ -3308,6 +3449,11 @@ class BaseMindooTenantDirectory {
|
|
|
3308
3449
|
// new usernames. Reads match both v1 and v2 forms (docs/accesscontrol.md §6.5).
|
|
3309
3450
|
const newMembersHashes = await Promise.all(username.map(u => this.hashUsernameForWrite(u)));
|
|
3310
3451
|
const newMembersEncrypted = await Promise.all(username.map(u => this.encryptGroupMemberForTenant(u)));
|
|
3452
|
+
// Bring the cache up to date first: a stale miss (e.g. right after the
|
|
3453
|
+
// previous call invalidated it) would create a second document for an
|
|
3454
|
+
// existing group name.
|
|
3455
|
+
await directoryDB.syncStoreChanges();
|
|
3456
|
+
await this.updateUnifiedCache();
|
|
3311
3457
|
// Get the actual document from the database (using cached docId if available)
|
|
3312
3458
|
const cachedGroup = this.groupsCache.get(normalizedGroupName);
|
|
3313
3459
|
let groupDoc;
|
|
@@ -3353,7 +3499,7 @@ class BaseMindooTenantDirectory {
|
|
|
3353
3499
|
});
|
|
3354
3500
|
// Invalidate cache for this group
|
|
3355
3501
|
this.groupsCache.delete(normalizedGroupName);
|
|
3356
|
-
this.
|
|
3502
|
+
this.invalidateUnifiedCache();
|
|
3357
3503
|
}
|
|
3358
3504
|
async removeUsersFromGroup(groupName, username, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
3359
3505
|
const normalizedGroupName = this.normalizeGroupName(groupName);
|
|
@@ -3368,8 +3514,6 @@ class BaseMindooTenantDirectory {
|
|
|
3368
3514
|
// Group doesn't exist, nothing to do
|
|
3369
3515
|
return;
|
|
3370
3516
|
}
|
|
3371
|
-
// Load the actual document from the database
|
|
3372
|
-
const groupDoc = await directoryDB.getDocument(cachedGroup.docId);
|
|
3373
3517
|
const adminSigningKeyPair = {
|
|
3374
3518
|
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
3375
3519
|
privateKey: administrationPrivateKey,
|
|
@@ -3377,36 +3521,47 @@ class BaseMindooTenantDirectory {
|
|
|
3377
3521
|
// Compute hashes of usernames to remove. Include both legacy (v1) and
|
|
3378
3522
|
// salted (v2) forms so members stored under either scheme are removed.
|
|
3379
3523
|
const hashesToRemove = new Set((await Promise.all(username.map(u => this.usernameHashCandidates(u)))).flat());
|
|
3380
|
-
//
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
}
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
const
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3524
|
+
// Membership is the union over every document carrying this group name,
|
|
3525
|
+
// so the users must be removed from all of them, not only the latest.
|
|
3526
|
+
const groupDocIds = new Set([cachedGroup.docId]);
|
|
3527
|
+
for (const [docId, entry] of this.groupDocsById) {
|
|
3528
|
+
if (entry.normalizedName === normalizedGroupName && !entry.deleted) {
|
|
3529
|
+
groupDocIds.add(docId);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
for (const groupDocId of groupDocIds) {
|
|
3533
|
+
const groupDoc = await directoryDB.getDocument(groupDocId);
|
|
3534
|
+
// Remove users from members arrays
|
|
3535
|
+
await directoryDB.changeDoc(groupDoc, async (d) => {
|
|
3536
|
+
const data = d.getData();
|
|
3537
|
+
if (!data.members_hashes || !Array.isArray(data.members_hashes) ||
|
|
3538
|
+
!data.members_encrypted || !Array.isArray(data.members_encrypted)) {
|
|
3539
|
+
return;
|
|
3394
3540
|
}
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3541
|
+
// Filter out members whose hashes match the ones to remove
|
|
3542
|
+
// Keep arrays in sync by filtering indices
|
|
3543
|
+
const indicesToKeep = [];
|
|
3544
|
+
const hashesArray = data.members_hashes;
|
|
3545
|
+
for (let i = 0; i < hashesArray.length; i++) {
|
|
3546
|
+
if (!hashesToRemove.has(hashesArray[i])) {
|
|
3547
|
+
indicesToKeep.push(i);
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
const encryptedArray = data.members_encrypted;
|
|
3551
|
+
data.members_hashes = indicesToKeep.map(i => hashesArray[i]);
|
|
3552
|
+
data.members_encrypted = indicesToKeep.map(i => encryptedArray[i]);
|
|
3553
|
+
// Ensure form, type, and groupName fields are always correct
|
|
3554
|
+
data.form = "group";
|
|
3555
|
+
data.type = "group";
|
|
3556
|
+
data.groupName = normalizedGroupName;
|
|
3557
|
+
}, {
|
|
3558
|
+
signingKeyPair: adminSigningKeyPair,
|
|
3559
|
+
signingKeyPassword: administrationPrivateKeyPassword,
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3407
3562
|
// Invalidate cache for this group
|
|
3408
3563
|
this.groupsCache.delete(normalizedGroupName);
|
|
3409
|
-
this.
|
|
3564
|
+
this.invalidateUnifiedCache();
|
|
3410
3565
|
}
|
|
3411
3566
|
async deleteGroup(groupName, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
3412
3567
|
const normalizedGroupName = this.normalizeGroupName(groupName);
|
|
@@ -3432,7 +3587,7 @@ class BaseMindooTenantDirectory {
|
|
|
3432
3587
|
});
|
|
3433
3588
|
// Remove from cache
|
|
3434
3589
|
this.groupsCache.delete(normalizedGroupName);
|
|
3435
|
-
this.
|
|
3590
|
+
this.invalidateUnifiedCache();
|
|
3436
3591
|
}
|
|
3437
3592
|
async getUserNamesList(username) {
|
|
3438
3593
|
// Generate username variants (wildcards)
|