Skip to content

Commit f1e1566

Browse files
Merge #850
850: Sanitize private fields selectively r=brunoocasali a=haZya # Pull Request ## Related issue Fixes #773 ## What does this PR do? This allows user to selectively index private fields which are by default sanitized. - Added a setting called `noSanitizePrivateFields` to the meilisearch config that allows user to define an array of private field names that need to be indexed. ## PR checklist Please check if your PR fulfills the following requirements: - [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)? - [x] Have you read the contributing guidelines? - [x] Have you made sure that the title is accurate and descriptive of the changes? Co-authored-by: haZya <[email protected]>
2 parents 54440e1 + bc1f74f commit f1e1566

File tree

5 files changed

+111
-6
lines changed

5 files changed

+111
-6
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ Settings:
211211
- [🤚 Filter entries](#-filter-entries)
212212
- [🏗 Add Meilisearch settings](#-add-meilisearch-settings)
213213
- [🔎 Entries query](#-entries-query)
214+
- [🔐 Selectively index private fields](#-selectively-index-private-fields)
214215

215216
### 🏷 Custom index name
216217

@@ -412,6 +413,27 @@ module.exports = {
412413

413414
[See resources](./resources/entries-query) for more entriesQuery examples.
414415

416+
### 🔐 Selectively index private fields
417+
418+
Private fields are sanitized by default to prevent data leaks. However, you might want to allow some of these private fields to be used for `search`, `filter` or `sort`. This is possible with the `noSanitizePrivateFields`. For example, if you have a private field called `internal_notes` in your content-type schema that you wish to include in searching, you can add it to the `noSanitizePrivateFields` array to allow it to be indexed.
419+
420+
```js
421+
// config/plugins.js
422+
423+
module.exports = {
424+
meilisearch: {
425+
config: {
426+
restaurant: {
427+
noSanitizePrivateFields: ["internal_notes"], // All attributes: ["*"]
428+
settings: {
429+
"searchableAttributes": ["internal_notes"],
430+
}
431+
},
432+
},
433+
},
434+
}
435+
```
436+
415437
### 🕵️‍♀️ Start Searching <!-- omit in toc -->
416438

417439
Once you have a content-type indexed in Meilisearch, you can [start searching](https://www.meilisearch.com/docs/learn/getting_started/quick_start.html#search).

server/__tests__/configuration-validation.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,41 @@ describe('Test plugin configuration', () => {
245245
expect(strapiMock.log.error).toHaveBeenCalledTimes(0)
246246
})
247247

248+
test('Test noSanitizePrivateFields with wrong type', async () => {
249+
validatePluginConfig({
250+
restaurant: {
251+
noSanitizePrivateFields: 0,
252+
},
253+
})
254+
expect(strapiMock.log.warn).toHaveBeenCalledTimes(0)
255+
expect(strapiMock.log.error).toHaveBeenCalledTimes(1)
256+
expect(strapiMock.log.error).toHaveBeenCalledWith(
257+
'The "noSanitizePrivateFields" option of "restaurant" should be an array of strings.'
258+
)
259+
})
260+
261+
test('Test noSanitizePrivateFields with array of strings', async () => {
262+
const configuration = validatePluginConfig({
263+
restaurant: {
264+
noSanitizePrivateFields: ['test'],
265+
},
266+
})
267+
expect(strapiMock.log.warn).toHaveBeenCalledTimes(0)
268+
expect(strapiMock.log.error).toHaveBeenCalledTimes(0)
269+
expect(configuration.restaurant.noSanitizePrivateFields).toEqual(['test'])
270+
})
271+
272+
test('Test noSanitizePrivateFields with undefined', async () => {
273+
validatePluginConfig({
274+
restaurant: {
275+
noSanitizePrivateFields: undefined,
276+
},
277+
})
278+
279+
expect(strapiMock.log.warn).toHaveBeenCalledTimes(0)
280+
expect(strapiMock.log.error).toHaveBeenCalledTimes(0)
281+
})
282+
248283
test('Test configuration with random field ', async () => {
249284
validatePluginConfig({
250285
restaurant: {

server/__tests__/meilisearch.test.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,26 @@ describe('Tests content types', () => {
119119
)
120120
})
121121

122-
test('sanitizes the private fields from the entries', async () => {
122+
test('selectively sanitizes the private fields from the entries', async () => {
123123
const pluginMock = jest.fn(() => ({
124124
// This rewrites only the needed methods to reach the system under test (removeSensitiveFields)
125125
service: jest.fn().mockImplementation(() => {
126126
return {
127127
async actionInBatches({ contentType = 'restaurant', callback }) {
128128
await callback({
129129
entries: [
130-
{ id: 1, title: 'title', secret: '123' },
131-
{ id: 2, title: 'abc', secret: '234' },
130+
{
131+
id: 1,
132+
title: 'title',
133+
internal_notes: 'note123',
134+
secret: '123',
135+
},
136+
{
137+
id: 2,
138+
title: 'abc',
139+
internal_notes: 'note234',
140+
secret: '234',
141+
},
132142
],
133143
contentType,
134144
})
@@ -149,11 +159,18 @@ describe('Tests content types', () => {
149159
attributes: {
150160
id: { private: false },
151161
title: { private: false },
162+
internal_notes: { private: true },
152163
secret: { private: true },
153164
},
154165
},
155166
},
156-
config: { get: jest.fn(() => ({ restaurant: jest.fn() })) },
167+
config: {
168+
get: jest.fn(() => ({
169+
restaurant: {
170+
noSanitizePrivateFields: ['internal_notes'],
171+
},
172+
})),
173+
},
157174
log: mockLogger,
158175
},
159176
})
@@ -169,11 +186,13 @@ describe('Tests content types', () => {
169186
_meilisearch_id: 'restaurant-1',
170187
id: 1,
171188
title: 'title',
189+
internal_notes: 'note123',
172190
},
173191
{
174192
_meilisearch_id: 'restaurant-2',
175193
id: 2,
176194
title: 'abc',
195+
internal_notes: 'note234',
177196
},
178197
],
179198
{ primaryKey: '_meilisearch_id' }

server/configuration-validation.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ function CollectionConfig({ collectionName, configuration }) {
175175
filterEntry,
176176
settings,
177177
entriesQuery,
178+
noSanitizePrivateFields,
178179
...unknownFields
179180
} = configuration
180181
const options = {}
@@ -262,6 +263,21 @@ function CollectionConfig({ collectionName, configuration }) {
262263

263264
return this
264265
},
266+
validateNoSanitizePrivateFields() {
267+
// noSanitizePrivateFields is either undefined or an array
268+
if (
269+
noSanitizePrivateFields !== undefined &&
270+
!Array.isArray(noSanitizePrivateFields)
271+
) {
272+
log.error(
273+
`The "noSanitizePrivateFields" option of "${collectionName}" should be an array of strings.`
274+
)
275+
} else if (noSanitizePrivateFields !== undefined) {
276+
options.noSanitizePrivateFields = noSanitizePrivateFields
277+
}
278+
279+
return this
280+
},
265281

266282
validateNoInvalidKeys() {
267283
// Keys that should not be present in the configuration
@@ -323,8 +339,9 @@ function PluginConfig({ configuration }) {
323339
.validateFilterEntry()
324340
.validateTransformEntry()
325341
.validateMeilisearchSettings()
326-
.validateNoInvalidKeys()
327342
.validateEntriesQuery()
343+
.validateNoSanitizePrivateFields()
344+
.validateNoInvalidKeys()
328345
.get()
329346
}
330347
}

server/services/meilisearch/config.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,22 @@ module.exports = ({ strapi }) => {
189189
* @return {Array<Object>} - Entries
190190
*/
191191
removeSensitiveFields: function ({ contentType, entries }) {
192+
const collection = contentTypeService.getCollectionName({ contentType })
193+
const contentTypeConfig = meilisearchConfig[collection] || {}
194+
195+
const noSanitizePrivateFields =
196+
contentTypeConfig.noSanitizePrivateFields || []
197+
198+
if (noSanitizePrivateFields.includes('*')) {
199+
return entries
200+
}
201+
192202
// TODO: should be persisted somewhere to make it more performant
193203
const attrs = strapi.contentTypes[contentType].attributes
194204
const privateFields = Object.entries(attrs).map(([field, schema]) =>
195-
schema.private ? field : false
205+
schema.private && !noSanitizePrivateFields.includes(field)
206+
? field
207+
: false
196208
)
197209

198210
return entries.map(entry => {

0 commit comments

Comments
 (0)