Skip to content

Commit

Permalink
feat: implement account restriction (#1053)
Browse files Browse the repository at this point in the history
API implementation for account restriction:
Adds middleware to check if account is restricted
Moves and adds user tag based tests to auth.spec.js
If user account has HasAccountRestricted user tag, they can no longer (even if they are PSA enabled)
Adds some documentation to the readme
  • Loading branch information
Alexandra Stoica authored Mar 24, 2022
1 parent c7d562e commit 6f6f279
Show file tree
Hide file tree
Showing 13 changed files with 433 additions and 125 deletions.
4 changes: 4 additions & 0 deletions packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ The given API has a set of three different authentication levels:

The 👮 API methods are only allowed with a Magic Token, and consequently only available via https://web3.storage

### Account restriction

If a user's account is restricted, it means that they might have gone over the storage limit assigned to them. This restriction disables several actions such as uploading files, adding and replacing pin requests, or publishing a name record. Note that even if the account has [pinning service API access](https://docs.web3.storage/how-tos/pinning-services-api/#requesting-access), account restriction will disable adding and replacing of pins. It is however still possible to delete pins and create/delete API tokens. For more information, please email <[email protected]>.

### 🔒 `POST /car`

Upload a CAR file for a root CID. _Authenticated_
Expand Down
55 changes: 44 additions & 11 deletions packages/api/src/auth.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import * as JWT from './utils/jwt.js'
import {
UserNotFoundError,
AccountRestrictedError,
MagicTokenRequiredError,
NoTokenError,
PinningUnauthorizedError,
TokenNotFoundError,
UnrecognisedTokenError,
NoTokenError,
MagicTokenRequiredError
UserNotFoundError
} from './errors.js'
import { USER_TAGS } from './constants.js'

/**
* Middleware: verify the request is authenticated with a valid magic link token.
* On successful login, adds `auth.user` to the Request
* On successful login, adds `auth.user` and `auth.userTags` to the Request
*
* @param {import('itty-router').RouteHandler} handler
* @returns {import('itty-router').RouteHandler}
Expand All @@ -19,14 +21,15 @@ export function withMagicToken (handler) {
/**
* @param {Request} request
* @param {import('./env').Env}
* @returns {Response}
* @returns {Promise<Response>}
*/
return async (request, env, ctx) => {
const token = getTokenFromRequest(request, env)

const magicUser = await tryMagicToken(token, env)
if (magicUser) {
request.auth = { user: magicUser }
const userTags = await getUserTags(magicUser._id, env)
request.auth = { user: magicUser, userTags }
env.sentry && env.sentry.setUser(magicUser)
return handler(request, env, ctx)
}
Expand All @@ -37,7 +40,7 @@ export function withMagicToken (handler) {

/**
* Middleware: verify the request is authenticated with a valid an api token *or* a magic link token.
* On successful login, adds `auth.user` and `auth.auth.token` to the Request
* On successful login, adds `auth.user`, `auth.authToken`, and `auth.userTags` to the Request
*
* @param {import('itty-router').RouteHandler} handler
* @returns {import('itty-router').RouteHandler}
Expand All @@ -46,21 +49,30 @@ export function withApiOrMagicToken (handler) {
/**
* @param {Request} request
* @param {import('./env').Env}
* @returns {Response}
* @returns {Promise<Response>}
*/
return async (request, env, ctx) => {
const token = getTokenFromRequest(request, env)

const magicUser = await tryMagicToken(token, env)
if (magicUser) {
request.auth = { user: magicUser }
const userTags = await getUserTags(magicUser._id, env)
request.auth = {
user: magicUser,
userTags
}
env.sentry && env.sentry.setUser(magicUser)
return handler(request, env, ctx)
}

const apiToken = await tryWeb3ApiToken(token, env)
if (apiToken) {
request.auth = { authToken: apiToken, user: apiToken.user }
const userTags = await getUserTags(apiToken.user._id, env)
request.auth = {
authToken: apiToken,
user: apiToken.user,
userTags
}
env.sentry && env.sentry.setUser(apiToken.user)
return handler(request, env, ctx)
}
Expand All @@ -69,6 +81,23 @@ export function withApiOrMagicToken (handler) {
}
}

/**
* Middleware: verify that the authenticated request is for a user whose
* account is not restricted.
*
* @param {import('itty-router').RouteHandler} handler
* @returns {import('itty-router').RouteHandler}
*/
export function withAccountNotRestricted (handler) {
return async (request, env, ctx) => {
const isAccountRestricted = request.auth.userTags.find(v => (v.tag === USER_TAGS.ACCOUNT_RESTRICTION && v.value === 'true'))
if (!isAccountRestricted) {
return handler(request, env, ctx)
}
throw new AccountRestrictedError()
}
}

/**
* Middleware: verify that the authenticated request is for a user who is
* authorized to pin.
Expand All @@ -78,7 +107,7 @@ export function withApiOrMagicToken (handler) {
*/
export function withPinningAuthorized (handler) {
return async (request, env, ctx) => {
const authorized = await env.db.isPinningAuthorized(request.auth.user._id)
const authorized = request.auth.userTags.find(v => (v.tag === USER_TAGS.PSA_ACCESS && v.value === 'true'))
if (authorized) {
return handler(request, env, ctx)
}
Expand Down Expand Up @@ -146,6 +175,10 @@ function findUserByIssuer (issuer, env) {
return env.db.getUser(issuer)
}

function getUserTags (userId, env) {
return env.db.getUserTags(userId)
}

function verifyAuthToken (token, decoded, env) {
return env.db.getKey(decoded.sub, token)
}
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ export const DAG_SIZE_CALC_LIMIT = 1024 * 1024 * 9
export const MAX_BLOCK_SIZE = 1 << 20 // 1MiB
export const UPLOAD_TYPES = ['Car', 'Blob', 'Multipart', 'Upload']
export const PIN_STATUSES = ['PinQueued', 'Pinning', 'Pinned', 'PinError']
export const USER_TAGS = {
ACCOUNT_RESTRICTION: 'HasAccountRestriction',
PSA_ACCESS: 'HasPsaAccess'
}
9 changes: 9 additions & 0 deletions packages/api/src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export class PinningUnauthorizedError extends HTTPError {
}
PinningUnauthorizedError.CODE = 'ERROR_PINNING_UNAUTHORIZED'

export class AccountRestrictedError extends HTTPError {
constructor (msg = 'This account is restricted, email [email protected] for more information.') {
super(msg, 403)
this.name = 'AccountRestrictedError'
this.code = AccountRestrictedError.CODE
}
}
AccountRestrictedError.CODE = 'ERROR_ACCOUNT_RESTRICTED'

export class TokenNotFoundError extends HTTPError {
constructor (msg = 'API token no longer valid') {
super(msg, 401)
Expand Down
23 changes: 12 additions & 11 deletions packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Router } from 'itty-router'
import { errorHandler } from './error-handler.js'
import { addCorsHeaders, withCorsHeaders, corsOptions } from './cors.js'
import { withApiOrMagicToken, withMagicToken, withPinningAuthorized } from './auth.js'
import { withAccountNotRestricted, withApiOrMagicToken, withMagicToken, withPinningAuthorized } from './auth.js'
import { envAll } from './env.js'
import { statusGet } from './status.js'
import { carHead, carGet, carPut, carPost } from './car.js'
Expand All @@ -27,7 +27,8 @@ const auth = {
'🤲': handler => withCorsHeaders(handler),
'🔒': handler => withCorsHeaders(withApiOrMagicToken(handler)),
'👮': handler => withCorsHeaders(withMagicToken(handler)),
'📌': handler => auth['🔒'](withPinningAuthorized(handler))
'📌': handler => withPinningAuthorized(handler),
'🚫': handler => withAccountNotRestricted(handler)
}

const mode = {
Expand All @@ -41,20 +42,20 @@ router.get('/status/:cid', mode['👀'](auth['🤲'](statusGet)))
router.get('/car/:cid', mode['👀'](auth['🤲'](carGet)))
router.head('/car/:cid', mode['👀'](auth['🤲'](carHead)))

router.post('/car', mode['📝'](auth['🔒'](carPost)))
router.put('/car/:cid', mode['📝'](auth['🔒'](carPut)))
router.post('/upload', mode['📝'](auth['🔒'](uploadPost)))
router.post('/car', mode['📝'](auth['🔒'](auth['🚫'](carPost))))
router.put('/car/:cid', mode['📝'](auth['🔒'](auth['🚫'](carPut))))
router.post('/upload', mode['📝'](auth['🔒'](auth['🚫'](uploadPost))))
router.get('/user/uploads', mode['👀'](auth['🔒'](userUploadsGet)))

router.post('/pins', mode['📝'](auth['📌'](pinPost)))
router.post('/pins/:requestId', mode['📝'](auth['📌'](pinPost)))
router.get('/pins/:requestId', mode['👀'](auth['📌'](pinGet)))
router.get('/pins', mode['👀'](auth['📌'](pinsGet)))
router.delete('/pins/:requestId', mode['📝'](auth['📌'](pinDelete)))
router.post('/pins', mode['📝'](auth['🔒'](auth['🚫'](auth['📌'](pinPost)))))
router.post('/pins/:requestId', mode['📝'](auth['🔒'](auth['🚫'](auth['📌'](pinPost)))))
router.get('/pins/:requestId', mode['👀'](auth['🔒'](auth['📌'](pinGet))))
router.get('/pins', mode['👀'](auth['🔒'](auth['📌'](pinsGet))))
router.delete('/pins/:requestId', mode['📝'](auth['🔒'](auth['📌'](pinDelete))))

router.get('/name/:key', mode['👀'](auth['🤲'](nameGet)))
router.get('/name/:key/watch', mode['👀'](auth['🤲'](nameWatchGet)))
router.post('/name/:key', mode['📝'](auth['🔒'](namePost)))
router.post('/name/:key', mode['📝'](auth['🔒'](auth['🚫'](namePost))))

router.delete('/user/uploads/:cid', mode['📝'](auth['👮'](userUploadsDelete)))
router.post('/user/uploads/:cid/rename', mode['📝'](auth['👮'](userUploadsRename)))
Expand Down
Loading

0 comments on commit 6f6f279

Please sign in to comment.