Skip to content

Commit b8d64a3

Browse files
authored
Merge pull request #441 from impresso/develop
Release v3.0.3
2 parents 89a53b7 + bbebf2c commit b8d64a3

File tree

17 files changed

+353
-21
lines changed

17 files changed

+353
-21
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@
2121
},
2222
"[json]": {
2323
"editor.formatOnSave": true
24-
}
24+
},
25+
"mochaExplorer.files": "test/**/*.test.{ts,js}"
2526
}

package-lock.json

Lines changed: 48 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"watch": "tsc -p ./tsconfig.json -w & tscp -w",
2626
"build": "tsc -p ./tsconfig.json",
2727
"copy-files": "tscp",
28-
"test": "mocha 'test/**/*.test.js'",
29-
"test-watch": "mocha 'test/**/*.test.js' --watch",
28+
"test": "mocha --require ts-node/register 'test/**/*.test.{js,ts}'",
29+
"test-watch": "mocha --require ts-node/register --watch 'test/**/*.test.{js,ts}'",
3030
"integration-test": "NODE_ENV=test mocha --config ./.mocharc-integration.json 'test/integration/**/*.test.js'",
3131
"lintfix": "eslint src/. --config .eslintrc.js --fix",
3232
"lint": "eslint src/. --config .eslintrc.js",
@@ -92,6 +92,7 @@
9292
"http-proxy-middleware": "^2.0.1",
9393
"impresso-jscommons": "https://github.com/impresso/impresso-jscommons/tarball/v1.4.3",
9494
"json2csv": "^4.3.3",
95+
"jsonpath-plus": "^10.0.1",
9596
"jsonschema": "^1.4.1",
9697
"lodash": "^4.17.21",
9798
"lodash.first": "^3.0.0",
@@ -123,7 +124,7 @@
123124
"wikidata-sdk": "^5.15.10",
124125
"winston": "3.13.0",
125126
"xml2js": "^0.6.2",
126-
"yaml": "^2.1.1",
127+
"yaml": "^2.6.0",
127128
"undici": "6.19.8"
128129
},
129130
"devDependencies": {

src/authentication.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface SlimUser {
5959
uid: string
6060
id: number
6161
isStaff: boolean
62+
groups: string[]
6263
}
6364

6465
/**
@@ -91,6 +92,7 @@ class NoDBJWTStrategy extends JWTStrategy {
9192
uid: payload.userId,
9293
id: parseInt(payload.sub),
9394
isStaff: payload.isStaff ?? false,
95+
groups: payload.userGroups ?? [],
9496
}
9597
return {
9698
...result,

src/hooks/redaction.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { HookContext, HookFunction } from '@feathersjs/feathers'
2+
import { FindResponse } from '../models/common'
3+
import { ImpressoApplication } from '../types'
4+
import { Redactable, RedactionPolicy, redactObject } from '../util/redaction'
5+
import { SlimUser } from '../authentication'
6+
7+
export type RedactCondition = (context: HookContext<ImpressoApplication>) => boolean
8+
9+
/**
10+
* Redact the response object using the provided redaction policy.
11+
* If the condition is provided, the redaction will only be applied if the condition is met.
12+
*/
13+
export const redactResponse = <S>(
14+
policy: RedactionPolicy,
15+
condition?: (context: HookContext<ImpressoApplication>) => boolean
16+
): HookFunction<ImpressoApplication, S> => {
17+
return context => {
18+
if (context.type != 'after') throw new Error('The redactResponse hook should be used as an after hook only')
19+
20+
if (condition != null && !condition(context)) return context
21+
22+
if (context.result != null) {
23+
context.result = redactObject(context.result, policy)
24+
}
25+
return context
26+
}
27+
}
28+
29+
/**
30+
* Redact the response object using the provided redaction policy.
31+
* Assumes that the response is a FindResponse object (has a `data` field with
32+
* an array of objects).
33+
* If the condition is provided, the redaction will only be applied if the condition is met.
34+
*/
35+
export const redactResponseDataItem = <S>(
36+
policy: RedactionPolicy,
37+
condition?: (context: HookContext<ImpressoApplication>) => boolean,
38+
dataItemsField?: string
39+
): HookFunction<ImpressoApplication, S> => {
40+
return context => {
41+
if (context.type != 'after') throw new Error('The redactResponseDataItem hook should be used as an after hook only')
42+
43+
if (condition != null && !condition(context)) return context
44+
45+
if (context.result != null) {
46+
if (dataItemsField != null) {
47+
const result = context.result as Record<string, any>
48+
result[dataItemsField] = result[dataItemsField].map((item: Redactable) => redactObject(item, policy))
49+
} else {
50+
const result = context.result as any as FindResponse<Redactable>
51+
result.data = result.data.map(item => redactObject(item, policy))
52+
}
53+
}
54+
return context
55+
}
56+
}
57+
58+
/**
59+
* Below are conditions that can be used in the redactResponse hook.
60+
*/
61+
export const inPublicApi: RedactCondition = context => {
62+
return context.app.get('isPublicApi') == true
63+
}
64+
65+
/**
66+
* Condition is:
67+
* - user is not authenticated
68+
* - OR user is authenticated and is not in the specified group
69+
*/
70+
export const notInGroup =
71+
(groupName: string): RedactCondition =>
72+
context => {
73+
const user = context.params?.user as any as SlimUser
74+
return user == null || !user.groups.includes(groupName)
75+
}
76+
77+
const NoRedactionGroup = 'NoRedaction'
78+
79+
/**
80+
* Default condition we should currently use:
81+
* - running as Public API
82+
* - AND user is not in the NoRedaction group
83+
*/
84+
export const defaultCondition: RedactCondition = context => {
85+
return inPublicApi(context) && notInGroup(NoRedactionGroup)(context)
86+
}
87+
88+
export type { RedactionPolicy }
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "RedactionPolicy",
4+
"type": "object",
5+
"properties": {
6+
"name": {
7+
"type": "string"
8+
},
9+
"items": {
10+
"type": "array",
11+
"items": {
12+
"$ref": "#/definitions/RedactionPolicyItem"
13+
}
14+
}
15+
},
16+
"required": ["name", "items"],
17+
"definitions": {
18+
"RedactionPolicyItem": {
19+
"type": "object",
20+
"properties": {
21+
"jsonPath": {
22+
"type": "string"
23+
},
24+
"valueConverterName": {
25+
"type": "string",
26+
"enum": ["redact", "contextNotAllowedImage", "remove", "emptyArray"]
27+
}
28+
},
29+
"required": ["jsonPath", "valueConverterName"]
30+
}
31+
}
32+
}

src/schema/schemas/Topic.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
"w": {
4848
"type": "number",
4949
"description": "TODO"
50+
},
51+
"avg": {
52+
"type": "number",
53+
"description": "TODO"
5054
}
5155
},
5256
"required": ["uid", "w"]

src/services/articles/articles.hooks.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { rateLimit } from '../../hooks/rateLimiter'
22
import { authenticateAround as authenticate } from '../../hooks/authenticate'
3+
import { redactResponse, redactResponseDataItem, defaultCondition } from '../../hooks/redaction'
4+
import { loadYamlFile } from '../../util/yaml'
35

46
const {
57
utils,
@@ -17,6 +19,8 @@ const { resolveTopics, resolveUserAddons } = require('../../hooks/resolvers/arti
1719
const { obfuscate } = require('../../hooks/access-rights')
1820
const { SolrMappings } = require('../../data/constants')
1921

22+
const articleRedactionPolicy = loadYamlFile(`${__dirname}/resources/articleRedactionPolicy.yml`)
23+
2024
module.exports = {
2125
around: {
2226
all: [authenticate({ allowUnauthenticated: true }), rateLimit()],
@@ -90,6 +94,7 @@ module.exports = {
9094
resolveTopics(),
9195
saveResultsInCache(),
9296
obfuscate(),
97+
redactResponseDataItem(articleRedactionPolicy, defaultCondition),
9398
],
9499
get: [
95100
// save here cache, flush cache here
@@ -100,6 +105,7 @@ module.exports = {
100105
saveResultsInCache(),
101106
resolveUserAddons(),
102107
obfuscate(),
108+
redactResponse(articleRedactionPolicy, defaultCondition),
103109
],
104110
create: [],
105111
update: [],
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# yaml-language-server: $schema=../../../schema/common/redactionPolicy.json
2+
name: artice-redaction-policy
3+
items:
4+
- jsonPath: $.title
5+
valueConverterName: redact
6+
- jsonPath: $.excerpt
7+
valueConverterName: redact
8+
- jsonPath: $.content
9+
valueConverterName: redact
10+
- jsonPath: $.regions
11+
valueConverterName: emptyArray
12+
- jsonPath: $.matches
13+
valueConverterName: emptyArray
14+
- jsonPath: $.pages[*].iiif
15+
valueConverterName: contextNotAllowedImage
16+
- jsonPath: $.pages[*].iiifThumbnail
17+
valueConverterName: contextNotAllowedImage

src/services/search/search.hooks.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { authenticateAround as authenticate } from '../../hooks/authenticate'
22
import { rateLimit } from '../../hooks/rateLimiter'
3+
import { redactResponseDataItem, defaultCondition } from '../../hooks/redaction'
4+
import { loadYamlFile } from '../../util/yaml'
35

46
const { protect } = require('@feathersjs/authentication-local').hooks
57
const {
@@ -16,6 +18,8 @@ const { paramsValidator, eachFilterValidator, eachFacetFilterValidator } = requi
1618
const { SolrMappings } = require('../../data/constants')
1719
const { SolrNamespaces } = require('../../solr')
1820

21+
const articleRedactionPolicy = loadYamlFile(`${__dirname}/../articles/resources/articleRedactionPolicy.yml`)
22+
1923
module.exports = {
2024
around: {
2125
find: [authenticate({ allowUnauthenticated: true }), rateLimit()],
@@ -93,7 +97,12 @@ module.exports = {
9397

9498
after: {
9599
all: [],
96-
find: [displayQueryParams(['queryComponents', 'filters']), resolveQueryComponents(), protect('content')],
100+
find: [
101+
displayQueryParams(['queryComponents', 'filters']),
102+
resolveQueryComponents(),
103+
protect('content'),
104+
redactResponseDataItem(articleRedactionPolicy, defaultCondition),
105+
],
97106
get: [],
98107
create: [],
99108
update: [],

0 commit comments

Comments
 (0)