mindoodb 0.0.1
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/README.md +263 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts +5 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.d.ts.map +1 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +17 -0
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +111 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +237 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts +78 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -0
- package/dist/appendonlystores/network/HttpTransport.js +356 -0
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +131 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +248 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -0
- package/dist/appendonlystores/network/index.d.ts +8 -0
- package/dist/appendonlystores/network/index.d.ts.map +1 -0
- package/dist/appendonlystores/network/index.js +21 -0
- package/dist/appendonlystores/network/index.js.map +1 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.d.ts +14 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.d.ts.map +1 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.js +30 -0
- package/dist/browser/crypto/BrowserCryptoAdapter.js.map +1 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +26 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/core/BaseMindooDB.d.ts +182 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -0
- package/dist/core/BaseMindooDB.js +1701 -0
- package/dist/core/BaseMindooDB.js.map +1 -0
- package/dist/core/BaseMindooTenant.d.ts +155 -0
- package/dist/core/BaseMindooTenant.d.ts.map +1 -0
- package/dist/core/BaseMindooTenant.js +673 -0
- package/dist/core/BaseMindooTenant.js.map +1 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts +99 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -0
- package/dist/core/BaseMindooTenantDirectory.js +776 -0
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -0
- package/dist/core/BaseMindooTenantFactory.d.ts +91 -0
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -0
- package/dist/core/BaseMindooTenantFactory.js +297 -0
- package/dist/core/BaseMindooTenantFactory.js.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +111 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +293 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +78 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/AuthenticationService.js +297 -0
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -0
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +174 -0
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/NetworkTransport.js +3 -0
- package/dist/core/appendonlystores/network/NetworkTransport.js.map +1 -0
- package/dist/core/appendonlystores/network/types.d.ts +135 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/types.js +35 -0
- package/dist/core/appendonlystores/network/types.js.map +1 -0
- package/dist/core/appendonlystores/types.d.ts +157 -0
- package/dist/core/appendonlystores/types.d.ts.map +1 -0
- package/dist/core/appendonlystores/types.js +3 -0
- package/dist/core/appendonlystores/types.js.map +1 -0
- package/dist/core/crypto/CryptoAdapter.d.ts +19 -0
- package/dist/core/crypto/CryptoAdapter.d.ts.map +1 -0
- package/dist/core/crypto/CryptoAdapter.js +3 -0
- package/dist/core/crypto/CryptoAdapter.js.map +1 -0
- package/dist/core/crypto/MindooDocSigner.d.ts +106 -0
- package/dist/core/crypto/MindooDocSigner.d.ts.map +1 -0
- package/dist/core/crypto/MindooDocSigner.js +192 -0
- package/dist/core/crypto/MindooDocSigner.js.map +1 -0
- package/dist/core/crypto/RSAEncryption.d.ts +62 -0
- package/dist/core/crypto/RSAEncryption.d.ts.map +1 -0
- package/dist/core/crypto/RSAEncryption.js +167 -0
- package/dist/core/crypto/RSAEncryption.js.map +1 -0
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +53 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts +33 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js +27 -0
- package/dist/core/indexing/virtualviews/IViewEntryAccessCheck.js.map +1 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts +31 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js +3 -0
- package/dist/core/indexing/virtualviews/IVirtualViewDataProvider.js.map +1 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts +52 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +106 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts +44 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.js +107 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKey.js.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts +28 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js +133 -0
- package/dist/core/indexing/virtualviews/ViewEntrySortKeyComparator.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualView.d.ts +109 -0
- package/dist/core/indexing/virtualviews/VirtualView.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualView.js +504 -0
- package/dist/core/indexing/virtualviews/VirtualView.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts +59 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.js +70 -0
- package/dist/core/indexing/virtualviews/VirtualViewColumn.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts +49 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.js +69 -0
- package/dist/core/indexing/virtualviews/VirtualViewDataChange.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts +121 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js +365 -0
- package/dist/core/indexing/virtualviews/VirtualViewEntryData.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts +131 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js +236 -0
- package/dist/core/indexing/virtualviews/VirtualViewFactory.js.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts +330 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.js +1010 -0
- package/dist/core/indexing/virtualviews/VirtualViewNavigator.js.map +1 -0
- package/dist/core/indexing/virtualviews/index.d.ts +53 -0
- package/dist/core/indexing/virtualviews/index.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/index.js +89 -0
- package/dist/core/indexing/virtualviews/index.js.map +1 -0
- package/dist/core/indexing/virtualviews/types.d.ts +115 -0
- package/dist/core/indexing/virtualviews/types.d.ts.map +1 -0
- package/dist/core/indexing/virtualviews/types.js +175 -0
- package/dist/core/indexing/virtualviews/types.js.map +1 -0
- package/dist/core/keys/KeyBag.d.ts +122 -0
- package/dist/core/keys/KeyBag.d.ts.map +1 -0
- package/dist/core/keys/KeyBag.js +401 -0
- package/dist/core/keys/KeyBag.js.map +1 -0
- package/dist/core/types.d.ts +1004 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/userid/index.d.ts +53 -0
- package/dist/core/userid/index.d.ts.map +1 -0
- package/dist/core/userid/index.js +3 -0
- package/dist/core/userid/index.js.map +1 -0
- package/dist/core/utils/idGeneration.d.ts +109 -0
- package/dist/core/utils/idGeneration.d.ts.map +1 -0
- package/dist/core/utils/idGeneration.js +182 -0
- package/dist/core/utils/idGeneration.js.map +1 -0
- package/dist/core/utils/index.d.ts +5 -0
- package/dist/core/utils/index.d.ts.map +1 -0
- package/dist/core/utils/index.js +19 -0
- package/dist/core/utils/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/node/crypto/NodeCryptoAdapter.d.ts +16 -0
- package/dist/node/crypto/NodeCryptoAdapter.d.ts.map +1 -0
- package/dist/node/crypto/NodeCryptoAdapter.js +53 -0
- package/dist/node/crypto/NodeCryptoAdapter.js.map +1 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +26 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseMindooTenantDirectory = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
const RSAEncryption_1 = require("./crypto/RSAEncryption");
|
|
6
|
+
class BaseMindooTenantDirectory {
|
|
7
|
+
constructor(tenant) {
|
|
8
|
+
this.directoryDB = null;
|
|
9
|
+
// Cache for trusted public keys: key -> isActive (true = granted, false = revoked)
|
|
10
|
+
// This cache is updated incrementally as changes are processed
|
|
11
|
+
// Note: No recursion guard needed because directory DB is admin-only,
|
|
12
|
+
// so loading entries doesn't trigger validatePublicSigningKey recursively
|
|
13
|
+
this.trustedKeysCache = new Map();
|
|
14
|
+
// Mapping from grant document ID to public key (needed for revocation lookups)
|
|
15
|
+
this.grantDocIdToPublicKey = new Map();
|
|
16
|
+
// Cache for settings documents
|
|
17
|
+
this.tenantSettingsCache = null;
|
|
18
|
+
this.dbSettingsCache = new Map();
|
|
19
|
+
// Cache for groups: key -> merged group data (key: lowercase groupName)
|
|
20
|
+
// We store merged data separately to avoid mutating MindooDoc objects
|
|
21
|
+
// members_hashes are used for lookups, members_encrypted only admin can decrypt
|
|
22
|
+
this.groupsCache = new Map();
|
|
23
|
+
// Unified cache cursor for all document types
|
|
24
|
+
this.unifiedCacheLastCursor = null;
|
|
25
|
+
this.tenant = tenant;
|
|
26
|
+
}
|
|
27
|
+
async getDirectoryDB() {
|
|
28
|
+
if (!this.directoryDB) {
|
|
29
|
+
// Open directory DB with admin-only flag (also enforced at tenant level)
|
|
30
|
+
this.directoryDB = await this.tenant.openDB("directory", { adminOnlyDb: true });
|
|
31
|
+
// Defensive check: verify the DB was opened with admin-only mode
|
|
32
|
+
// This is a security invariant that must always hold
|
|
33
|
+
if (!this.directoryDB.isAdminOnlyDb()) {
|
|
34
|
+
throw new Error("Directory database must be opened with adminOnlyDb=true for security");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return this.directoryDB;
|
|
38
|
+
}
|
|
39
|
+
async registerUser(userId, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
40
|
+
console.log(`[BaseMindooTenantDirectory] Registering user: ${userId.username}`);
|
|
41
|
+
// Check if user with same username (case-insensitive) already exists
|
|
42
|
+
const existingDocs = await this.findGrantAccessDocuments(userId.username);
|
|
43
|
+
if (existingDocs.length > 0) {
|
|
44
|
+
// Check if the keys match the existing registration
|
|
45
|
+
const existingDoc = existingDocs[existingDocs.length - 1]; // Use most recent
|
|
46
|
+
const existingData = existingDoc.getData();
|
|
47
|
+
const keysMatch = existingData.userSigningPublicKey === userId.userSigningPublicKey &&
|
|
48
|
+
existingData.userEncryptionPublicKey === userId.userEncryptionPublicKey;
|
|
49
|
+
if (keysMatch) {
|
|
50
|
+
// Same user with same keys - skip re-registration
|
|
51
|
+
console.log(`[BaseMindooTenantDirectory] User ${userId.username} already registered with same keys, skipping`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Different keys for same username - this is an error
|
|
56
|
+
throw new Error(`Cannot register user "${userId.username}": a user with the same username (case-insensitive) ` +
|
|
57
|
+
`is already registered with different keys`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Cast to BaseMindooTenant to access protected methods
|
|
61
|
+
const baseTenant = this.tenant;
|
|
62
|
+
// Create SigningKeyPair for the administration key
|
|
63
|
+
const adminSigningKeyPair = {
|
|
64
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
65
|
+
privateKey: administrationPrivateKey,
|
|
66
|
+
};
|
|
67
|
+
// Compute username hash and encrypted username before creating the document
|
|
68
|
+
// (since the callback to changeDocWithSigningKey cannot be async for automerge reasons)
|
|
69
|
+
const usernameHash = await this.hashUsername(userId.username);
|
|
70
|
+
const usernameEncrypted = await this.encryptForAdmin(userId.username);
|
|
71
|
+
// Add user to directory database
|
|
72
|
+
console.log(`[BaseMindooTenantDirectory] Creating document for user registration`);
|
|
73
|
+
const directoryDB = await this.getDirectoryDB();
|
|
74
|
+
// Create document with admin signing key so the initial entry is trusted
|
|
75
|
+
// Use PUBLIC_INFOS_KEY_ID so servers can validate users without full tenant access
|
|
76
|
+
const newDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword, types_1.PUBLIC_INFOS_KEY_ID);
|
|
77
|
+
console.log(`[BaseMindooTenantDirectory] Document created: ${newDoc.getId()}`);
|
|
78
|
+
try {
|
|
79
|
+
// Set the document data fields - changeDocWithSigningKey handles signing at entry level
|
|
80
|
+
await directoryDB.changeDocWithSigningKey(newDoc, async (doc) => {
|
|
81
|
+
const data = doc.getData();
|
|
82
|
+
data.form = "useroperation";
|
|
83
|
+
data.type = "grantaccess";
|
|
84
|
+
data.username_hash = usernameHash;
|
|
85
|
+
data.username_encrypted = usernameEncrypted;
|
|
86
|
+
data.userSigningPublicKey = userId.userSigningPublicKey;
|
|
87
|
+
data.userEncryptionPublicKey = userId.userEncryptionPublicKey;
|
|
88
|
+
}, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error(`[BaseMindooTenantDirectory] ERROR in changeDocWithSigningKey:`, error);
|
|
92
|
+
console.error(`[BaseMindooTenantDirectory] Error type: ${error instanceof Error ? error.constructor.name : typeof error}`);
|
|
93
|
+
console.error(`[BaseMindooTenantDirectory] Error message: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
if (error instanceof Error && error.stack) {
|
|
95
|
+
console.error(`[BaseMindooTenantDirectory] Error stack: ${error.stack}`);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
console.log(`[BaseMindooTenantDirectory] Registered user: ${userId.username}`);
|
|
100
|
+
}
|
|
101
|
+
async findGrantAccessDocuments(username) {
|
|
102
|
+
console.log(`[BaseMindooTenantDirectory] Finding grant access documents for username: ${username}`);
|
|
103
|
+
// Sync changes to make sure everything is processed
|
|
104
|
+
const directoryDB = await this.getDirectoryDB();
|
|
105
|
+
await directoryDB.syncStoreChanges();
|
|
106
|
+
// Update unified cache to ensure we have latest data
|
|
107
|
+
await this.updateUnifiedCache();
|
|
108
|
+
// Compute the hash to search for
|
|
109
|
+
const targetHash = await this.hashUsername(username);
|
|
110
|
+
const matchingDocs = [];
|
|
111
|
+
const revokedDocIds = new Set();
|
|
112
|
+
// Use the generator-based iteration API for cleaner code
|
|
113
|
+
// No signature verification needed - DB already enforces admin-only
|
|
114
|
+
for await (const { doc } of directoryDB.iterateChangesSince(null, 100)) {
|
|
115
|
+
const data = doc.getData();
|
|
116
|
+
// Check if this is a grant access document we're looking for (by username_hash)
|
|
117
|
+
if (data.form === "useroperation" &&
|
|
118
|
+
data.type === "grantaccess" &&
|
|
119
|
+
data.username_hash === targetHash) {
|
|
120
|
+
console.log(`[BaseMindooTenantDirectory] Found grant access document for username hash: ${targetHash}`);
|
|
121
|
+
matchingDocs.push(doc);
|
|
122
|
+
}
|
|
123
|
+
// Check if this is a revoke access document for the same username (by username_hash)
|
|
124
|
+
if (data.form === "useroperation" &&
|
|
125
|
+
data.type === "revokeaccess" &&
|
|
126
|
+
data.username_hash === targetHash &&
|
|
127
|
+
data.revokeDocId &&
|
|
128
|
+
typeof data.revokeDocId === "string") {
|
|
129
|
+
console.log(`[BaseMindooTenantDirectory] Found revoke access document for username hash: ${targetHash}, revoking doc ID: ${data.revokeDocId}`);
|
|
130
|
+
revokedDocIds.add(data.revokeDocId);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Filter out revoked documents
|
|
134
|
+
const activeDocs = matchingDocs.filter(doc => !revokedDocIds.has(doc.getId()));
|
|
135
|
+
if (activeDocs.length === 0) {
|
|
136
|
+
console.log(`[BaseMindooTenantDirectory] No active grant access documents found for username: ${username} (found ${matchingDocs.length} total, ${revokedDocIds.size} revoked)`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(`[BaseMindooTenantDirectory] Found ${activeDocs.length} active grant access document(s) for username: ${username} (${matchingDocs.length} total, ${revokedDocIds.size} revoked)`);
|
|
140
|
+
}
|
|
141
|
+
return activeDocs;
|
|
142
|
+
}
|
|
143
|
+
async revokeUser(username, requestDataWipe, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
144
|
+
console.log(`[BaseMindooTenantDirectory] Revoking user: ${username}`);
|
|
145
|
+
// Find all grant access documents for this user
|
|
146
|
+
const grantAccessDocs = await this.findGrantAccessDocuments(username);
|
|
147
|
+
// If no grant access documents found, exit early
|
|
148
|
+
if (grantAccessDocs.length === 0) {
|
|
149
|
+
console.log(`[BaseMindooTenantDirectory] No grant access documents found for ${username}, exiting revocation`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Cast to BaseMindooTenant to access protected methods
|
|
153
|
+
const baseTenant = this.tenant;
|
|
154
|
+
const directoryDB = await this.getDirectoryDB();
|
|
155
|
+
// Create SigningKeyPair for the administration key
|
|
156
|
+
const adminSigningKeyPair = {
|
|
157
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
158
|
+
privateKey: administrationPrivateKey,
|
|
159
|
+
};
|
|
160
|
+
// Compute username hash and encrypted username before creating documents
|
|
161
|
+
const usernameHash = await this.hashUsername(username);
|
|
162
|
+
const usernameEncrypted = await this.encryptForAdmin(username);
|
|
163
|
+
// Create revocation documents for each grant access document found
|
|
164
|
+
for (const grantAccessDoc of grantAccessDocs) {
|
|
165
|
+
const revokeDocId = grantAccessDoc.getId();
|
|
166
|
+
// Create document with admin signing key so the initial entry is trusted
|
|
167
|
+
// Use PUBLIC_INFOS_KEY_ID so servers can validate users without full tenant access
|
|
168
|
+
const newDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword, types_1.PUBLIC_INFOS_KEY_ID);
|
|
169
|
+
// Set the document data fields - changeDocWithSigningKey handles signing at entry level
|
|
170
|
+
await directoryDB.changeDocWithSigningKey(newDoc, async (doc) => {
|
|
171
|
+
const data = doc.getData();
|
|
172
|
+
data.form = "useroperation";
|
|
173
|
+
data.type = "revokeaccess";
|
|
174
|
+
data.username_hash = usernameHash;
|
|
175
|
+
data.username_encrypted = usernameEncrypted;
|
|
176
|
+
data.revokeDocId = revokeDocId;
|
|
177
|
+
data.requestDataWipe = requestDataWipe;
|
|
178
|
+
}, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
179
|
+
console.log(`[BaseMindooTenantDirectory] Created revocation document for grant access doc: ${revokeDocId}`);
|
|
180
|
+
}
|
|
181
|
+
console.log(`[BaseMindooTenantDirectory] Revoked user: ${username} (created ${grantAccessDocs.length} revocation document(s))`);
|
|
182
|
+
}
|
|
183
|
+
async validatePublicSigningKey(publicKey) {
|
|
184
|
+
console.log(`[BaseMindooTenantDirectory] Validating public signing key`);
|
|
185
|
+
// Cast to BaseMindooTenant to access protected methods
|
|
186
|
+
const baseTenant = this.tenant;
|
|
187
|
+
// Get administration public key - this is always trusted as the root of trust
|
|
188
|
+
const administrationPublicKey = baseTenant.getAdministrationPublicKey();
|
|
189
|
+
// The administration key is always trusted - it's the root of trust for the tenant
|
|
190
|
+
if (publicKey === administrationPublicKey) {
|
|
191
|
+
console.log(`[BaseMindooTenantDirectory] Public key is administration key, trusted`);
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
// Check cache first - if we have a cached result, return it after ensuring we're up to date
|
|
195
|
+
// Sync changes to make sure everything is processed
|
|
196
|
+
const directoryDB = await this.getDirectoryDB();
|
|
197
|
+
await directoryDB.syncStoreChanges();
|
|
198
|
+
// Update unified cache with any new changes since last cursor
|
|
199
|
+
await this.updateUnifiedCache();
|
|
200
|
+
// Now check the cache
|
|
201
|
+
const cachedResult = this.trustedKeysCache.get(publicKey);
|
|
202
|
+
if (cachedResult !== undefined) {
|
|
203
|
+
console.log(`[BaseMindooTenantDirectory] Public key validation result (from cache): ${cachedResult}`);
|
|
204
|
+
return cachedResult;
|
|
205
|
+
}
|
|
206
|
+
// Key not found in cache means it was never granted access
|
|
207
|
+
console.log(`[BaseMindooTenantDirectory] Public key not found in cache, returning false`);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Update all caches (trusted keys, groups, settings) by processing new changes since the last cursor.
|
|
212
|
+
* This unified method processes all document types in a single loop for efficiency.
|
|
213
|
+
* No signature verification is needed since the DB already enforces admin-only access.
|
|
214
|
+
*/
|
|
215
|
+
async updateUnifiedCache() {
|
|
216
|
+
const directoryDB = await this.getDirectoryDB();
|
|
217
|
+
// Determine starting cursor (null = process all, otherwise incremental)
|
|
218
|
+
const startCursor = this.unifiedCacheLastCursor;
|
|
219
|
+
// If processing from the beginning, clear all caches first
|
|
220
|
+
if (startCursor === null) {
|
|
221
|
+
this.trustedKeysCache.clear();
|
|
222
|
+
this.grantDocIdToPublicKey.clear();
|
|
223
|
+
this.tenantSettingsCache = null;
|
|
224
|
+
this.dbSettingsCache.clear();
|
|
225
|
+
this.groupsCache.clear();
|
|
226
|
+
}
|
|
227
|
+
// Track group documents by name for merging (handles offline sync scenarios)
|
|
228
|
+
const groupDocsByName = new Map();
|
|
229
|
+
// Process changes (documents are returned in order by lastModified, oldest to newest)
|
|
230
|
+
for await (const { doc, cursor } of directoryDB.iterateChangesSince(startCursor, 100)) {
|
|
231
|
+
const data = doc.getData();
|
|
232
|
+
// Process user operation documents (grant/revoke access)
|
|
233
|
+
if (data.form === "useroperation") {
|
|
234
|
+
// Check if this is a grant access document
|
|
235
|
+
if (data.type === "grantaccess" &&
|
|
236
|
+
data.userSigningPublicKey &&
|
|
237
|
+
typeof data.userSigningPublicKey === "string") {
|
|
238
|
+
const userPublicKey = data.userSigningPublicKey;
|
|
239
|
+
console.log(`[BaseMindooTenantDirectory] Cache: adding trusted key from grant access document`);
|
|
240
|
+
// Mark as active (true) unless already revoked
|
|
241
|
+
if (!this.trustedKeysCache.has(userPublicKey) || this.trustedKeysCache.get(userPublicKey) === true) {
|
|
242
|
+
this.trustedKeysCache.set(userPublicKey, true);
|
|
243
|
+
}
|
|
244
|
+
// Store mapping for future revocation lookups (persisted across iterations)
|
|
245
|
+
this.grantDocIdToPublicKey.set(doc.getId(), userPublicKey);
|
|
246
|
+
}
|
|
247
|
+
// Check if this is a revoke access document
|
|
248
|
+
if (data.type === "revokeaccess" &&
|
|
249
|
+
data.revokeDocId &&
|
|
250
|
+
typeof data.revokeDocId === "string") {
|
|
251
|
+
const revokedDocId = data.revokeDocId;
|
|
252
|
+
// Find the public key for this grant doc ID and mark as revoked
|
|
253
|
+
// Use the persistent mapping that survives across iterations
|
|
254
|
+
const revokedPublicKey = this.grantDocIdToPublicKey.get(revokedDocId);
|
|
255
|
+
if (revokedPublicKey) {
|
|
256
|
+
console.log(`[BaseMindooTenantDirectory] Cache: revoking key for doc ${revokedDocId}`);
|
|
257
|
+
this.trustedKeysCache.set(revokedPublicKey, false);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.warn(`[BaseMindooTenantDirectory] Cache: revocation for unknown grant doc ${revokedDocId}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Process group documents
|
|
265
|
+
if (data.form === "group" &&
|
|
266
|
+
data.type === "group" &&
|
|
267
|
+
data.groupName &&
|
|
268
|
+
typeof data.groupName === "string") {
|
|
269
|
+
const normalizedGroupName = this.normalizeGroupName(data.groupName);
|
|
270
|
+
if (!groupDocsByName.has(normalizedGroupName)) {
|
|
271
|
+
groupDocsByName.set(normalizedGroupName, []);
|
|
272
|
+
}
|
|
273
|
+
groupDocsByName.get(normalizedGroupName).push(doc);
|
|
274
|
+
}
|
|
275
|
+
// Process tenant settings (no signature verification needed - Automerge handles merging)
|
|
276
|
+
if (data.form === "tenantsettings") {
|
|
277
|
+
// Just overwrite - last one seen will be the latest
|
|
278
|
+
this.tenantSettingsCache = doc;
|
|
279
|
+
}
|
|
280
|
+
// Process DB settings (no signature verification needed - Automerge handles merging)
|
|
281
|
+
if (data.form === "dbsettings" && data.dbid && typeof data.dbid === "string") {
|
|
282
|
+
const dbId = data.dbid;
|
|
283
|
+
// Just overwrite - last one seen will be the latest
|
|
284
|
+
this.dbSettingsCache.set(dbId, doc);
|
|
285
|
+
}
|
|
286
|
+
// Update cursor after each document
|
|
287
|
+
this.unifiedCacheLastCursor = cursor;
|
|
288
|
+
}
|
|
289
|
+
// Merge group documents with the same name
|
|
290
|
+
for (const [normalizedGroupName, docs] of groupDocsByName.entries()) {
|
|
291
|
+
// Collect all member hashes from all documents with this group name
|
|
292
|
+
const allMembersHashes = new Set();
|
|
293
|
+
let latestDoc = null;
|
|
294
|
+
let isDeleted = false;
|
|
295
|
+
for (const doc of docs) {
|
|
296
|
+
const data = doc.getData();
|
|
297
|
+
// Track the latest document (by iteration order, which is by lastModified)
|
|
298
|
+
latestDoc = doc;
|
|
299
|
+
// Check if document is deleted (via isDeleted() or _deleted flag in data)
|
|
300
|
+
if (doc.isDeleted() || data._deleted === true) {
|
|
301
|
+
isDeleted = true;
|
|
302
|
+
}
|
|
303
|
+
// Use members_hashes for the cache (privacy-preserving lookups)
|
|
304
|
+
if (data.members_hashes && Array.isArray(data.members_hashes)) {
|
|
305
|
+
for (const memberHash of data.members_hashes) {
|
|
306
|
+
if (typeof memberHash === "string") {
|
|
307
|
+
allMembersHashes.add(memberHash);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// If latest version is deleted, remove from cache
|
|
313
|
+
if (isDeleted) {
|
|
314
|
+
this.groupsCache.delete(normalizedGroupName);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// Store merged group data in cache (without mutating the MindooDoc)
|
|
318
|
+
if (latestDoc) {
|
|
319
|
+
this.groupsCache.set(normalizedGroupName, {
|
|
320
|
+
docId: latestDoc.getId(),
|
|
321
|
+
members_hashes: Array.from(allMembersHashes),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Normalize group name to lowercase for case-insensitive comparison.
|
|
328
|
+
*/
|
|
329
|
+
normalizeGroupName(name) {
|
|
330
|
+
return name.toLowerCase();
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Generate username variants with wildcards for hierarchical matching.
|
|
334
|
+
* For example: "CN=john/OU=team1/O=example.com" becomes an array containing:
|
|
335
|
+
* - The original username
|
|
336
|
+
* - Wildcard variants like "*\/OU=team1/O=example.com", "*\/O=example.com", and "*"
|
|
337
|
+
*/
|
|
338
|
+
generateUsernameVariants(username) {
|
|
339
|
+
const variants = [username]; // Start with original username
|
|
340
|
+
// Parse the username format: CN=<name>/OU=<ou>/O=<org> (may have multiple OU components)
|
|
341
|
+
// Split by '/' to get components
|
|
342
|
+
const parts = username.split('/');
|
|
343
|
+
// Generate variants by replacing prefixes with a single wildcard
|
|
344
|
+
// For "CN=john/OU=team1/O=example.com":
|
|
345
|
+
// - */OU=team1/O=example.com (skip CN)
|
|
346
|
+
// - */O=example.com (skip CN and OU)
|
|
347
|
+
for (let i = 1; i < parts.length; i++) {
|
|
348
|
+
const suffixParts = parts.slice(i);
|
|
349
|
+
variants.push('*/' + suffixParts.join('/'));
|
|
350
|
+
}
|
|
351
|
+
// Always add the final wildcard
|
|
352
|
+
variants.push('*');
|
|
353
|
+
return variants;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Recursively resolve all groups that contain the given username (directly or via nested groups).
|
|
357
|
+
* This resolves upwards: if user is in group A, and group A is in group B, then user is also in group B.
|
|
358
|
+
* Stops on cycles with a console warning.
|
|
359
|
+
*/
|
|
360
|
+
async resolveGroupsForUser(username) {
|
|
361
|
+
const resultGroups = new Set();
|
|
362
|
+
// Sync changes and update cache
|
|
363
|
+
const directoryDB = await this.getDirectoryDB();
|
|
364
|
+
await directoryDB.syncStoreChanges();
|
|
365
|
+
await this.updateUnifiedCache();
|
|
366
|
+
// Generate username variants for matching and compute their hashes
|
|
367
|
+
const usernameVariants = this.generateUsernameVariants(username);
|
|
368
|
+
// Hash all variants for comparison with members_hashes
|
|
369
|
+
const variantHashes = new Set(await Promise.all(usernameVariants.map(v => this.hashUsername(v))));
|
|
370
|
+
// First pass: find all groups that directly contain the user or username variants (by hash)
|
|
371
|
+
const directGroups = new Set();
|
|
372
|
+
for (const [groupName, groupData] of this.groupsCache.entries()) {
|
|
373
|
+
for (const memberHash of groupData.members_hashes) {
|
|
374
|
+
if (variantHashes.has(memberHash)) {
|
|
375
|
+
directGroups.add(groupName);
|
|
376
|
+
resultGroups.add(groupName);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Second pass: find parent groups (groups that contain the user's groups)
|
|
382
|
+
// Groups can be nested - a group name can be a member of another group
|
|
383
|
+
// Keep iterating until no new groups are found
|
|
384
|
+
let groupsToCheck = new Set(directGroups);
|
|
385
|
+
const visitedGroups = new Set(); // Track visited groups for cycle detection
|
|
386
|
+
while (groupsToCheck.size > 0) {
|
|
387
|
+
const nextGroups = new Set();
|
|
388
|
+
for (const childGroup of groupsToCheck) {
|
|
389
|
+
// Skip if already checked (cycle detection)
|
|
390
|
+
if (visitedGroups.has(childGroup)) {
|
|
391
|
+
console.warn(`[BaseMindooTenantDirectory] Cycle detected in group resolution: ${childGroup} already visited, stopping recursion`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
visitedGroups.add(childGroup);
|
|
395
|
+
// Hash the child group name for comparison
|
|
396
|
+
const childGroupHash = await this.hashUsername(childGroup);
|
|
397
|
+
// Find all groups that contain this child group as a member (by hash)
|
|
398
|
+
for (const [parentGroupName, parentGroupData] of this.groupsCache.entries()) {
|
|
399
|
+
if (resultGroups.has(parentGroupName)) {
|
|
400
|
+
continue; // Already found this group
|
|
401
|
+
}
|
|
402
|
+
if (parentGroupData.members_hashes.includes(childGroupHash)) {
|
|
403
|
+
resultGroups.add(parentGroupName);
|
|
404
|
+
nextGroups.add(parentGroupName);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
groupsToCheck = nextGroups;
|
|
409
|
+
}
|
|
410
|
+
return Array.from(resultGroups);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get a user's public keys from the directory.
|
|
414
|
+
* Used for authentication (signature verification) and encryption (transport encryption).
|
|
415
|
+
*
|
|
416
|
+
* @param username The username to look up (format: "CN=<username>/O=<tenantId>")
|
|
417
|
+
* @return The user's public keys, or null if user not found or has been revoked
|
|
418
|
+
*/
|
|
419
|
+
async getUserPublicKeys(username) {
|
|
420
|
+
console.log(`[BaseMindooTenantDirectory] Getting public keys for username: ${username}`);
|
|
421
|
+
// Find all active (non-revoked) grant access documents for this user
|
|
422
|
+
const activeDocs = await this.findGrantAccessDocuments(username);
|
|
423
|
+
if (activeDocs.length === 0) {
|
|
424
|
+
console.log(`[BaseMindooTenantDirectory] No active grant access documents found for username: ${username}`);
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
// Use the last active grant access document (most recent registration)
|
|
428
|
+
// In most cases there should only be one active grant access document per user
|
|
429
|
+
const doc = activeDocs[activeDocs.length - 1];
|
|
430
|
+
const data = doc.getData();
|
|
431
|
+
const signingPublicKey = data.userSigningPublicKey;
|
|
432
|
+
const encryptionPublicKey = data.userEncryptionPublicKey;
|
|
433
|
+
if (typeof signingPublicKey !== "string" || typeof encryptionPublicKey !== "string") {
|
|
434
|
+
console.error(`[BaseMindooTenantDirectory] Invalid key data in grant access document for username: ${username}`);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
console.log(`[BaseMindooTenantDirectory] Found public keys for username: ${username}`);
|
|
438
|
+
return {
|
|
439
|
+
signingPublicKey,
|
|
440
|
+
encryptionPublicKey,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Check if a user has been revoked.
|
|
445
|
+
* A user is considered revoked if they have no active (non-revoked) grant access documents.
|
|
446
|
+
*
|
|
447
|
+
* @param username The username to check (format: "CN=<username>/O=<tenantId>")
|
|
448
|
+
* @return True if the user has been revoked, false if they have active access
|
|
449
|
+
*/
|
|
450
|
+
async isUserRevoked(username) {
|
|
451
|
+
console.log(`[BaseMindooTenantDirectory] Checking if user is revoked: ${username}`);
|
|
452
|
+
// Find all active grant access documents for this user
|
|
453
|
+
const activeDocs = await this.findGrantAccessDocuments(username);
|
|
454
|
+
// User is revoked if they have no active grant access documents
|
|
455
|
+
const isRevoked = activeDocs.length === 0;
|
|
456
|
+
console.log(`[BaseMindooTenantDirectory] User ${username} revoked: ${isRevoked}`);
|
|
457
|
+
return isRevoked;
|
|
458
|
+
}
|
|
459
|
+
async requestDocHistoryPurge(dbId, docId, reason, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
460
|
+
console.log(`[BaseMindooTenantDirectory] Requesting purge for document: ${docId} in ${dbId}`);
|
|
461
|
+
const baseTenant = this.tenant;
|
|
462
|
+
const directoryDB = await this.getDirectoryDB();
|
|
463
|
+
const adminSigningKeyPair = {
|
|
464
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
465
|
+
privateKey: administrationPrivateKey,
|
|
466
|
+
};
|
|
467
|
+
// Create document with admin signing key so the initial entry is trusted
|
|
468
|
+
const newDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
469
|
+
// Set the document data fields - changeDocWithSigningKey handles signing at entry level
|
|
470
|
+
await directoryDB.changeDocWithSigningKey(newDoc, async (doc) => {
|
|
471
|
+
const data = doc.getData();
|
|
472
|
+
data.form = "useroperation";
|
|
473
|
+
data.type = "requestdochistorypurge";
|
|
474
|
+
data.dbId = dbId;
|
|
475
|
+
data.docId = docId;
|
|
476
|
+
if (reason) {
|
|
477
|
+
data.reason = reason;
|
|
478
|
+
}
|
|
479
|
+
data.requestedAt = Date.now();
|
|
480
|
+
}, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
481
|
+
console.log(`[BaseMindooTenantDirectory] Created purge request for ${docId} in ${dbId}`);
|
|
482
|
+
}
|
|
483
|
+
async getRequestedDocHistoryPurges() {
|
|
484
|
+
const directoryDB = await this.getDirectoryDB();
|
|
485
|
+
await directoryDB.syncStoreChanges();
|
|
486
|
+
const purgeRequests = [];
|
|
487
|
+
// No signature verification needed - DB already enforces admin-only
|
|
488
|
+
for await (const { doc } of directoryDB.iterateChangesSince(null, 100)) {
|
|
489
|
+
const data = doc.getData();
|
|
490
|
+
if (data.form === "useroperation" && data.type === "requestdochistorypurge") {
|
|
491
|
+
const dbId = data.dbId;
|
|
492
|
+
const docId = data.docId;
|
|
493
|
+
const reason = data.reason;
|
|
494
|
+
const requestedAt = data.requestedAt;
|
|
495
|
+
if (typeof dbId === "string" && typeof docId === "string") {
|
|
496
|
+
purgeRequests.push({
|
|
497
|
+
dbId: dbId,
|
|
498
|
+
docId: docId,
|
|
499
|
+
reason: typeof reason === "string" ? reason : undefined,
|
|
500
|
+
requestedAt: typeof requestedAt === "number" ? requestedAt : Date.now(),
|
|
501
|
+
purgeRequestDocId: doc.getId(),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return purgeRequests;
|
|
507
|
+
}
|
|
508
|
+
async getTenantSettings() {
|
|
509
|
+
const directoryDB = await this.getDirectoryDB();
|
|
510
|
+
await directoryDB.syncStoreChanges();
|
|
511
|
+
// Update unified cache if directory has new changes
|
|
512
|
+
await this.updateUnifiedCache();
|
|
513
|
+
return this.tenantSettingsCache;
|
|
514
|
+
}
|
|
515
|
+
async getDBSettings(dbId) {
|
|
516
|
+
const directoryDB = await this.getDirectoryDB();
|
|
517
|
+
await directoryDB.syncStoreChanges();
|
|
518
|
+
// Update unified cache if directory has new changes
|
|
519
|
+
await this.updateUnifiedCache();
|
|
520
|
+
return this.dbSettingsCache.get(dbId) || null;
|
|
521
|
+
}
|
|
522
|
+
async changeTenantSettings(changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
523
|
+
const baseTenant = this.tenant;
|
|
524
|
+
const directoryDB = await this.getDirectoryDB();
|
|
525
|
+
const adminSigningKeyPair = {
|
|
526
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
527
|
+
privateKey: administrationPrivateKey,
|
|
528
|
+
};
|
|
529
|
+
// Get cached settings doc or create new one
|
|
530
|
+
let settingsDoc = await this.getTenantSettings();
|
|
531
|
+
if (!settingsDoc) {
|
|
532
|
+
// Create new document
|
|
533
|
+
settingsDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
534
|
+
}
|
|
535
|
+
// Apply changes and ensure form field is correct
|
|
536
|
+
await directoryDB.changeDocWithSigningKey(settingsDoc, async (d) => {
|
|
537
|
+
// Call user's change function
|
|
538
|
+
await changeFunc(d);
|
|
539
|
+
// Ensure form field is always correct (overwrite if user changed it)
|
|
540
|
+
d.getData().form = "tenantsettings";
|
|
541
|
+
}, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
542
|
+
// Invalidate cache
|
|
543
|
+
this.tenantSettingsCache = null;
|
|
544
|
+
this.unifiedCacheLastCursor = null;
|
|
545
|
+
}
|
|
546
|
+
async changeDBSettings(dbId, changeFunc, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
547
|
+
const baseTenant = this.tenant;
|
|
548
|
+
const directoryDB = await this.getDirectoryDB();
|
|
549
|
+
const adminSigningKeyPair = {
|
|
550
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
551
|
+
privateKey: administrationPrivateKey,
|
|
552
|
+
};
|
|
553
|
+
// Get cached settings doc for this dbId or create new one
|
|
554
|
+
let settingsDoc = await this.getDBSettings(dbId);
|
|
555
|
+
if (!settingsDoc) {
|
|
556
|
+
// Create new document
|
|
557
|
+
settingsDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
558
|
+
}
|
|
559
|
+
// Apply changes and ensure form and dbid fields are correct
|
|
560
|
+
await directoryDB.changeDocWithSigningKey(settingsDoc, async (d) => {
|
|
561
|
+
// Call user's change function
|
|
562
|
+
await changeFunc(d);
|
|
563
|
+
// Ensure form and dbid fields are always correct (overwrite if user changed them)
|
|
564
|
+
const data = d.getData();
|
|
565
|
+
data.form = "dbsettings";
|
|
566
|
+
data.dbid = dbId;
|
|
567
|
+
}, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
568
|
+
// Invalidate cache for this dbId
|
|
569
|
+
this.dbSettingsCache.delete(dbId);
|
|
570
|
+
this.unifiedCacheLastCursor = null;
|
|
571
|
+
}
|
|
572
|
+
async getGroups() {
|
|
573
|
+
const directoryDB = await this.getDirectoryDB();
|
|
574
|
+
await directoryDB.syncStoreChanges();
|
|
575
|
+
// Update unified cache to ensure we have latest data
|
|
576
|
+
await this.updateUnifiedCache();
|
|
577
|
+
// Return array of group names (keys from groupsCache)
|
|
578
|
+
return Array.from(this.groupsCache.keys());
|
|
579
|
+
}
|
|
580
|
+
async getGroupMembers(groupName) {
|
|
581
|
+
const normalizedGroupName = this.normalizeGroupName(groupName);
|
|
582
|
+
const directoryDB = await this.getDirectoryDB();
|
|
583
|
+
await directoryDB.syncStoreChanges();
|
|
584
|
+
// Update unified cache to ensure we have latest data
|
|
585
|
+
await this.updateUnifiedCache();
|
|
586
|
+
// Look up group in cache
|
|
587
|
+
const groupData = this.groupsCache.get(normalizedGroupName);
|
|
588
|
+
if (!groupData) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
// Return member hashes (actual usernames are encrypted and only admin can decrypt)
|
|
592
|
+
// This allows checking membership via hash comparison
|
|
593
|
+
return [...groupData.members_hashes];
|
|
594
|
+
}
|
|
595
|
+
async addUsersToGroup(groupName, username, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
596
|
+
const normalizedGroupName = this.normalizeGroupName(groupName);
|
|
597
|
+
const baseTenant = this.tenant;
|
|
598
|
+
const directoryDB = await this.getDirectoryDB();
|
|
599
|
+
const adminSigningKeyPair = {
|
|
600
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
601
|
+
privateKey: administrationPrivateKey,
|
|
602
|
+
};
|
|
603
|
+
// Compute hashes and encrypted values for all new usernames
|
|
604
|
+
const newMembersHashes = await Promise.all(username.map(u => this.hashUsername(u)));
|
|
605
|
+
const newMembersEncrypted = await Promise.all(username.map(u => this.encryptForAdmin(u)));
|
|
606
|
+
// Get the actual document from the database (using cached docId if available)
|
|
607
|
+
const cachedGroup = this.groupsCache.get(normalizedGroupName);
|
|
608
|
+
let groupDoc;
|
|
609
|
+
if (cachedGroup) {
|
|
610
|
+
// Load the actual document from the database
|
|
611
|
+
groupDoc = await directoryDB.getDocument(cachedGroup.docId);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
// Create new document with PUBLIC_INFOS_KEY_ID for server access
|
|
615
|
+
groupDoc = await directoryDB.createDocumentWithSigningKey(adminSigningKeyPair, administrationPrivateKeyPassword, types_1.PUBLIC_INFOS_KEY_ID);
|
|
616
|
+
}
|
|
617
|
+
// Apply changes and ensure form, type, and groupName fields are correct
|
|
618
|
+
await directoryDB.changeDocWithSigningKey(groupDoc, async (d) => {
|
|
619
|
+
const data = d.getData();
|
|
620
|
+
// Get existing arrays (copy them to avoid mutating in place)
|
|
621
|
+
const existingHashes = Array.isArray(data.members_hashes)
|
|
622
|
+
? data.members_hashes.filter((h) => typeof h === "string")
|
|
623
|
+
: [];
|
|
624
|
+
const existingEncrypted = Array.isArray(data.members_encrypted)
|
|
625
|
+
? data.members_encrypted.filter((e) => typeof e === "string")
|
|
626
|
+
: [];
|
|
627
|
+
// Build new arrays with existing + new members (avoiding duplicates by hash)
|
|
628
|
+
const existingHashesSet = new Set(existingHashes);
|
|
629
|
+
const updatedHashes = [...existingHashes];
|
|
630
|
+
const updatedEncrypted = [...existingEncrypted];
|
|
631
|
+
for (let i = 0; i < newMembersHashes.length; i++) {
|
|
632
|
+
if (!existingHashesSet.has(newMembersHashes[i])) {
|
|
633
|
+
updatedHashes.push(newMembersHashes[i]);
|
|
634
|
+
updatedEncrypted.push(newMembersEncrypted[i]);
|
|
635
|
+
existingHashesSet.add(newMembersHashes[i]);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// Assign the complete arrays (this triggers the proxy's set handler)
|
|
639
|
+
data.members_hashes = updatedHashes;
|
|
640
|
+
data.members_encrypted = updatedEncrypted;
|
|
641
|
+
// Ensure form, type, and groupName fields are always correct
|
|
642
|
+
data.form = "group";
|
|
643
|
+
data.type = "group";
|
|
644
|
+
data.groupName = normalizedGroupName;
|
|
645
|
+
}, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
646
|
+
// Invalidate cache for this group
|
|
647
|
+
this.groupsCache.delete(normalizedGroupName);
|
|
648
|
+
this.unifiedCacheLastCursor = null;
|
|
649
|
+
}
|
|
650
|
+
async removeUsersFromGroup(groupName, username, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
651
|
+
const normalizedGroupName = this.normalizeGroupName(groupName);
|
|
652
|
+
const baseTenant = this.tenant;
|
|
653
|
+
const directoryDB = await this.getDirectoryDB();
|
|
654
|
+
// Sync and update cache to get latest group
|
|
655
|
+
await directoryDB.syncStoreChanges();
|
|
656
|
+
await this.updateUnifiedCache();
|
|
657
|
+
// Get group from cache
|
|
658
|
+
const cachedGroup = this.groupsCache.get(normalizedGroupName);
|
|
659
|
+
if (!cachedGroup) {
|
|
660
|
+
// Group doesn't exist, nothing to do
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
// Load the actual document from the database
|
|
664
|
+
const groupDoc = await directoryDB.getDocument(cachedGroup.docId);
|
|
665
|
+
const adminSigningKeyPair = {
|
|
666
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
667
|
+
privateKey: administrationPrivateKey,
|
|
668
|
+
};
|
|
669
|
+
// Compute hashes of usernames to remove
|
|
670
|
+
const hashesToRemove = new Set(await Promise.all(username.map(u => this.hashUsername(u))));
|
|
671
|
+
// Remove users from members arrays
|
|
672
|
+
await directoryDB.changeDocWithSigningKey(groupDoc, async (d) => {
|
|
673
|
+
const data = d.getData();
|
|
674
|
+
if (!data.members_hashes || !Array.isArray(data.members_hashes) ||
|
|
675
|
+
!data.members_encrypted || !Array.isArray(data.members_encrypted)) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
// Filter out members whose hashes match the ones to remove
|
|
679
|
+
// Keep arrays in sync by filtering indices
|
|
680
|
+
const indicesToKeep = [];
|
|
681
|
+
const hashesArray = data.members_hashes;
|
|
682
|
+
for (let i = 0; i < hashesArray.length; i++) {
|
|
683
|
+
if (!hashesToRemove.has(hashesArray[i])) {
|
|
684
|
+
indicesToKeep.push(i);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const encryptedArray = data.members_encrypted;
|
|
688
|
+
data.members_hashes = indicesToKeep.map(i => hashesArray[i]);
|
|
689
|
+
data.members_encrypted = indicesToKeep.map(i => encryptedArray[i]);
|
|
690
|
+
// Ensure form, type, and groupName fields are always correct
|
|
691
|
+
data.form = "group";
|
|
692
|
+
data.type = "group";
|
|
693
|
+
data.groupName = normalizedGroupName;
|
|
694
|
+
}, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
695
|
+
// Invalidate cache for this group
|
|
696
|
+
this.groupsCache.delete(normalizedGroupName);
|
|
697
|
+
this.unifiedCacheLastCursor = null;
|
|
698
|
+
}
|
|
699
|
+
async deleteGroup(groupName, administrationPrivateKey, administrationPrivateKeyPassword) {
|
|
700
|
+
const normalizedGroupName = this.normalizeGroupName(groupName);
|
|
701
|
+
const baseTenant = this.tenant;
|
|
702
|
+
const directoryDB = await this.getDirectoryDB();
|
|
703
|
+
// Sync and update cache to get latest group
|
|
704
|
+
await directoryDB.syncStoreChanges();
|
|
705
|
+
await this.updateUnifiedCache();
|
|
706
|
+
// Get group from cache
|
|
707
|
+
const cachedGroup = this.groupsCache.get(normalizedGroupName);
|
|
708
|
+
if (!cachedGroup) {
|
|
709
|
+
// Group doesn't exist, nothing to do
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const adminSigningKeyPair = {
|
|
713
|
+
publicKey: baseTenant.getAdministrationPublicKey(),
|
|
714
|
+
privateKey: administrationPrivateKey,
|
|
715
|
+
};
|
|
716
|
+
// Delete the document using the proper API
|
|
717
|
+
await directoryDB.deleteDocumentWithSigningKey(cachedGroup.docId, adminSigningKeyPair, administrationPrivateKeyPassword);
|
|
718
|
+
// Remove from cache
|
|
719
|
+
this.groupsCache.delete(normalizedGroupName);
|
|
720
|
+
this.unifiedCacheLastCursor = null;
|
|
721
|
+
}
|
|
722
|
+
async getUserNamesList(username) {
|
|
723
|
+
// Generate username variants (wildcards)
|
|
724
|
+
const usernameVariants = this.generateUsernameVariants(username);
|
|
725
|
+
const result = new Set(usernameVariants);
|
|
726
|
+
// Find all groups containing the username (directly or via nested groups)
|
|
727
|
+
const groups = await this.resolveGroupsForUser(username);
|
|
728
|
+
// Add all groups to result
|
|
729
|
+
for (const group of groups) {
|
|
730
|
+
result.add(group);
|
|
731
|
+
}
|
|
732
|
+
return Array.from(result);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Compute SHA-256 hash of lowercase username for lookups.
|
|
736
|
+
* This enables searching for users without exposing the actual username.
|
|
737
|
+
*
|
|
738
|
+
* @param username The username to hash
|
|
739
|
+
* @return The hex-encoded SHA-256 hash of the lowercase username
|
|
740
|
+
*/
|
|
741
|
+
async hashUsername(username) {
|
|
742
|
+
const encoder = new TextEncoder();
|
|
743
|
+
const data = encoder.encode(username.toLowerCase());
|
|
744
|
+
const subtle = this.tenant.getCryptoAdapter().getSubtle();
|
|
745
|
+
const hashBuffer = await subtle.digest('SHA-256', data);
|
|
746
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
747
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
748
|
+
.join('');
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Encrypt plaintext with admin's RSA public encryption key.
|
|
752
|
+
* Only the admin with the corresponding private key can decrypt.
|
|
753
|
+
*
|
|
754
|
+
* @param plaintext The text to encrypt
|
|
755
|
+
* @return The base64-encoded encrypted data
|
|
756
|
+
*/
|
|
757
|
+
async encryptForAdmin(plaintext) {
|
|
758
|
+
const adminEncKey = this.tenant.getAdministrationEncryptionPublicKey();
|
|
759
|
+
const rsaEnc = new RSAEncryption_1.RSAEncryption(this.tenant.getCryptoAdapter());
|
|
760
|
+
const encoder = new TextEncoder();
|
|
761
|
+
const encrypted = await rsaEnc.encrypt(encoder.encode(plaintext), adminEncKey);
|
|
762
|
+
return this.uint8ArrayToBase64(encrypted);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Convert a Uint8Array to a base64 string.
|
|
766
|
+
*/
|
|
767
|
+
uint8ArrayToBase64(bytes) {
|
|
768
|
+
let binary = "";
|
|
769
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
770
|
+
binary += String.fromCharCode(bytes[i]);
|
|
771
|
+
}
|
|
772
|
+
return btoa(binary);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
exports.BaseMindooTenantDirectory = BaseMindooTenantDirectory;
|
|
776
|
+
//# sourceMappingURL=BaseMindooTenantDirectory.js.map
|