Skip to content

Commit

Permalink
Merge pull request #441 from impresso/develop
Browse files Browse the repository at this point in the history
Release v3.0.3
  • Loading branch information
theorm authored Oct 17, 2024
2 parents 89a53b7 + bbebf2c commit b8d64a3
Show file tree
Hide file tree
Showing 17 changed files with 353 additions and 21 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@
},
"[json]": {
"editor.formatOnSave": true
}
},
"mochaExplorer.files": "test/**/*.test.{ts,js}"
}
64 changes: 48 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
"watch": "tsc -p ./tsconfig.json -w & tscp -w",
"build": "tsc -p ./tsconfig.json",
"copy-files": "tscp",
"test": "mocha 'test/**/*.test.js'",
"test-watch": "mocha 'test/**/*.test.js' --watch",
"test": "mocha --require ts-node/register 'test/**/*.test.{js,ts}'",
"test-watch": "mocha --require ts-node/register --watch 'test/**/*.test.{js,ts}'",
"integration-test": "NODE_ENV=test mocha --config ./.mocharc-integration.json 'test/integration/**/*.test.js'",
"lintfix": "eslint src/. --config .eslintrc.js --fix",
"lint": "eslint src/. --config .eslintrc.js",
Expand Down Expand Up @@ -92,6 +92,7 @@
"http-proxy-middleware": "^2.0.1",
"impresso-jscommons": "https://github.com/impresso/impresso-jscommons/tarball/v1.4.3",
"json2csv": "^4.3.3",
"jsonpath-plus": "^10.0.1",
"jsonschema": "^1.4.1",
"lodash": "^4.17.21",
"lodash.first": "^3.0.0",
Expand Down Expand Up @@ -123,7 +124,7 @@
"wikidata-sdk": "^5.15.10",
"winston": "3.13.0",
"xml2js": "^0.6.2",
"yaml": "^2.1.1",
"yaml": "^2.6.0",
"undici": "6.19.8"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions src/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface SlimUser {
uid: string
id: number
isStaff: boolean
groups: string[]
}

/**
Expand Down Expand Up @@ -91,6 +92,7 @@ class NoDBJWTStrategy extends JWTStrategy {
uid: payload.userId,
id: parseInt(payload.sub),
isStaff: payload.isStaff ?? false,
groups: payload.userGroups ?? [],
}
return {
...result,
Expand Down
88 changes: 88 additions & 0 deletions src/hooks/redaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { HookContext, HookFunction } from '@feathersjs/feathers'
import { FindResponse } from '../models/common'
import { ImpressoApplication } from '../types'
import { Redactable, RedactionPolicy, redactObject } from '../util/redaction'
import { SlimUser } from '../authentication'

export type RedactCondition = (context: HookContext<ImpressoApplication>) => boolean

/**
* Redact the response object using the provided redaction policy.
* If the condition is provided, the redaction will only be applied if the condition is met.
*/
export const redactResponse = <S>(
policy: RedactionPolicy,
condition?: (context: HookContext<ImpressoApplication>) => boolean
): HookFunction<ImpressoApplication, S> => {
return context => {
if (context.type != 'after') throw new Error('The redactResponse hook should be used as an after hook only')

if (condition != null && !condition(context)) return context

if (context.result != null) {
context.result = redactObject(context.result, policy)
}
return context
}
}

/**
* Redact the response object using the provided redaction policy.
* Assumes that the response is a FindResponse object (has a `data` field with
* an array of objects).
* If the condition is provided, the redaction will only be applied if the condition is met.
*/
export const redactResponseDataItem = <S>(
policy: RedactionPolicy,
condition?: (context: HookContext<ImpressoApplication>) => boolean,
dataItemsField?: string
): HookFunction<ImpressoApplication, S> => {
return context => {
if (context.type != 'after') throw new Error('The redactResponseDataItem hook should be used as an after hook only')

if (condition != null && !condition(context)) return context

if (context.result != null) {
if (dataItemsField != null) {
const result = context.result as Record<string, any>
result[dataItemsField] = result[dataItemsField].map((item: Redactable) => redactObject(item, policy))
} else {
const result = context.result as any as FindResponse<Redactable>
result.data = result.data.map(item => redactObject(item, policy))
}
}
return context
}
}

/**
* Below are conditions that can be used in the redactResponse hook.
*/
export const inPublicApi: RedactCondition = context => {
return context.app.get('isPublicApi') == true
}

/**
* Condition is:
* - user is not authenticated
* - OR user is authenticated and is not in the specified group
*/
export const notInGroup =
(groupName: string): RedactCondition =>
context => {
const user = context.params?.user as any as SlimUser
return user == null || !user.groups.includes(groupName)
}

const NoRedactionGroup = 'NoRedaction'

/**
* Default condition we should currently use:
* - running as Public API
* - AND user is not in the NoRedaction group
*/
export const defaultCondition: RedactCondition = context => {
return inPublicApi(context) && notInGroup(NoRedactionGroup)(context)
}

export type { RedactionPolicy }
32 changes: 32 additions & 0 deletions src/schema/common/redactionPolicy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RedactionPolicy",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/RedactionPolicyItem"
}
}
},
"required": ["name", "items"],
"definitions": {
"RedactionPolicyItem": {
"type": "object",
"properties": {
"jsonPath": {
"type": "string"
},
"valueConverterName": {
"type": "string",
"enum": ["redact", "contextNotAllowedImage", "remove", "emptyArray"]
}
},
"required": ["jsonPath", "valueConverterName"]
}
}
}
4 changes: 4 additions & 0 deletions src/schema/schemas/Topic.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
"w": {
"type": "number",
"description": "TODO"
},
"avg": {
"type": "number",
"description": "TODO"
}
},
"required": ["uid", "w"]
Expand Down
6 changes: 6 additions & 0 deletions src/services/articles/articles.hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { rateLimit } from '../../hooks/rateLimiter'
import { authenticateAround as authenticate } from '../../hooks/authenticate'
import { redactResponse, redactResponseDataItem, defaultCondition } from '../../hooks/redaction'
import { loadYamlFile } from '../../util/yaml'

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

const articleRedactionPolicy = loadYamlFile(`${__dirname}/resources/articleRedactionPolicy.yml`)

module.exports = {
around: {
all: [authenticate({ allowUnauthenticated: true }), rateLimit()],
Expand Down Expand Up @@ -90,6 +94,7 @@ module.exports = {
resolveTopics(),
saveResultsInCache(),
obfuscate(),
redactResponseDataItem(articleRedactionPolicy, defaultCondition),
],
get: [
// save here cache, flush cache here
Expand All @@ -100,6 +105,7 @@ module.exports = {
saveResultsInCache(),
resolveUserAddons(),
obfuscate(),
redactResponse(articleRedactionPolicy, defaultCondition),
],
create: [],
update: [],
Expand Down
17 changes: 17 additions & 0 deletions src/services/articles/resources/articleRedactionPolicy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# yaml-language-server: $schema=../../../schema/common/redactionPolicy.json
name: artice-redaction-policy
items:
- jsonPath: $.title
valueConverterName: redact
- jsonPath: $.excerpt
valueConverterName: redact
- jsonPath: $.content
valueConverterName: redact
- jsonPath: $.regions
valueConverterName: emptyArray
- jsonPath: $.matches
valueConverterName: emptyArray
- jsonPath: $.pages[*].iiif
valueConverterName: contextNotAllowedImage
- jsonPath: $.pages[*].iiifThumbnail
valueConverterName: contextNotAllowedImage
11 changes: 10 additions & 1 deletion src/services/search/search.hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { authenticateAround as authenticate } from '../../hooks/authenticate'
import { rateLimit } from '../../hooks/rateLimiter'
import { redactResponseDataItem, defaultCondition } from '../../hooks/redaction'
import { loadYamlFile } from '../../util/yaml'

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

const articleRedactionPolicy = loadYamlFile(`${__dirname}/../articles/resources/articleRedactionPolicy.yml`)

module.exports = {
around: {
find: [authenticate({ allowUnauthenticated: true }), rateLimit()],
Expand Down Expand Up @@ -93,7 +97,12 @@ module.exports = {

after: {
all: [],
find: [displayQueryParams(['queryComponents', 'filters']), resolveQueryComponents(), protect('content')],
find: [
displayQueryParams(['queryComponents', 'filters']),
resolveQueryComponents(),
protect('content'),
redactResponseDataItem(articleRedactionPolicy, defaultCondition),
],
get: [],
create: [],
update: [],
Expand Down
Loading

0 comments on commit b8d64a3

Please sign in to comment.