diff --git a/docker-compose.yml b/docker-compose.yml index 8d1f37b6..2f90dee9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,46 +5,46 @@ services: db: image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.3.2 ports: - - "62223:9200" + - "62222:9200" volumes: - "./esdata:/usr/share/elasticsearch/data" environment: - "path.repo=/usr/share/elasticsearch/data" - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # Prevent elasticsearch eating up too much memory - # kibana: - # image: docker.elastic.co/kibana/kibana-oss:6.3.2 - # depends_on: - # - db - # environment: - # ELASTICSEARCH_URL: http://db:9200 # Through docker network, not exposed port - # ports: - # - "6222:5601" + kibana: + image: docker.elastic.co/kibana/kibana-oss:6.3.2 + depends_on: + - db + environment: + ELASTICSEARCH_URL: http://db:9200 # Through docker network, not exposed port + ports: + - "6222:5601" - # url-resolver: - # image: cofacts/url-resolver - # ports: - # - "4000:4000" + url-resolver: + image: cofacts/url-resolver + ports: + - "4000:4000" - # api: - # image: node:18 - # container_name: rumors-api - # depends_on: - # - db - # working_dir: "/srv/www" - # entrypoint: npm run dev - # volumes: - # - ".:/srv/www" - # environment: - # ELASTICSEARCH_URL: "http://db:9200" - # URL_RESOLVER_URL: "url-resolver:4000" - # ports: - # - "6000:5000" - # - "5500:5500" + api: + image: node:18 + container_name: rumors-api + depends_on: + - db + working_dir: "/srv/www" + entrypoint: npm run dev + volumes: + - ".:/srv/www" + environment: + ELASTICSEARCH_URL: "http://db:9200" + URL_RESOLVER_URL: "url-resolver:4000" + ports: + - "6000:5000" + - "5500:5500" - # site: - # image: cofacts/rumors-site:latest-en - # ports: - # - "3000:3000" - # environment: - # PUBLIC_API_URL: http://localhost:5000 + site: + image: cofacts/rumors-site:latest-en + ports: + - "3000:3000" + environment: + PUBLIC_API_URL: http://localhost:5000 diff --git a/src/adm/handlers/moderation/__fixtures__/awardBadge.ts b/src/adm/handlers/moderation/__fixtures__/awardBadge.ts index c14bb885..ac394c60 100644 --- a/src/adm/handlers/moderation/__fixtures__/awardBadge.ts +++ b/src/adm/handlers/moderation/__fixtures__/awardBadge.ts @@ -1,4 +1,5 @@ import type { User } from 'rumors-db/schema/users'; +import type { Badge } from 'rumors-db/schema/badges'; export default { '/users/doc/user-to-award-badge': { @@ -15,11 +16,23 @@ export default { badges: [ { badgeId: 'test-certification-001', - badgeMetaData: '{"from":"some-orgnization}', + badgeMetaData: '{"from":"some-orgnization"}', isDisplayed: false, createdAt: '2020-01-01T00:00:00.000Z', updatedAt: '2020-01-01T00:00:00.000Z', }, ], } satisfies User, + + '/badges/doc/test-certification-001': { + name: 'Test Certification', + displayName: 'Test Certification', + description: 'A test certification badge', + link: 'https://badge.source.com', + icon: 'https://badge.source.com/icon.png', + borderImage: 'https://badge.source.com/border.png', + issuers: ['authorized-issuer@test.com', 'service-token-123'], + createdAt: '2020-01-01T00:00:00.000Z', + updatedAt: '2020-01-01T00:00:00.000Z', + } satisfies Badge, }; diff --git a/src/adm/handlers/moderation/__tests__/awardBadge.js b/src/adm/handlers/moderation/__tests__/awardBadge.js index 6e5e875d..ba2a7dae 100644 --- a/src/adm/handlers/moderation/__tests__/awardBadge.js +++ b/src/adm/handlers/moderation/__tests__/awardBadge.js @@ -7,68 +7,104 @@ import fixtures from '../__fixtures__/awardBadge'; const FIXED_DATE = 612921600000; -beforeEach(() => loadFixtures(fixtures)); -afterEach(() => unloadFixtures(fixtures)); - -it('fails if userId is not valid', async () => { - await expect( - awardBadge({ - userId: 'not-exist', - badgeId: 'badge id', - badgeMetaData: '{}', - }) - ).rejects.toMatchInlineSnapshot( - `[HTTPError: User with ID=not-exist does not exist]` - ); +beforeEach(async () => { + await loadFixtures(fixtures); + MockDate.set(FIXED_DATE); }); -/** - * Asserts the document in database is the same as in the fixture, - * i.e. the document is not modified - * - * @param {string} fixtureKey - * @param {{index: string; id: string;}} clientGetArgs - Arguments for client.get() - */ +afterEach(async () => { + await unloadFixtures(fixtures); + MockDate.reset(); +}); -it('correctly sets the awarded badge id and updates status of their works', async () => { - MockDate.set(FIXED_DATE); - const result = await awardBadge({ - userId: 'user-to-award-badge', - badgeId: 'test-certification-001', - badgeMetaData: '{"from":"some-orgnization}', +describe('awardBadge', () => { + it('fails if userId is not valid', async () => { + await expect( + awardBadge({ + userId: 'not-exist', + badgeId: 'badge id', + badgeMetaData: '{}', + request: { userId: 'authorized-issuer@test.com' }, + }) + ).rejects.toMatchInlineSnapshot( + `[HTTPError: User with ID=not-exist does not exist]` + ); }); - MockDate.reset(); - expect(result).toMatchInlineSnapshot(` - Object { - "badgeId": "test-certification-001", - "badgeMetaData": "{\\"from\\":\\"some-orgnization}", - } - `); + /** + * Asserts the document in database is the same as in the fixture, + * i.e. the document is not modified + * + * @param {string} fixtureKey + * @param {{index: string; id: string;}} clientGetArgs - Arguments for client.get() + */ + + it('correctly sets the awarded badge id when authorized', async () => { + const result = await awardBadge({ + userId: 'user-to-award-badge', + badgeId: 'test-certification-001', + badgeMetaData: '{"from":"some-orgnization"}', + request: { userId: 'authorized-issuer@test.com' }, + }); - const { - body: { _source: userWithBadge }, - } = await client.get({ - index: 'users', - type: 'doc', - id: 'user-to-award-badge', + expect(result).toMatchInlineSnapshot(` + Object { + "badgeId": "test-certification-001", + "badgeMetaData": "{\\"from\\":\\"some-orgnization\\"}", + } + `); + + const { + body: { _source: userWithBadge }, + } = await client.get({ + index: 'users', + type: 'doc', + id: 'user-to-award-badge', + }); + + // Assert that badgeId is written on the user + expect(userWithBadge).toMatchInlineSnapshot(` + Object { + "badges": Array [ + Object { + "badgeId": "test-certification-001", + "badgeMetaData": "{\\"from\\":\\"some-orgnization\\"}", + "createdAt": "1989-06-04T00:00:00.000Z", + "isDisplayed": true, + "updatedAt": "1989-06-04T00:00:00.000Z", + }, + ], + "createdAt": "2020-01-01T00:00:00.000Z", + "googleId": "some-google-id", + "name": "user-to-award-badge", + } + `); }); - // Assert that badgeId is written on the user - expect(userWithBadge).toMatchInlineSnapshot(` - Object { - "badges": Array [ - Object { - "badgeId": "test-certification-001", - "badgeMetaData": "{\\"from\\":\\"some-orgnization}", - "createdAt": "1989-06-04T00:00:00.000Z", - "isDisplayed": true, - "updatedAt": "1989-06-04T00:00:00.000Z", - }, - ], - "createdAt": "2020-01-01T00:00:00.000Z", - "googleId": "some-google-id", - "name": "user-to-award-badge", - } - `); + it('allows service token to award badge', async () => { + const result = await awardBadge({ + userId: 'user-to-award-badge', + badgeId: 'test-certification-001', + badgeMetaData: '{"from":"service"}', + request: { userId: 'service-token-123' }, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "badgeId": "test-certification-001", + "badgeMetaData": "{\\"from\\":\\"service\\"}", + } + `); + + const { + body: { _source: userWithBadge }, + } = await client.get({ + index: 'users', + type: 'doc', + id: 'user-to-award-badge', + }); + + expect(userWithBadge.badges).toHaveLength(1); + expect(userWithBadge.badges[0].badgeId).toBe('test-certification-001'); + }); }); diff --git a/src/adm/handlers/moderation/awardBadge.ts b/src/adm/handlers/moderation/awardBadge.ts index 62b7a740..9028738c 100644 --- a/src/adm/handlers/moderation/awardBadge.ts +++ b/src/adm/handlers/moderation/awardBadge.ts @@ -68,6 +68,39 @@ async function appendBadgeToList( } } +/** + * Verify if the badge exists and if the current user is authorized to issue it + * + * @param badgeId - ID of the badge to verify + * @param requestUserId - ID of the user making the request + * @throws {HTTPError} if badge doesn't exist or user is not authorized + */ +async function verifyBadgeIssuer(badgeId: string, requestUserId: string) { + try { + const { + body: { _source: badge }, + } = await client.get({ + index: 'badges', + type: 'doc', + id: badgeId, + }); + + if (!badge) { + throw new HTTPError(404, `Badge with ID=${badgeId} does not exist`); + } + + if (!badge.issuers?.includes(requestUserId)) { + throw new HTTPError( + 403, + `User ${requestUserId} is not authorized to issue badge ${badgeId}` + ); + } + } catch (e) { + if (e instanceof HTTPError) throw e; + throw new HTTPError(404, `Badge with ID=${badgeId} does not exist`); + } +} + type awardBadgeReturnValue = { badgeId: string; badgeMetaData: string; @@ -77,11 +110,31 @@ async function main({ userId, badgeId, badgeMetaData, + request, }: { userId: string; badgeId: string; badgeMetaData: string; + request: { userId: string }; }): Promise { + // Check if user exists first + try { + const { body } = await client.get({ + index: 'users', + type: 'doc', + id: userId, + }); + if (!body._source) { + throw new HTTPError(400, `User with ID=${userId} does not exist`); + } + } catch (e) { + if (e instanceof HTTPError) throw e; + throw new HTTPError(400, `User with ID=${userId} does not exist`); + } + + // Verify if the current user/service is authorized to issue this badge + await verifyBadgeIssuer(badgeId, request.userId); + await appendBadgeToList(userId, badgeId, badgeMetaData); return { diff --git a/src/adm/index.ts b/src/adm/index.ts index b1219e86..47373ab6 100644 --- a/src/adm/index.ts +++ b/src/adm/index.ts @@ -50,7 +50,7 @@ const router = createRouter({ ), }, }, - handler: async (request) => + handler: async (request: Request) => Response.json(pingHandler(await request.json())), }) .route({ @@ -81,7 +81,7 @@ const router = createRouter({ }), }, }, - handler: async (request) => + handler: async (request: Request) => Response.json(await blockUser(await request.json())), }) .route({ @@ -112,8 +112,15 @@ const router = createRouter({ }), }, }, - handler: async (request) => - Response.json(await awardBadge(await request.json())), + handler: async (request: Request) => { + const body = await request.json(); + return Response.json( + await awardBadge({ + ...body, + request, // Pass the entire request object from feTS + }) + ); + }, }); createServer(router).listen(process.env.ADM_PORT, () => {