@flowerforce/flowerbase 1.7.5 → 1.7.6-beta.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 +125 -1
- package/dist/auth/controller.d.ts.map +1 -1
- package/dist/auth/controller.js +11 -10
- package/dist/auth/plugins/jwt.js +1 -1
- package/dist/auth/providers/anon-user/controller.js +1 -1
- package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
- package/dist/auth/providers/custom-function/controller.js +28 -7
- package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
- package/dist/auth/providers/local-userpass/controller.js +15 -14
- package/dist/auth/utils.d.ts +1 -0
- package/dist/auth/utils.d.ts.map +1 -1
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -3
- package/dist/features/encryption/interface.d.ts +36 -0
- package/dist/features/encryption/interface.d.ts.map +1 -0
- package/dist/features/encryption/interface.js +2 -0
- package/dist/features/encryption/utils.d.ts +9 -0
- package/dist/features/encryption/utils.d.ts.map +1 -0
- package/dist/features/encryption/utils.js +34 -0
- package/dist/features/rules/utils.d.ts.map +1 -1
- package/dist/features/rules/utils.js +1 -11
- package/dist/features/triggers/index.d.ts.map +1 -1
- package/dist/features/triggers/index.js +5 -1
- package/dist/features/triggers/utils.js +3 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/monitoring/plugin.d.ts.map +1 -1
- package/dist/monitoring/plugin.js +31 -0
- package/dist/monitoring/routes/users.d.ts.map +1 -1
- package/dist/monitoring/routes/users.js +7 -6
- package/dist/monitoring/utils.d.ts.map +1 -1
- package/dist/monitoring/utils.js +5 -4
- package/dist/services/api/index.d.ts +4 -0
- package/dist/services/api/index.d.ts.map +1 -1
- package/dist/services/api/utils.d.ts +1 -0
- package/dist/services/api/utils.d.ts.map +1 -1
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +9 -7
- package/dist/services/mongodb-atlas/model.d.ts +2 -1
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/shared/handleUserDeletion.js +1 -1
- package/dist/shared/handleUserRegistration.js +2 -2
- package/dist/utils/context/helpers.d.ts +12 -0
- package/dist/utils/context/helpers.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +14 -3
- package/dist/utils/initializer/exposeRoutes.js +1 -1
- package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
- package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
- package/dist/utils/initializer/mongodbCSFLE.js +131 -0
- package/dist/utils/initializer/registerPlugins.d.ts +5 -1
- package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
- package/dist/utils/initializer/registerPlugins.js +27 -5
- package/dist/utils/rules-matcher/interface.d.ts +5 -1
- package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
- package/dist/utils/rules-matcher/interface.js +2 -0
- package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
- package/dist/utils/rules-matcher/utils.js +51 -16
- package/package.json +4 -2
- package/src/auth/__tests__/controller.test.ts +1 -0
- package/src/auth/controller.ts +12 -11
- package/src/auth/plugins/jwt.ts +2 -2
- package/src/auth/providers/anon-user/__tests__/controller.test.ts +1 -0
- package/src/auth/providers/anon-user/controller.ts +2 -2
- package/src/auth/providers/custom-function/controller.ts +29 -8
- package/src/auth/providers/local-userpass/controller.ts +16 -15
- package/src/auth/utils.ts +1 -0
- package/src/constants.ts +14 -4
- package/src/features/encryption/interface.ts +46 -0
- package/src/features/encryption/utils.ts +22 -0
- package/src/features/rules/utils.ts +1 -11
- package/src/features/triggers/__tests__/index.test.ts +1 -0
- package/src/features/triggers/index.ts +6 -2
- package/src/features/triggers/utils.ts +4 -4
- package/src/index.ts +10 -2
- package/src/monitoring/plugin.ts +33 -0
- package/src/monitoring/routes/users.ts +8 -7
- package/src/monitoring/ui.collections.js +7 -10
- package/src/monitoring/ui.css +383 -1
- package/src/monitoring/ui.endpoints.js +5 -10
- package/src/monitoring/ui.events.js +4 -6
- package/src/monitoring/ui.functions.js +64 -71
- package/src/monitoring/ui.html +8 -0
- package/src/monitoring/ui.js +189 -0
- package/src/monitoring/ui.shared.js +239 -3
- package/src/monitoring/ui.triggers.js +2 -3
- package/src/monitoring/ui.users.js +5 -9
- package/src/monitoring/utils.ts +6 -5
- package/src/services/mongodb-atlas/index.ts +10 -13
- package/src/services/mongodb-atlas/model.ts +3 -1
- package/src/shared/handleUserDeletion.ts +2 -2
- package/src/shared/handleUserRegistration.ts +3 -3
- package/src/types/fastify-raw-body.d.ts +0 -9
- package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
- package/src/utils/__tests__/operators.test.ts +24 -0
- package/src/utils/__tests__/rule.test.ts +39 -0
- package/src/utils/__tests__/rulesMatcherInterfaces.test.ts +2 -0
- package/src/utils/index.ts +12 -1
- package/src/utils/initializer/exposeRoutes.ts +2 -2
- package/src/utils/initializer/mongodbCSFLE.ts +224 -0
- package/src/utils/initializer/registerPlugins.ts +45 -10
- package/src/utils/rules-matcher/interface.ts +5 -1
- package/src/utils/rules-matcher/utils.ts +78 -32
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ObjectId } from 'mongodb'
|
|
1
2
|
import rulesMatcherUtils from '../rules-matcher/utils'
|
|
2
3
|
|
|
3
4
|
describe('rule function', () => {
|
|
@@ -46,4 +47,42 @@ describe('rule function', () => {
|
|
|
46
47
|
rulesMatcherUtils.rule(missingOperatorBlock, mockData, mockOptions)
|
|
47
48
|
}).toThrow('Error missing operator:$notFoundOperator')
|
|
48
49
|
})
|
|
50
|
+
|
|
51
|
+
it('should support %stringToOid with $ref values', () => {
|
|
52
|
+
const companyId = new ObjectId()
|
|
53
|
+
const data = {
|
|
54
|
+
user: {
|
|
55
|
+
_id: companyId
|
|
56
|
+
},
|
|
57
|
+
auth: {
|
|
58
|
+
company: companyId.toHexString()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = rulesMatcherUtils.rule({ _id: { '%stringToOid': '$ref:auth.company' } }, data, {
|
|
63
|
+
prefix: 'user'
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(result.valid).toBe(true)
|
|
67
|
+
expect(result.name).toBe('user._id___%stringToOid')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should support %oidToString with $ref values', () => {
|
|
71
|
+
const authId = new ObjectId()
|
|
72
|
+
const data = {
|
|
73
|
+
user: {
|
|
74
|
+
authId: authId.toHexString()
|
|
75
|
+
},
|
|
76
|
+
auth: {
|
|
77
|
+
id: authId
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = rulesMatcherUtils.rule({ authId: { '%oidToString': '$ref:auth.id' } }, data, {
|
|
82
|
+
prefix: 'user'
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(result.valid).toBe(true)
|
|
86
|
+
expect(result.name).toBe('user.authId___%oidToString')
|
|
87
|
+
})
|
|
49
88
|
})
|
|
@@ -22,6 +22,8 @@ describe('Enums and Types', () => {
|
|
|
22
22
|
expect(RulesOperators.$nin).toBe('$nin')
|
|
23
23
|
expect(RulesOperators.$all).toBe('$all')
|
|
24
24
|
expect(RulesOperators.$regex).toBe('$regex')
|
|
25
|
+
expect(RulesOperators['%stringToOid']).toBe('%stringToOid')
|
|
26
|
+
expect(RulesOperators['%oidToString']).toBe('%oidToString')
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
it('should validate RulesOperatorsInArray type', () => {
|
package/src/utils/index.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
2
3
|
|
|
3
4
|
export const readFileContent = (filePath: string) => fs.readFileSync(filePath, 'utf-8')
|
|
4
5
|
export const readJsonContent = (filePath: string) =>
|
|
5
6
|
JSON.parse(readFileContent(filePath)) as unknown
|
|
7
|
+
|
|
8
|
+
export const recursivelyCollectFiles = (dir: string): string[] => {
|
|
9
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
10
|
+
const fullPath = path.join(dir, entry.name)
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
return recursivelyCollectFiles(fullPath)
|
|
13
|
+
}
|
|
14
|
+
return entry.isFile() ? [fullPath] : []
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -2,7 +2,7 @@ import { uptime } from 'node:process'
|
|
|
2
2
|
import { FastifyInstance } from 'fastify'
|
|
3
3
|
import { RegistrationDto } from '../../auth/providers/local-userpass/dtos'
|
|
4
4
|
import { AUTH_ENDPOINTS, REGISTRATION_SCHEMA } from '../../auth/utils'
|
|
5
|
-
import { API_VERSION, AUTH_CONFIG,
|
|
5
|
+
import { API_VERSION, AUTH_CONFIG, AUTH_DB_NAME, DEFAULT_CONFIG } from '../../constants'
|
|
6
6
|
import { PROVIDER } from '../../shared/models/handleUserRegistration.model'
|
|
7
7
|
import { hashPassword } from '../crypto'
|
|
8
8
|
|
|
@@ -46,7 +46,7 @@ export const exposeRoutes = async (fastify: FastifyInstance) => {
|
|
|
46
46
|
schema: REGISTRATION_SCHEMA
|
|
47
47
|
}, async function (req, res) {
|
|
48
48
|
const { authCollection } = AUTH_CONFIG
|
|
49
|
-
const db = fastify.mongo.client.db(
|
|
49
|
+
const db = fastify.mongo.client.db(AUTH_DB_NAME)
|
|
50
50
|
const { email, password } = req.body
|
|
51
51
|
const hashedPassword = await hashPassword(password)
|
|
52
52
|
const now = new Date()
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientEncryption,
|
|
3
|
+
MongoClient,
|
|
4
|
+
UUID,
|
|
5
|
+
Binary,
|
|
6
|
+
type Document,
|
|
7
|
+
type AWSEncryptionKeyOptions,
|
|
8
|
+
type AWSKMSProviderConfiguration,
|
|
9
|
+
type AzureEncryptionKeyOptions,
|
|
10
|
+
type AzureKMSProviderConfiguration,
|
|
11
|
+
type GCPKMSProviderConfiguration,
|
|
12
|
+
type GCPEncryptionKeyOptions,
|
|
13
|
+
type KMIPKMSProviderConfiguration,
|
|
14
|
+
type KMIPEncryptionKeyOptions,
|
|
15
|
+
type LocalKMSProviderConfiguration,
|
|
16
|
+
type AutoEncryptionExtraOptions,
|
|
17
|
+
type KMSProviders,
|
|
18
|
+
type AutoEncryptionOptions,
|
|
19
|
+
} from "mongodb";
|
|
20
|
+
import { EncryptionSchemaProperty, MappedEncryptionSchema, MappedEncryptionSchemaProperty, type EncryptionSchemas } from "../../features/encryption/interface";
|
|
21
|
+
import { DEFAULT_CONFIG } from "../../constants";
|
|
22
|
+
|
|
23
|
+
type KMSProviderConfig =
|
|
24
|
+
| {
|
|
25
|
+
/**
|
|
26
|
+
* The alias of the key. It must be referenced in the schema map
|
|
27
|
+
* to select which key to use for encryption.
|
|
28
|
+
*/
|
|
29
|
+
keyAlias: string
|
|
30
|
+
/**
|
|
31
|
+
* KMS Provider name.
|
|
32
|
+
*/
|
|
33
|
+
provider: "aws"
|
|
34
|
+
/**
|
|
35
|
+
* KMS Provider specific authorization configuration.
|
|
36
|
+
*/
|
|
37
|
+
config: AWSKMSProviderConfiguration
|
|
38
|
+
/**
|
|
39
|
+
* Configuration of the master key.
|
|
40
|
+
*/
|
|
41
|
+
masterKey: AWSEncryptionKeyOptions
|
|
42
|
+
} | {
|
|
43
|
+
keyAlias: string
|
|
44
|
+
provider: "azure"
|
|
45
|
+
config: AzureKMSProviderConfiguration,
|
|
46
|
+
masterKey: AzureEncryptionKeyOptions
|
|
47
|
+
} | {
|
|
48
|
+
keyAlias: string
|
|
49
|
+
provider: "gcp"
|
|
50
|
+
config: GCPKMSProviderConfiguration,
|
|
51
|
+
masterKey: GCPEncryptionKeyOptions
|
|
52
|
+
} | {
|
|
53
|
+
keyAlias: string
|
|
54
|
+
provider: "kmip"
|
|
55
|
+
config: KMIPKMSProviderConfiguration,
|
|
56
|
+
masterKey: KMIPEncryptionKeyOptions
|
|
57
|
+
} | {
|
|
58
|
+
keyAlias: string
|
|
59
|
+
provider: "local"
|
|
60
|
+
config: LocalKMSProviderConfiguration,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type MongoDbEncryptionConfig = {
|
|
64
|
+
kmsProviders: KMSProviderConfig[],
|
|
65
|
+
/**
|
|
66
|
+
* The Key Vault database name
|
|
67
|
+
* @default encryption
|
|
68
|
+
*/
|
|
69
|
+
keyVaultDb?: string
|
|
70
|
+
/**
|
|
71
|
+
* The Key Vault database collection
|
|
72
|
+
* @default __keyVault
|
|
73
|
+
*/
|
|
74
|
+
keyVaultCollection?: string
|
|
75
|
+
extraOptions?: AutoEncryptionExtraOptions
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
type RequiredConfig = Required<Omit<MongoDbEncryptionConfig, "extraOptions">>
|
|
82
|
+
|
|
83
|
+
type DataKey = { dataKeyId: UUID, dataKeyAlias: string }
|
|
84
|
+
|
|
85
|
+
async function ensureUniqueKeyAltNameIndex(db: ReturnType<MongoClient['db']>, config: RequiredConfig): Promise<void> {
|
|
86
|
+
await db.collection(config.keyVaultCollection).createIndex(
|
|
87
|
+
{ keyAltNames: 1 },
|
|
88
|
+
{
|
|
89
|
+
unique: true,
|
|
90
|
+
partialFilterExpression: { keyAltNames: { $exists: true } },
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Ensure provided KMS Providers DEK keys exist in the key vault. If not, they are created.
|
|
97
|
+
*/
|
|
98
|
+
async function ensureDataEncryptionKeys(
|
|
99
|
+
clientEncryption: ClientEncryption,
|
|
100
|
+
keyVaultDb: ReturnType<MongoClient['db']>,
|
|
101
|
+
config: RequiredConfig
|
|
102
|
+
): Promise<DataKey[]> {
|
|
103
|
+
const keys: DataKey[] = []
|
|
104
|
+
|
|
105
|
+
for (const kmsProvider of config.kmsProviders) {
|
|
106
|
+
const existingKey = await keyVaultDb.collection(config.keyVaultCollection).findOne({
|
|
107
|
+
keyAltNames: kmsProvider.keyAlias,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (existingKey?._id instanceof Binary) {
|
|
111
|
+
keys.push({ dataKeyId: existingKey._id, dataKeyAlias: kmsProvider.keyAlias })
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const dataKeyId = await clientEncryption.createDataKey(kmsProvider.provider, {
|
|
116
|
+
masterKey: "masterKey" in kmsProvider ? kmsProvider.masterKey : undefined,
|
|
117
|
+
keyAltNames: [kmsProvider.keyAlias],
|
|
118
|
+
});
|
|
119
|
+
console.log(`[MongoDB Encryption] Created new key with alias ${kmsProvider.keyAlias}`)
|
|
120
|
+
keys.push({ dataKeyId, dataKeyAlias: kmsProvider.keyAlias })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return keys
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Recursively resolve key aliases in an encryption schema to their corresponding key IDs.
|
|
128
|
+
*/
|
|
129
|
+
const resolveKeyAliases = (schema: EncryptionSchemaProperty, dataKeys: DataKey[]): MappedEncryptionSchemaProperty => {
|
|
130
|
+
if ("encrypt" in schema) {
|
|
131
|
+
if (!schema.encrypt.keyAlias) {
|
|
132
|
+
return schema
|
|
133
|
+
}
|
|
134
|
+
const keyId = dataKeys.find(k => k.dataKeyAlias === schema.encrypt.keyAlias)?.dataKeyId
|
|
135
|
+
if (!keyId) {
|
|
136
|
+
throw new Error(`Key with alias ${schema.encrypt.keyAlias} could not be found in the Key Vault.`)
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
encrypt: {
|
|
140
|
+
bsonType: schema.encrypt.bsonType,
|
|
141
|
+
algorithm: schema.encrypt.algorithm,
|
|
142
|
+
keyId: [keyId]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const mappedSchema: MappedEncryptionSchema = {
|
|
147
|
+
bsonType: "object",
|
|
148
|
+
properties: Object.entries(schema.properties).reduce((acc, [property, config]) => {
|
|
149
|
+
acc[property] = resolveKeyAliases(config, dataKeys)
|
|
150
|
+
return acc
|
|
151
|
+
}, {} as Record<string, MappedEncryptionSchemaProperty>)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (schema.encryptMetadata) {
|
|
155
|
+
const keyId = dataKeys.find(k => k.dataKeyAlias === schema.encryptMetadata!.keyAlias)?.dataKeyId
|
|
156
|
+
if (!keyId) {
|
|
157
|
+
throw new Error(`Key with alias ${schema.encryptMetadata.keyAlias} could not be found in the Key Vault.`)
|
|
158
|
+
}
|
|
159
|
+
mappedSchema.encryptMetadata = { keyId: [keyId] }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return mappedSchema
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const buildSchemaMap = (schemas: EncryptionSchemas, dataKeys: DataKey[]) => {
|
|
166
|
+
return Object.entries(schemas).reduce((acc, [key, schema]) => {
|
|
167
|
+
acc[key] = resolveKeyAliases(schema, dataKeys)
|
|
168
|
+
return acc
|
|
169
|
+
}, {} as Record<string, Document>)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Setup MongoDB Client-Side Field Level Encryption (CSFLE).
|
|
174
|
+
* @see https://www.mongodb.com/docs/manual/core/csfle
|
|
175
|
+
*/
|
|
176
|
+
export const setupMongoDbCSFLE = async (
|
|
177
|
+
config: MongoDbEncryptionConfig & { mongodbUrl: string; schemas?: EncryptionSchemas }
|
|
178
|
+
): Promise<AutoEncryptionOptions> => {
|
|
179
|
+
if (config.kmsProviders.length === 0) {
|
|
180
|
+
throw new Error('At least one KMS Provider is required when using MongoDB encryption')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const requiredConfig: RequiredConfig = {
|
|
184
|
+
kmsProviders: config.kmsProviders,
|
|
185
|
+
keyVaultDb: config.keyVaultDb ?? DEFAULT_CONFIG.MONGODB_ENCRYPTION_CONFIG.keyVaultDb,
|
|
186
|
+
keyVaultCollection: config.keyVaultDb ?? DEFAULT_CONFIG.MONGODB_ENCRYPTION_CONFIG.keyVaultCollection,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const kmsProviders = requiredConfig.kmsProviders.reduce(
|
|
190
|
+
(acc, { provider, config }) => ({ ...acc, [provider]: config }),
|
|
191
|
+
{} as KMSProviders
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const keyVaultNamespace = `${requiredConfig.keyVaultDb}.${requiredConfig.keyVaultCollection}`
|
|
195
|
+
const keyVaultClient = new MongoClient(config.mongodbUrl, {
|
|
196
|
+
maxPoolSize: 1,
|
|
197
|
+
autoEncryption: {
|
|
198
|
+
keyVaultNamespace,
|
|
199
|
+
kmsProviders,
|
|
200
|
+
extraOptions: config.extraOptions
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await keyVaultClient.connect();
|
|
205
|
+
|
|
206
|
+
const keyVaultDb = keyVaultClient.db(requiredConfig.keyVaultDb);
|
|
207
|
+
await ensureUniqueKeyAltNameIndex(keyVaultDb, requiredConfig)
|
|
208
|
+
|
|
209
|
+
const clientEncryption = new ClientEncryption(keyVaultClient, {
|
|
210
|
+
keyVaultNamespace,
|
|
211
|
+
kmsProviders,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const dataKeys = await ensureDataEncryptionKeys(clientEncryption, keyVaultDb, requiredConfig)
|
|
215
|
+
|
|
216
|
+
await keyVaultClient.close()
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
keyVaultNamespace,
|
|
220
|
+
kmsProviders,
|
|
221
|
+
schemaMap: config.schemas ? buildSchemaMap(config.schemas, dataKeys) : undefined,
|
|
222
|
+
extraOptions: config.extraOptions
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import cors from '@fastify/cors'
|
|
2
|
-
import fastifyMongodb from '@fastify/mongodb'
|
|
2
|
+
import fastifyMongodb, { FastifyMongodbOptions } from '@fastify/mongodb'
|
|
3
3
|
import { FastifyInstance } from 'fastify'
|
|
4
4
|
import fastifyRawBody from 'fastify-raw-body'
|
|
5
5
|
import { CorsConfig } from '../../'
|
|
@@ -10,7 +10,9 @@ import { customFunctionController } from '../../auth/providers/custom-function/c
|
|
|
10
10
|
import { localUserPassController } from '../../auth/providers/local-userpass/controller'
|
|
11
11
|
import { API_VERSION, DEFAULT_CONFIG } from '../../constants'
|
|
12
12
|
import { Functions } from '../../features/functions/interface'
|
|
13
|
+
import { EncryptionSchemas } from '../../features/encryption/interface'
|
|
13
14
|
import monitoringPlugin from '../../monitoring/plugin'
|
|
15
|
+
import { setupMongoDbCSFLE, MongoDbEncryptionConfig } from './mongodbCSFLE'
|
|
14
16
|
|
|
15
17
|
type RegisterFunction = FastifyInstance['register']
|
|
16
18
|
type RegisterParameters = Parameters<RegisterFunction>
|
|
@@ -21,6 +23,8 @@ type RegisterPluginsParams = {
|
|
|
21
23
|
jwtSecret: string
|
|
22
24
|
functionsList: Functions
|
|
23
25
|
corsConfig?: CorsConfig
|
|
26
|
+
encryptionSchemas?: EncryptionSchemas
|
|
27
|
+
mongodbEncryptionConfig?: MongoDbEncryptionConfig
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
type RegisterConfig = {
|
|
@@ -41,14 +45,18 @@ export const registerPlugins = async ({
|
|
|
41
45
|
mongodbUrl,
|
|
42
46
|
jwtSecret,
|
|
43
47
|
functionsList,
|
|
44
|
-
corsConfig
|
|
48
|
+
corsConfig,
|
|
49
|
+
mongodbEncryptionConfig,
|
|
50
|
+
encryptionSchemas
|
|
45
51
|
}: RegisterPluginsParams) => {
|
|
46
52
|
try {
|
|
47
53
|
const registersConfig = await getRegisterConfig({
|
|
48
54
|
mongodbUrl,
|
|
49
55
|
jwtSecret,
|
|
50
56
|
corsConfig,
|
|
51
|
-
functionsList
|
|
57
|
+
functionsList,
|
|
58
|
+
mongodbEncryptionConfig,
|
|
59
|
+
encryptionSchemas
|
|
52
60
|
})
|
|
53
61
|
|
|
54
62
|
registersConfig.forEach(({ plugin, options, pluginName }) => {
|
|
@@ -75,15 +83,23 @@ export const registerPlugins = async ({
|
|
|
75
83
|
const getRegisterConfig = async ({
|
|
76
84
|
mongodbUrl,
|
|
77
85
|
jwtSecret,
|
|
78
|
-
corsConfig
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
> => {
|
|
86
|
+
corsConfig,
|
|
87
|
+
encryptionSchemas,
|
|
88
|
+
mongodbEncryptionConfig,
|
|
89
|
+
}: Omit<RegisterPluginsParams, "register">): Promise<RegisterConfig[]> => {
|
|
82
90
|
const corsOptions = corsConfig ?? {
|
|
83
91
|
origin: '*',
|
|
84
92
|
methods: ['POST', 'GET']
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
const autoEncryption = mongodbEncryptionConfig
|
|
96
|
+
? await setupMongoDbCSFLE({
|
|
97
|
+
mongodbUrl,
|
|
98
|
+
schemas: encryptionSchemas,
|
|
99
|
+
...mongodbEncryptionConfig
|
|
100
|
+
})
|
|
101
|
+
: undefined
|
|
102
|
+
|
|
87
103
|
const baseConfig = [
|
|
88
104
|
{
|
|
89
105
|
pluginName: 'cors',
|
|
@@ -94,9 +110,28 @@ const getRegisterConfig = async ({
|
|
|
94
110
|
pluginName: 'fastifyMongodb',
|
|
95
111
|
plugin: fastifyMongodb,
|
|
96
112
|
options: {
|
|
113
|
+
url: mongodbUrl,
|
|
97
114
|
forceClose: true,
|
|
98
|
-
|
|
99
|
-
}
|
|
115
|
+
autoEncryption
|
|
116
|
+
} satisfies FastifyMongodbOptions
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* When auto-encryption is active, add another MongoDB client with bypass for change streams.
|
|
120
|
+
* The $changeStream operator does not support automatic encryption, only decryption.
|
|
121
|
+
* @see https://www.mongodb.com/docs/manual/core/csfle/reference/supported-operations
|
|
122
|
+
*/
|
|
123
|
+
autoEncryption && {
|
|
124
|
+
pluginName: 'fastifyMongodb',
|
|
125
|
+
plugin: fastifyMongodb,
|
|
126
|
+
options: {
|
|
127
|
+
name: "changestream",
|
|
128
|
+
url: mongodbUrl,
|
|
129
|
+
forceClose: true,
|
|
130
|
+
autoEncryption: {
|
|
131
|
+
...autoEncryption,
|
|
132
|
+
bypassAutoEncryption: true
|
|
133
|
+
}
|
|
134
|
+
} satisfies FastifyMongodbOptions
|
|
100
135
|
},
|
|
101
136
|
{
|
|
102
137
|
pluginName: 'jwtAuthPlugin',
|
|
@@ -153,5 +188,5 @@ const getRegisterConfig = async ({
|
|
|
153
188
|
} as RegisterConfig)
|
|
154
189
|
}
|
|
155
190
|
|
|
156
|
-
return baseConfig
|
|
191
|
+
return baseConfig.filter(Boolean)
|
|
157
192
|
}
|
|
@@ -331,6 +331,8 @@ export type Operators = {
|
|
|
331
331
|
* @returns
|
|
332
332
|
*/
|
|
333
333
|
$regex: OperatorsFunction
|
|
334
|
+
'%stringToOid': OperatorsFunction
|
|
335
|
+
'%oidToString': OperatorsFunction
|
|
334
336
|
}
|
|
335
337
|
|
|
336
338
|
export enum RulesOperators {
|
|
@@ -349,7 +351,9 @@ export enum RulesOperators {
|
|
|
349
351
|
$nin = '$nin',
|
|
350
352
|
$all = '$all',
|
|
351
353
|
$size = '$size',
|
|
352
|
-
$regex = '$regex'
|
|
354
|
+
$regex = '$regex',
|
|
355
|
+
'%stringToOid' = '%stringToOid',
|
|
356
|
+
'%oidToString' = '%oidToString'
|
|
353
357
|
}
|
|
354
358
|
|
|
355
359
|
export type RulesOperatorsInArray<T> = Partial<{
|
|
@@ -1,9 +1,59 @@
|
|
|
1
|
+
import { ObjectId } from 'bson'
|
|
1
2
|
import _get from 'lodash/get'
|
|
2
|
-
import _intersection from 'lodash/intersection'
|
|
3
3
|
import _trimStart from 'lodash/trimStart'
|
|
4
4
|
import { Operators, RulesMatcherUtils, RulesObject } from './interface'
|
|
5
5
|
|
|
6
6
|
const EMPTY_STRING_REGEXP = /^\s*$/
|
|
7
|
+
const HEX_24_REGEXP = /^[a-fA-F0-9]{24}$/
|
|
8
|
+
|
|
9
|
+
const toObjectIdHex = (value: unknown): string | null => {
|
|
10
|
+
if (value instanceof ObjectId) {
|
|
11
|
+
return value.toHexString()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
if (!HEX_24_REGEXP.test(value) || !ObjectId.isValid(value)) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
return new ObjectId(value).toHexString()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!value || typeof value !== 'object') {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const maybeObjectId = value as { _bsontype?: string; toHexString?: () => string }
|
|
26
|
+
if (maybeObjectId._bsontype === 'ObjectId' && typeof maybeObjectId.toHexString === 'function') {
|
|
27
|
+
const hex = maybeObjectId.toHexString()
|
|
28
|
+
return HEX_24_REGEXP.test(hex) ? hex.toLowerCase() : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const areSemanticallyEqual = (left: unknown, right: unknown): boolean => {
|
|
35
|
+
const leftOid = toObjectIdHex(left)
|
|
36
|
+
const rightOid = toObjectIdHex(right)
|
|
37
|
+
|
|
38
|
+
if (leftOid || rightOid) {
|
|
39
|
+
return leftOid !== null && rightOid !== null && leftOid === rightOid
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return left === right
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const includesWithSemanticEquality = (value: unknown, candidate: unknown): boolean =>
|
|
46
|
+
rulesMatcherUtils
|
|
47
|
+
.forceArray(candidate)
|
|
48
|
+
.some((item) =>
|
|
49
|
+
rulesMatcherUtils
|
|
50
|
+
.forceArray(value)
|
|
51
|
+
.some((sourceItem) =>
|
|
52
|
+
rulesMatcherUtils.forceArray(item).some((candidateItem) =>
|
|
53
|
+
areSemanticallyEqual(sourceItem, candidateItem)
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
)
|
|
7
57
|
|
|
8
58
|
/**
|
|
9
59
|
* Defines a utility object named rulesMatcherUtils, which contains various helper functions used for processing rules and data in a rule-matching context.
|
|
@@ -24,10 +74,10 @@ const rulesMatcherUtils: RulesMatcherUtils = {
|
|
|
24
74
|
const valueRef =
|
|
25
75
|
value && String(value).indexOf('$ref:') === 0
|
|
26
76
|
? _get(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
77
|
+
data,
|
|
78
|
+
rulesMatcherUtils.getPath(value.replace('$ref:', ''), prefix),
|
|
79
|
+
undefined
|
|
80
|
+
)
|
|
31
81
|
: value
|
|
32
82
|
|
|
33
83
|
if (!operators[op]) {
|
|
@@ -230,9 +280,9 @@ const rulesMatcherUtils: RulesMatcherUtils = {
|
|
|
230
280
|
export const operators: Operators = {
|
|
231
281
|
$exists: (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
|
|
232
282
|
|
|
233
|
-
$eq: (a, b) => a
|
|
283
|
+
$eq: (a, b) => areSemanticallyEqual(a, b),
|
|
234
284
|
|
|
235
|
-
$ne: (a, b) => a
|
|
285
|
+
$ne: (a, b) => !areSemanticallyEqual(a, b),
|
|
236
286
|
|
|
237
287
|
$gt: (a, b) => rulesMatcherUtils.forceNumber(a) > parseFloat(b),
|
|
238
288
|
|
|
@@ -250,39 +300,35 @@ export const operators: Operators = {
|
|
|
250
300
|
|
|
251
301
|
$strLte: (a, b) => String(a || '').length <= parseFloat(b),
|
|
252
302
|
|
|
253
|
-
$in: (a, b) =>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
.some(
|
|
257
|
-
(c) =>
|
|
258
|
-
_intersection(rulesMatcherUtils.forceArray(a), rulesMatcherUtils.forceArray(c))
|
|
259
|
-
.length
|
|
260
|
-
),
|
|
261
|
-
|
|
262
|
-
$nin: (a, b) =>
|
|
263
|
-
!rulesMatcherUtils
|
|
264
|
-
.forceArray(b)
|
|
265
|
-
.some(
|
|
266
|
-
(c) =>
|
|
267
|
-
_intersection(rulesMatcherUtils.forceArray(a), rulesMatcherUtils.forceArray(c))
|
|
268
|
-
.length
|
|
269
|
-
),
|
|
303
|
+
$in: (a, b) => includesWithSemanticEquality(a, b),
|
|
304
|
+
|
|
305
|
+
$nin: (a, b) => !includesWithSemanticEquality(a, b),
|
|
270
306
|
|
|
271
307
|
$all: (a, b) =>
|
|
272
|
-
rulesMatcherUtils
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
308
|
+
rulesMatcherUtils.forceArray(b).every((candidate) =>
|
|
309
|
+
rulesMatcherUtils
|
|
310
|
+
.forceArray(a)
|
|
311
|
+
.some((value) =>
|
|
312
|
+
rulesMatcherUtils.forceArray(candidate).some((item) => areSemanticallyEqual(value, item))
|
|
313
|
+
)
|
|
314
|
+
),
|
|
279
315
|
|
|
280
316
|
$size: (a, b) => Array.isArray(a) && a.length === parseFloat(b),
|
|
281
317
|
|
|
282
318
|
$regex: (a, b, opt) =>
|
|
283
319
|
rulesMatcherUtils
|
|
284
320
|
.forceArray(b)
|
|
285
|
-
.some((c) => (c instanceof RegExp ? c.test(a) : new RegExp(c, opt).test(a)))
|
|
321
|
+
.some((c) => (c instanceof RegExp ? c.test(a) : new RegExp(c, opt).test(a))),
|
|
322
|
+
|
|
323
|
+
'%stringToOid': (a, b) => {
|
|
324
|
+
const converted = toObjectIdHex(b)
|
|
325
|
+
return converted !== null && areSemanticallyEqual(a, converted)
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
'%oidToString': (a, b) => {
|
|
329
|
+
const converted = toObjectIdHex(b)
|
|
330
|
+
return converted !== null && areSemanticallyEqual(a, converted)
|
|
331
|
+
}
|
|
286
332
|
}
|
|
287
333
|
|
|
288
334
|
// export default operators
|