From 5ad7cd154d0f0bd76b0c17c7af83d6006b3364f0 Mon Sep 17 00:00:00 2001 From: trigramdev9 Date: Tue, 31 May 2022 11:23:13 -0700 Subject: [PATCH] feat: Adding HasDeleteRestriction user_tag * Adding the type and failing HTTP DELETE operations if this tag is set. * See nftstorage/admin.storage#66 --- packages/api/src/auth.js | 18 ++++++++++++++++++ packages/api/src/constants.js | 1 + packages/api/src/errors.js | 9 +++++++++ packages/api/src/index.js | 14 ++++++++++---- packages/api/src/user.js | 1 + .../012-add-HasDeleteRestriction-user_tag.sql | 1 + packages/db/postgres/tables.sql | 7 ++++--- 7 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 packages/db/postgres/migrations/012-add-HasDeleteRestriction-user_tag.sql diff --git a/packages/api/src/auth.js b/packages/api/src/auth.js index 67432e6ca6..36982844cf 100644 --- a/packages/api/src/auth.js +++ b/packages/api/src/auth.js @@ -1,6 +1,7 @@ import * as JWT from './utils/jwt.js' import { AccountRestrictedError, + DeleteRestrictedError, MagicTokenRequiredError, NoTokenError, PinningUnauthorizedError, @@ -102,6 +103,23 @@ export function withAccountNotRestricted (handler) { } } +/** + * Middleware: verify that the authenticated request is for a user whose + * ability to delete is not restricted. + * + * @param {import('itty-router').RouteHandler} handler + * @returns {import('itty-router').RouteHandler} + */ +export function withDeleteNotRestricted (handler) { + return async (request, env, ctx) => { + const isDeleteRestricted = request.auth.userTags.find(v => (v.tag === USER_TAGS.DELETE_RESTRICTION && v.value === 'true')) + if (!isDeleteRestricted) { + return handler(request, env, ctx) + } + throw new DeleteRestrictedError() + } +} + /** * Middleware: verify that the authenticated request is for a user who is * authorized to pin. diff --git a/packages/api/src/constants.js b/packages/api/src/constants.js index 04e7ddac3e..1bb5ce8998 100644 --- a/packages/api/src/constants.js +++ b/packages/api/src/constants.js @@ -7,5 +7,6 @@ export const UPLOAD_TYPES = ['Car', 'Blob', 'Multipart', 'Upload'] export const PIN_STATUSES = ['PinQueued', 'Pinning', 'Pinned', 'PinError'] export const USER_TAGS = { ACCOUNT_RESTRICTION: 'HasAccountRestriction', + DELETE_RESTRICTION: 'HasDeleteRestriction', PSA_ACCESS: 'HasPsaAccess' } diff --git a/packages/api/src/errors.js b/packages/api/src/errors.js index b243abd7f1..01264e64bd 100644 --- a/packages/api/src/errors.js +++ b/packages/api/src/errors.js @@ -49,6 +49,15 @@ export class AccountRestrictedError extends HTTPError { } AccountRestrictedError.CODE = 'ERROR_ACCOUNT_RESTRICTED' +export class DeleteRestrictedError extends HTTPError { + constructor (msg = 'Delete operations restricted.') { + super(msg, 403) + this.name = 'DeleteRestrictedError' + this.code = DeleteRestrictedError.CODE + } +} +DeleteRestrictedError.CODE = 'ERROR_DELETE_RESTRICTED' + export class TokenNotFoundError extends HTTPError { constructor (msg = 'API token no longer valid') { super(msg, 401) diff --git a/packages/api/src/index.js b/packages/api/src/index.js index c27bbb7138..1e2f056419 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -2,7 +2,7 @@ import { Router } from 'itty-router' import { errorHandler } from './error-handler.js' import { addCorsHeaders, withCorsHeaders, corsOptions } from './cors.js' -import { withAccountNotRestricted, withApiOrMagicToken, withMagicToken, withPinningAuthorized } from './auth.js' +import { withAccountNotRestricted, withDeleteNotRestricted, withApiOrMagicToken, withMagicToken, withPinningAuthorized } from './auth.js' import { envAll } from './env.js' import { statusGet } from './status.js' import { carHead, carGet, carPut, carPost } from './car.js' @@ -48,9 +48,15 @@ const auth = { // must be a logged in user '👤': compose(withCorsHeaders, withMagicToken), + // must be a logged in user with no delete restriction + '👤🗑️': compose(withCorsHeaders, withMagicToken, withDeleteNotRestricted), + // needs PSA & restricted users allowed '📌⚠️': compose(withCorsHeaders, withApiOrMagicToken, withPinningAuthorized), + // needs PSA & restricted users with no delete restriction allowed + '📌⚠️🗑️': compose(withCorsHeaders, withApiOrMagicToken, withDeleteNotRestricted, withPinningAuthorized), + // needs PSA '📌': compose(withCorsHeaders, withApiOrMagicToken, withAccountNotRestricted, withPinningAuthorized) // needs PSA } @@ -70,17 +76,17 @@ router.post('/pins', auth['📌'](pinPost)) router.post('/pins/:requestId', auth['📌'](pinPost)) router.get('/pins/:requestId', auth['📌⚠️'](pinGet)) router.get('/pins', auth['📌⚠️'](pinsGet)) -router.delete('/pins/:requestId', auth['📌⚠️'](pinDelete)) +router.delete('/pins/:requestId', auth['📌⚠️🗑️'](pinDelete)) router.get('/name/:key', auth['🌍'](nameGet)) router.get('/name/:key/watch', auth['🌍'](nameWatchGet)) router.post('/name/:key', auth['🔑'](namePost)) -router.delete('/user/uploads/:cid', auth['👤'](userUploadsDelete)) +router.delete('/user/uploads/:cid', auth['👤🗑️'](userUploadsDelete)) router.post('/user/uploads/:cid/rename', auth['👤'](userUploadsRename)) router.get('/user/tokens', auth['👤'](userTokensGet)) router.post('/user/tokens', auth['👤'](userTokensPost)) -router.delete('/user/tokens/:id', auth['👤'](userTokensDelete)) +router.delete('/user/tokens/:id', auth['👤🗑️'](userTokensDelete)) router.get('/user/account', auth['👤'](userAccountGet)) router.get('/user/info', auth['👤'](userInfoGet)) /* eslint-enable no-multi-spaces */ diff --git a/packages/api/src/user.js b/packages/api/src/user.js index 940cc3e914..96d4d14511 100644 --- a/packages/api/src/user.js +++ b/packages/api/src/user.js @@ -135,6 +135,7 @@ export async function userInfoGet (request, env) { ...user, tags: { HasAccountRestriction: hasTag(user, 'HasAccountRestriction', 'true'), + HasDeleteRestriction: hasTag(user, 'HasDeleteRestriction', 'true'), HasPsaAccess: hasTag(user, 'HasPsaAccess', 'true'), HasSuperHotAccess: hasTag(user, 'HasSuperHotAccess', 'true'), StorageLimitBytes: getTagValue(user, 'StorageLimitBytes', '') diff --git a/packages/db/postgres/migrations/012-add-HasDeleteRestriction-user_tag.sql b/packages/db/postgres/migrations/012-add-HasDeleteRestriction-user_tag.sql new file mode 100644 index 0000000000..f603ed4440 --- /dev/null +++ b/packages/db/postgres/migrations/012-add-HasDeleteRestriction-user_tag.sql @@ -0,0 +1 @@ +ALTER TYPE user_tag_type ADD VALUE 'HasDeleteRestriction'; diff --git a/packages/db/postgres/tables.sql b/packages/db/postgres/tables.sql index 1165357f95..cc54a4cd6f 100644 --- a/packages/db/postgres/tables.sql +++ b/packages/db/postgres/tables.sql @@ -1,4 +1,4 @@ -DO +DO $$ BEGIN -- Auth key blocked status type is the type of blocking that has occurred on the api @@ -19,6 +19,7 @@ BEGIN CREATE TYPE user_tag_type AS ENUM ( 'HasAccountRestriction', + 'HasDeleteRestriction', 'HasPsaAccess', 'StorageLimitBytes' ); @@ -26,7 +27,7 @@ BEGIN -- Types for notification emails IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'email_type') THEN - CREATE TYPE email_type AS ENUM + CREATE TYPE email_type AS ENUM ( 'User75PercentStorage', 'User80PercentStorage', @@ -310,7 +311,7 @@ CREATE TABLE IF NOT EXISTS metric updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL ); -CREATE TABLE IF NOT EXISTS email_history +CREATE TABLE IF NOT EXISTS email_history ( id BIGSERIAL PRIMARY KEY, -- the id of the user being notified