diff --git a/packages/api/db/migrations/003-add-HasDeleteRestriction-type.sql b/packages/api/db/migrations/003-add-HasDeleteRestriction-type.sql new file mode 100644 index 0000000000..f603ed4440 --- /dev/null +++ b/packages/api/db/migrations/003-add-HasDeleteRestriction-type.sql @@ -0,0 +1 @@ +ALTER TYPE user_tag_type ADD VALUE 'HasDeleteRestriction'; diff --git a/packages/api/db/tables.sql b/packages/api/db/tables.sql index fea8bd28d9..dc27766627 100644 --- a/packages/api/db/tables.sql +++ b/packages/api/db/tables.sql @@ -12,6 +12,7 @@ CREATE TYPE auth_key_blocked_status_type AS ENUM ( CREATE TYPE user_tag_type AS ENUM ( 'HasAccountRestriction', + 'HasDeleteRestriction', 'HasPsaAccess', 'HasSuperHotAccess', 'StorageLimitBytes' @@ -208,4 +209,4 @@ CREATE TABLE IF NOT EXISTS metric value BIGINT NOT NULL, inserted_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL -); \ No newline at end of file +); diff --git a/packages/api/src/bindings.d.ts b/packages/api/src/bindings.d.ts index bdcdb71c5f..4215fc9562 100644 --- a/packages/api/src/bindings.d.ts +++ b/packages/api/src/bindings.d.ts @@ -50,6 +50,7 @@ export interface Auth { export interface AuthOptions { checkUcan?: boolean checkHasAccountRestriction?: boolean + checkHasDeleteRestriction?: boolean checkHasPsaAccess?: boolean } diff --git a/packages/api/src/errors.js b/packages/api/src/errors.js index a28d84486d..61ba1896b5 100644 --- a/packages/api/src/errors.js +++ b/packages/api/src/errors.js @@ -197,6 +197,15 @@ export class ErrorPinningUnauthorized extends HTTPError { } ErrorPinningUnauthorized.CODE = 'ERROR_PINNING_UNAUTHORIZED' +export class ErrorDeleteRestricted extends HTTPError { + constructor(msg = 'Delete operations restricted.') { + super(msg, 403) + this.name = 'DeleteRestricted' + this.code = ErrorDeleteRestricted.CODE + } +} +ErrorDeleteRestricted.CODE = 'ERROR_DELETE_RESTRICTED' + export class ErrorAccountRestricted extends HTTPError { constructor(msg = 'Account restricted.') { super(msg, 403) diff --git a/packages/api/src/index.js b/packages/api/src/index.js index ce6880f329..136ccb6e0b 100644 --- a/packages/api/src/index.js +++ b/packages/api/src/index.js @@ -46,8 +46,9 @@ const r = new Router(getContext, { }, }) -const checkHasPsaAccess = true const checkHasAccountRestriction = true +const checkHasDeleteRestriction = true +const checkHasPsaAccess = true const checkUcan = true // Monitoring @@ -116,7 +117,10 @@ r.add( r.add( 'delete', '/pins/:requestid', - withAuth(withMode(pinsDelete, RW), { checkHasPsaAccess }), + withAuth(withMode(pinsDelete, RW), { + checkHasDeleteRestriction, + checkHasPsaAccess, + }), [postCors] ) @@ -145,7 +149,12 @@ r.add( withAuth(withMode(nftStore, RW), { checkHasAccountRestriction }), [postCors] ) -r.add('delete', '/:cid', withAuth(withMode(nftDelete, RW)), [postCors]) +r.add( + 'delete', + '/:cid', + withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }), + [postCors] +) // Temporary Metaplex upload route, mapped to metaplex user account. r.add('post', '/metaplex/upload', withMode(metaplexUpload, RW), [postCors]) @@ -167,9 +176,12 @@ r.add( withAuth(withMode(tokensCreate, RW), { checkHasAccountRestriction }), [postCors] ) -r.add('delete', '/internal/tokens', withAuth(withMode(tokensDelete, RW)), [ - postCors, -]) +r.add( + 'delete', + '/internal/tokens', + withAuth(withMode(tokensDelete, RW), { checkHasDeleteRestriction }), + [postCors] +) // Blog r.add('post', '/internal/blog/subscribe', blogSubscribe, [postCors]) @@ -208,7 +220,10 @@ r.add( r.add( 'delete', '/api/pins/:requestid', - withAuth(withMode(pinsDelete, RW), { checkHasPsaAccess }), + withAuth(withMode(pinsDelete, RW), { + checkHasDeleteRestriction, + checkHasPsaAccess, + }), [postCors] ) @@ -222,7 +237,12 @@ r.add( withAuth(withMode(nftUpload, RW), { checkUcan, checkHasAccountRestriction }), [postCors] ) -r.add('delete', '/api/:cid', withAuth(withMode(nftDelete, RW)), [postCors]) +r.add( + 'delete', + '/api/:cid', + withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }), + [postCors] +) r.add('all', '*', notFound) addEventListener('fetch', r.listen.bind(r)) diff --git a/packages/api/src/middleware/auth.js b/packages/api/src/middleware/auth.js index bf8cad65a0..aa505c5e14 100644 --- a/packages/api/src/middleware/auth.js +++ b/packages/api/src/middleware/auth.js @@ -1,4 +1,8 @@ -import { ErrorAccountRestricted, ErrorPinningUnauthorized } from '../errors' +import { + ErrorAccountRestricted, + ErrorDeleteRestricted, + ErrorPinningUnauthorized, +} from '../errors' import { validate } from '../utils/auth' import { hasTag } from '../utils/utils' @@ -19,6 +23,13 @@ export function withAuth(handler, options) { throw new ErrorAccountRestricted() } + if ( + options?.checkHasDeleteRestriction && + hasTag(auth.user, 'HasDeleteRestriction', 'true') + ) { + throw new ErrorDeleteRestricted() + } + if ( options?.checkHasPsaAccess && !hasTag(auth.user, 'HasPsaAccess', 'true') diff --git a/packages/api/src/routes/user-tags.js b/packages/api/src/routes/user-tags.js index 53385b6d9c..5c48dc7206 100644 --- a/packages/api/src/routes/user-tags.js +++ b/packages/api/src/routes/user-tags.js @@ -9,6 +9,7 @@ export const userTags = async (event, ctx) => { const tags = { HasAccountRestriction: hasTag(user, 'HasAccountRestriction', 'true'), HasPsaAccess: hasTag(user, 'HasPsaAccess', 'true'), + HasDeleteRestriction: hasTag(user, 'HasDeleteRestriction', 'true'), HasSuperHotAccess: hasTag(user, 'HasSuperHotAccess', 'true'), StorageLimitBytes: getTagValue(user, 'StorageLimitBytes', ''), }