diff --git a/packages/api/db/reset.sql b/packages/api/db/reset.sql index 54587d7d5c..f4a9fcf747 100644 --- a/packages/api/db/reset.sql +++ b/packages/api/db/reset.sql @@ -2,11 +2,14 @@ DROP TYPE IF EXISTS upload_type cascade; DROP TYPE IF EXISTS pin_status_type cascade; DROP TYPE IF EXISTS service_type cascade; DROP TYPE IF EXISTS auth_key_blocked_status_type cascade; +DROP TYPE IF EXISTS user_tag_type cascade; +DROP TYPE IF EXISTS user_tag_value_type cascade; DROP TABLE IF EXISTS upload CASCADE; DROP TABLE IF EXISTS pin; DROP TABLE IF EXISTS content; DROP TABLE IF EXISTS auth_key_history; DROP TABLE IF EXISTS auth_key; +DROP TABLE IF EXISTS public.user_tag; DROP TABLE IF EXISTS public.user; DROP TABLE IF EXISTS cargo.aggregate_entries; diff --git a/packages/api/src/constants.js b/packages/api/src/constants.js index b8c1675499..ab8cbe0fa5 100644 --- a/packages/api/src/constants.js +++ b/packages/api/src/constants.js @@ -52,13 +52,6 @@ export const database = { export const isDebug = DEBUG === 'true' -/** - * The list of user IDs that are allowed to use the Pinning Service API. By - * default ["*"] - meaning anyone can use it. - */ -export const psaAllow = - typeof PSA_ALLOW !== 'undefined' ? PSA_ALLOW.split(',') : ['*'] - export const s3 = { endpoint: typeof S3_ENDPOINT !== 'undefined' ? S3_ENDPOINT : '', region: typeof S3_REGION !== 'undefined' ? S3_REGION : '', diff --git a/packages/api/src/middleware/psa.js b/packages/api/src/middleware/psa.js index 9441c90745..529cd86d00 100644 --- a/packages/api/src/middleware/psa.js +++ b/packages/api/src/middleware/psa.js @@ -1,7 +1,6 @@ import { maybeCapture, ErrorPinningUnauthorized } from '../errors.js' import { JSONResponse } from '../utils/json-response.js' import { validate } from '../utils/auth.js' -import { psaAllow } from '../constants.js' /** @typedef {import('../bindings').Handler} Handler */ @@ -28,6 +27,19 @@ export function withPsaErrorHandler(handler) { } } } +/** + * Return true if a user has a tag with a given name and value. + * + * @param {import('../utils/db-client-types.js').UserOutput} user + * @param {string} tagName + * @param {string} value + * @returns {boolean} + */ +function hasTag(user, tagName, value) { + return Boolean( + user.tags?.find((tag) => tag.tag === tagName && tag.value === value) + ) +} /** * Verify that the authenticated request is for a user who is authorized to use @@ -40,7 +52,11 @@ export function withPinningAuthorized(handler) { return async (event, ctx) => { // TODO: we need withAuth middleware so we don't have to do this twice const { user } = await validate(event, ctx) - const authorized = psaAllow.includes(String(user.id)) || psaAllow[0] === '*' + + const authorized = + hasTag(user, 'PSA_ENABLED', 'true') && + !hasTag(user, 'ACCOUNT_ENABLED', 'false') + if (!authorized) { throw new ErrorPinningUnauthorized() } diff --git a/packages/api/src/utils/db-client-types.ts b/packages/api/src/utils/db-client-types.ts index 58013c3ec0..b84d7e95dd 100644 --- a/packages/api/src/utils/db-client-types.ts +++ b/packages/api/src/utils/db-client-types.ts @@ -17,8 +17,14 @@ export type UserOutputKey = Pick< 'user_id' | 'id' | 'name' | 'secret' > +export type UserOutputTag = Pick< + definitions['user_tag'], + 'user_id' | 'id' | 'tag' | 'value' +> + export type UserOutput = definitions['user'] & { keys: Array + tags: Array } export type UploadOutput = definitions['upload'] & { diff --git a/packages/api/src/utils/db-client.js b/packages/api/src/utils/db-client.js index 1db93d3aae..4c067a3b77 100644 --- a/packages/api/src/utils/db-client.js +++ b/packages/api/src/utils/db-client.js @@ -59,7 +59,8 @@ export class DBClient { id, magic_link_id, github_id, - keys:auth_key_user_id_fkey(user_id,id,name,secret) + keys:auth_key_user_id_fkey(user_id,id,name,secret), + tags:user_tag_user_id_fkey(user_id,id,tag,value) ` ) .or(`magic_link_id.eq.${id},github_id.eq.${id}`) @@ -377,6 +378,34 @@ export class DBClient { return data } + /** + * Create a new user tag + * + * @param {Object} tag + * @param {number} tag.user_id + * @param {string} tag.tag + * @param {string} tag.value + * @param {string} tag.value_type + * @param {string} tag.inserted_at + * @param {string} tag.reason + */ + async createUserTag(tag) { + /** @type {PostgrestQueryBuilder} */ + const query = this.client.from('user_tag') + + const { data, error } = await query.upsert(tag).single() + + if (error) { + throw new DBError(error) + } + + if (!data) { + throw new Error('User tag not created.') + } + + return data + } + /** * List auth keys * diff --git a/packages/api/src/utils/db-types.d.ts b/packages/api/src/utils/db-types.d.ts index 1e016544a3..4b517a7e6f 100644 --- a/packages/api/src/utils/db-types.d.ts +++ b/packages/api/src/utils/db-types.d.ts @@ -1030,6 +1030,38 @@ export interface definitions { */ updated_at: string } + user_tag: { + /** + * Format: bigint + * @description Note: + * This is a Primary Key. + */ + id: number + /** + * Format: bigint + * @description Note: + * This is a Foreign Key to `user.id`. + */ + user_id: number + /** Format: text */ + tag: string + /** Format: text */ + value: string + /** Format: text */ + user_tag_value_type: string + /** Format: text */ + reason: string + /** + * Format: timestamp with time zone + * @default timezone('utc'::text, now()) + */ + inserted_at: string + /** + * Format: timestamp with time zone + * @default timezone('utc'::text, now()) + */ + deleted_at?: string + } } export interface parameters { diff --git a/packages/api/test/scripts/helpers.js b/packages/api/test/scripts/helpers.js index 62a948cd56..62c8faae46 100644 --- a/packages/api/test/scripts/helpers.js +++ b/packages/api/test/scripts/helpers.js @@ -62,6 +62,23 @@ export async function createTestUserWithFixedToken({ userId: user.id, }) + await client.createUserTag({ + user_id: user.id, + tag: 'PSA_ENABLED', + value: 'true', + value_type: 'boolean', + reason: '', + inserted_at: '2/22/2022', + }) + + await client.createUserTag({ + user_id: user.id, + tag: 'ACCOUNT_ENABLED', + value: 'true', + value_type: 'boolean', + reason: '', + inserted_at: '2/22/2022', + }) return { token, userId: user.id, githubId: user.github_id } }