From ba9ea54880a9cea592ffd351e52bd8cc415da047 Mon Sep 17 00:00:00 2001 From: Johan Eliasson Date: Fri, 8 May 2020 07:08:21 +0200 Subject: [PATCH] Cookie updates, general improvements and bug fixes. SOOOOON! (#218) * set permission_variables cookie * use permission variables in storage from cookie * fix(security): upgrade Hasusa to v1.2.1 * refactor(auth): clear permission_variables cookie on logout * refactor(auth): clear permission_variables cookie when deleting an account * refactor: simplify generation of permission variables reuse the same code for both permission_variables cookie and jwt claims * fix(storage): change isOwner function according to the new cookie specs * test(storage): fix the tests so they work with the new way of getting auth info * style: typo * fix: send hasura claim values as string * storage polishing * updated standard storage rules * updated storage tests * remove login on test * user cookie user vars on delete * removed unused logs * specify 'user-id' to return string | number * formatting * user id is always string * use cookie to get user-id * typo * fix: clean and fix code so every test passes * ci: runInBand * refactor(auth): refactor routes Co-authored-by: Pierre-Louis Mercereau <24897252+plmercereau@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- custom/emails/activate-account/html.ejs | 2 +- custom/storage-rules/rules.yaml | 12 +-- docker-compose.yaml | 2 +- docs/api.md | 4 +- .../simple-hasura-minio/docker-compose.yaml | 2 +- package.json | 6 +- src/routes/auth/account/index.ts | 14 --- src/routes/auth/{account => }/activate.ts | 0 src/routes/auth/auth.test.ts | 67 ++++++------- src/routes/auth/change-email/utils.ts | 20 ++-- src/routes/auth/change-password/change.ts | 20 ++-- src/routes/auth/delete.test.ts | 29 ++++++ src/routes/auth/{account => }/delete.ts | 18 ++-- src/routes/auth/index.ts | 19 +++- src/routes/auth/logout.ts | 15 +-- src/routes/auth/mfa/disable.ts | 14 ++- src/routes/auth/mfa/enable.ts | 14 ++- src/routes/auth/mfa/generate.ts | 9 +- src/routes/auth/token/refresh.ts | 7 +- src/routes/auth/token/revoke.ts | 9 +- src/routes/storage/delete.ts | 4 +- src/routes/storage/get.ts | 6 +- src/routes/storage/index.ts | 63 ++++++------ src/routes/storage/list.ts | 27 ++--- src/routes/storage/storage.test.ts | 77 +++++++-------- src/routes/storage/upload.ts | 5 +- src/routes/storage/utils.ts | 30 +++--- src/shared/helpers.ts | 10 +- src/shared/jwt.ts | 98 ++++++++++++------- src/shared/queries.ts | 7 +- src/shared/types.ts | 10 ++ src/test/test-utils.ts | 4 +- yarn.lock | 7 +- 34 files changed, 352 insertions(+), 281 deletions(-) delete mode 100644 src/routes/auth/account/index.ts rename src/routes/auth/{account => }/activate.ts (100%) create mode 100644 src/routes/auth/delete.test.ts rename src/routes/auth/{account => }/delete.ts (52%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b5bf52..baf3524 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} options: --restart always --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 graphql-engine: - image: hasura/graphql-engine:v1.2.0 + image: hasura/graphql-engine:v1.2.1 env: HASURA_GRAPHQL_ENABLE_TELEMETRY: 'false' HASURA_GRAPHQL_ADMIN_SECRET: ${{ env.HASURA_GRAPHQL_ADMIN_SECRET }} diff --git a/custom/emails/activate-account/html.ejs b/custom/emails/activate-account/html.ejs index eaed5a2..b72169f 100644 --- a/custom/emails/activate-account/html.ejs +++ b/custom/emails/activate-account/html.ejs @@ -22,7 +22,7 @@

Hello <%= display_name %>

Please confirm your email address by clicking the button below:

- +

Thanks,
The Team

diff --git a/custom/storage-rules/rules.yaml b/custom/storage-rules/rules.yaml index b882691..c50b2bf 100644 --- a/custom/storage-rules/rules.yaml +++ b/custom/storage-rules/rules.yaml @@ -1,16 +1,10 @@ functions: isAuthenticated: 'return !!request.auth' - isOwner: - params: - - userId - code: "return !!request.auth && userId === request.auth['x-hasura-user-id']" + isOwner: "return !!request.auth && userId === request.auth['user-id']" validToken: 'return request.query.token === resource.Metadata.token' paths: /user/:userId/:fileId: read: 'isOwner(userId) || validToken()' write: 'isOwner(userId)' - meta: - read: 'isOwner(userId)' - write: 'isOwner(userId)' - values: - description: 'request.query.description' + metadata: + description: 'request.query.description' diff --git a/docker-compose.yaml b/docker-compose.yaml index 1a3cd93..357fef0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: environment: POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-postgrespassword}' graphql-engine: - image: hasura/graphql-engine:v1.2.0 + image: hasura/graphql-engine:v1.2.1 depends_on: - 'postgres' restart: always diff --git a/docs/api.md b/docs/api.md index 9fb6c0e..66525a5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -5,8 +5,8 @@ | [Authentication](#authentication) | [POST /auth/register](#registration) | Account registration | | ^^ | [POST /auth/login](#login) | Login | | ^^ | [GET /auth/jwks](#jwks) | JWK Set | -| ^^ | [POST /auth/account/activate](#activation) | Activate account | -| ^^ | [POST /auth/account/delete](#delete-account) | Delete account | +| ^^ | [POST /auth/activate](#activation) | Activate account | +| ^^ | [POST /auth/delete](#delete-account) | Delete account | | ^^ | [POST /auth/change-password/request](#forgotten-password) | Forgotten password | | ^^ | [POST /auth/change-password/change](#reset-password) | Reset password | | ^^ | [POST /auth/change-email/request](#) | | diff --git a/examples/simple-hasura-minio/docker-compose.yaml b/examples/simple-hasura-minio/docker-compose.yaml index 5d0bb32..81deadc 100644 --- a/examples/simple-hasura-minio/docker-compose.yaml +++ b/examples/simple-hasura-minio/docker-compose.yaml @@ -6,7 +6,7 @@ services: volumes: - db_data:/var/lib/postgresql/data graphql-engine: - image: hasura/graphql-engine:v1.2.0 + image: hasura/graphql-engine:v1.2.1 ports: - '8080:8080' depends_on: diff --git a/package.json b/package.json index 9d9618c..dfff7de 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "docs:dev": "vuepress dev docs", "start": "ts-node -r tsconfig-paths/register src/start.ts", "test": "NODE_ENV=test jest --coverage --forceExit", - "test:ci": "NODE_ENV=ci jest --coverage --forceExit", + "test:ci": "NODE_ENV=ci jest --coverage --forceExit --runInBand", "test:watch": "NODE_ENV=test jest --watch", "report-coverage": "codecov" }, @@ -73,7 +73,7 @@ "graphql": "15.0.0", "graphql-request": "1.8.2", "graphql-tag": "2.10.3", - "hasura-cli": "1.2.0", + "hasura-cli": "1.2.1", "helmet": "3.22.0", "hibp": "9.0.0", "jose": "1.26.0", @@ -170,4 +170,4 @@ "singleQuote": true, "trailingComma": "none" } -} +} \ No newline at end of file diff --git a/src/routes/auth/account/index.ts b/src/routes/auth/account/index.ts deleted file mode 100644 index bd8c6e8..0000000 --- a/src/routes/auth/account/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AUTO_ACTIVATE_NEW_USERS } from '@shared/config' -import { Router } from 'express' -import activateAccount from './activate' -import deleteAccount from './delete' - -const router = Router() - -if (!AUTO_ACTIVATE_NEW_USERS) { - router.get('/activate', activateAccount) -} - -router.post('/delete', deleteAccount) - -export default router diff --git a/src/routes/auth/account/activate.ts b/src/routes/auth/activate.ts similarity index 100% rename from src/routes/auth/account/activate.ts rename to src/routes/auth/activate.ts diff --git a/src/routes/auth/auth.test.ts b/src/routes/auth/auth.test.ts index 05847bb..c8717f9 100644 --- a/src/routes/auth/auth.test.ts +++ b/src/routes/auth/auth.test.ts @@ -13,12 +13,12 @@ import { ANONYMOUS_USERS_ENABLE } from '@shared/config' import { generateRandomString, selectAccountByEmail } from '@shared/helpers' -import { deleteMailHogEmail, mailHogSearch, registerAccount, deleteAccount } from '@test/test-utils' +import { deleteMailHogEmail, mailHogSearch, deleteAccount } from '@test/test-utils' import { JWT } from 'jose' import { Token } from '@shared/types' import { app } from '../../server' -import request from 'supertest' +import { SuperTest, Test, agent } from 'supertest' /** * Store variables in memory. @@ -31,18 +31,31 @@ let jwtToken: string const email = `${generateRandomString()}@${generateRandomString()}.com` const password = generateRandomString() -/** - * Create agent for global state. - */ +let request: SuperTest + const server = app.listen(PORT) -const agent = request(server) + +beforeAll(async () => { + request = agent(server) // * Create the SuperTest agent +}) + // * Code that is executed after any jest test file that imports test-utiles afterAll(async () => { server.close() }) +const pwndPasswordIt = HIBP_ENABLE ? it : it.skip +pwndPasswordIt('should tell the password has been pwned', async () => { + const { + status, + body: { message } + } = await request.post('/auth/register').send({ email: 'test@example.com', password: '123456' }) + expect(status).toEqual(400) + expect(message).toEqual('Password is too weak.') +}) + it('should create an account', async () => { - const { status } = await agent + const { status } = await request .post('/auth/register') .send({ email, password, user_data: { name: 'Test name' } }) expect(status).toEqual(204) @@ -52,7 +65,7 @@ it('should tell the account already exists', async () => { const { status, body: { message } - } = await agent.post('/auth/register').send({ email, password }) + } = await request.post('/auth/register').send({ email, password }) expect(status).toEqual(400) expect(message).toEqual('Account already exists.') }) @@ -61,7 +74,7 @@ it('should tell the account already exists', async () => { const manualActivationIt = !AUTO_ACTIVATE_NEW_USERS ? it : it.skip manualActivationIt('should fail to activate an user from a wrong ticket', async () => { - const { status, redirect, header } = await agent.get(`/auth/account/activate?ticket=${uuidv4()}`) + const { status, redirect, header } = await request.get(`/auth/activate?ticket=${uuidv4()}`) expect( status === 500 || (status === 302 && redirect && header?.location === REDIRECT_URL_ERROR) ).toBeTrue() @@ -79,12 +92,12 @@ manualActivationIt('should activate the account from a valid ticket', async () = } else { ticket = (await selectAccountByEmail(email)).ticket } - const { status } = await agent.get(`/auth/account/activate?ticket=${ticket}`) + const { status } = await request.get(`/auth/activate?ticket=${ticket}`) expect(status).toBeOneOf([204, 302]) }) it('should sign the user in', async () => { - const { body, status } = await agent.post('/auth/login').send({ email, password }) + const { body, status } = await request.post('/auth/login').send({ email, password }) // Save JWT token to globally scoped varaible. jwtToken = body.jwt_token expect(status).toEqual(200) @@ -99,37 +112,17 @@ it('should decode a valid custom user claim', async () => { expect(decodedJwt[JWT_CLAIMS_NAMESPACE]['x-hasura-name']).toEqual('Test name') }) -it('should delete the account', async () => { - const { status } = await agent - .post('/auth/account/delete') - .set('Authorization', `Bearer ${jwtToken}`) - expect(status).toEqual(204) -}) - -const pwndPasswordIt = HIBP_ENABLE ? it : it.skip -pwndPasswordIt('should tell the password has been pwned', async () => { - const { - status, - body: { message } - } = await agent.post('/auth/register').send({ email: 'test@example.com', password: '123456' }) - expect(status).toEqual(400) - expect(message).toEqual('Password is too weak.') +it('should logout', async () => { + const res = await request.post('/auth/logout').send() + expect(res.status).toBe(204) + await request.post('/auth/login').send({ email, password }) + await deleteAccount(request, { email, password }) }) const anonymousAccountIt = ANONYMOUS_USERS_ENABLE ? it : it.skip anonymousAccountIt('should login anonymously', async () => { - const { body, status } = await agent.post('/auth/login').send({ anonymous: true }) + const { body, status } = await request.post('/auth/login').send({ anonymous: true }) expect(status).toEqual(200) expect(body.jwt_token).toBeString() expect(body.jwt_expires_in).toBeNumber() }) - -it('should logout', async () => { - // TODO : review this test, including cookies - const account = await registerAccount(agent) - const res = await agent.post('/auth/logout').send() - expect(res.status).toBe(204) - await deleteAccount(agent, account) -}) - -// TODO test cookies diff --git a/src/routes/auth/change-email/utils.ts b/src/routes/auth/change-email/utils.ts index 3108bcb..fd7c38f 100644 --- a/src/routes/auth/change-email/utils.ts +++ b/src/routes/auth/change-email/utils.ts @@ -1,18 +1,17 @@ import { Request } from 'express' -import { selectAccountByEmail } from '@shared/helpers' +import { selectAccountByEmail, getPermissionVariablesFromCookie } from '@shared/helpers' import { emailResetSchema } from '@shared/validation' -import { getClaims } from '@shared/jwt' import Boom from '@hapi/boom' -export const getRequestInfo = async ({ - body, - headers -}: Request): Promise<{ user_id: string; new_email: string }> => { +export const getRequestInfo = async ( + req: Request +): Promise<{ user_id: string | number; new_email: string }> => { // get current user_id - const user_id = getClaims(headers.authorization)['x-hasura-user-id'] + const permission_variables = getPermissionVariablesFromCookie(req) + const user_id = permission_variables['user-id'] // validate new email - const { new_email } = await emailResetSchema.validateAsync(body) + const { new_email } = await emailResetSchema.validateAsync(req.body) // make sure new_email is not attached to an account yet let account_exists = true @@ -27,5 +26,8 @@ export const getRequestInfo = async ({ if (account_exists) { throw Boom.badRequest('Cannot use this email.') } - return { user_id, new_email } + return { + user_id, + new_email + } } diff --git a/src/routes/auth/change-password/change.ts b/src/routes/auth/change-password/change.ts index 681be8e..14de84e 100644 --- a/src/routes/auth/change-password/change.ts +++ b/src/routes/auth/change-password/change.ts @@ -1,4 +1,10 @@ -import { asyncWrapper, checkHibp, hashPassword, selectAccountByUserId } from '@shared/helpers' +import { + asyncWrapper, + checkHibp, + hashPassword, + selectAccountByUserId, + getPermissionVariablesFromCookie +} from '@shared/helpers' import { Request, Response } from 'express' import { resetPasswordWithOldPasswordSchema, @@ -10,18 +16,17 @@ import Boom from '@hapi/boom' import bcrypt from 'bcryptjs' import { request } from '@shared/request' import { v4 as uuidv4 } from 'uuid' -import { getClaims } from '@shared/jwt' import { UpdateAccountData } from '@shared/types' /** * Reset the password, either from a valid ticket or from a valid JWT and a valid password */ -async function changePassword({ body, headers }: Request, res: Response): Promise { +async function changePassword(req: Request, res: Response): Promise { let password_hash: string - if (body.ticket) { + if (req.body.ticket) { // Reset the password from { ticket, new_password } - const { ticket, new_password } = await resetPasswordWithTicketSchema.validateAsync(body) + const { ticket, new_password } = await resetPasswordWithTicketSchema.validateAsync(req.body) await checkHibp(new_password) password_hash = await hashPassword(new_password) @@ -39,10 +44,11 @@ async function changePassword({ body, headers }: Request, res: Response): Promis } } else { // Reset the password from valid JWT and { old_password, new_password } - const user_id = getClaims(headers.authorization)['x-hasura-user-id'] + const permission_variables = getPermissionVariablesFromCookie(req) + const user_id = permission_variables['user-id'] const { old_password, new_password } = await resetPasswordWithOldPasswordSchema.validateAsync( - body + req.body ) await checkHibp(new_password) diff --git a/src/routes/auth/delete.test.ts b/src/routes/auth/delete.test.ts new file mode 100644 index 0000000..00a1c03 --- /dev/null +++ b/src/routes/auth/delete.test.ts @@ -0,0 +1,29 @@ +import 'jest-extended' + +import { SuperTest, Test, agent } from 'supertest' + +import { PORT } from '@shared/config' +import { registerAccount } from '@test/test-utils' + +import { app } from '../../server' + +let request: SuperTest + +const server = app.listen(PORT) + +// * Code that is executed before any jest test file that imports this file +beforeAll(async () => { + request = agent(server) // * Create the SuperTest agent + registerAccount(request) +}) + +// * Code that is executed after any jest test file that imports test-utiles +afterAll(async () => { + server.close() +}) + +it('should delete an account', async () => { + await registerAccount(request) + const { status } = await request.post('/auth/delete') + expect(status).toEqual(204) +}) diff --git a/src/routes/auth/account/delete.ts b/src/routes/auth/delete.ts similarity index 52% rename from src/routes/auth/account/delete.ts rename to src/routes/auth/delete.ts index 405c3de..7e92ce6 100644 --- a/src/routes/auth/account/delete.ts +++ b/src/routes/auth/delete.ts @@ -1,28 +1,24 @@ import { Request, Response } from 'express' import Boom from '@hapi/boom' -import { asyncWrapper } from '@shared/helpers' +import { asyncWrapper, getPermissionVariablesFromCookie } from '@shared/helpers' import { deleteAccountByUserId } from '@shared/queries' import { request } from '@shared/request' -import { getClaims } from '@shared/jwt' import { DeleteAccountData } from '@shared/types' -import { ALLOW_USER_SELF_DELETE } from '@shared/config' -async function deleteUser({ headers }: Request, res: Response): Promise { - if (!ALLOW_USER_SELF_DELETE) { - throw Boom.notFound() - } - - const user_id = getClaims(headers.authorization)['x-hasura-user-id'] +async function deleteUser(req: Request, res: Response): Promise { + const permission_variables = getPermissionVariablesFromCookie(req) + const user_id = permission_variables['user-id'] const hasuraData = await request(deleteAccountByUserId, { user_id }) if (!hasuraData.delete_auth_accounts.affected_rows) { - throw Boom.unauthorized('Invalid or expired JWT token.') + throw Boom.unauthorized('Unable to delete account') } - // clear cookie + // clear cookies res.clearCookie('refresh_token') + res.clearCookie('permission_variables') return res.status(204).send() } diff --git a/src/routes/auth/index.ts b/src/routes/auth/index.ts index 26766aa..3233b51 100644 --- a/src/routes/auth/index.ts +++ b/src/routes/auth/index.ts @@ -1,6 +1,10 @@ -import { MFA_ENABLE, CHANGE_EMAIL_ENABLE } from '@shared/config' +import { + MFA_ENABLE, + CHANGE_EMAIL_ENABLE, + AUTO_ACTIVATE_NEW_USERS, + ALLOW_USER_SELF_DELETE +} from '@shared/config' import { Router } from 'express' -import account from './account' import changeEmail from './change-email' import getJwks from './jwks' import loginAccount from './login' @@ -10,6 +14,8 @@ import changePassword from './change-password' import providers from './providers' import registerAccount from './register' import token from './token' +import activateAccount from './activate' +import deleteAccount from './delete' const router = Router() @@ -25,13 +31,20 @@ if (CHANGE_EMAIL_ENABLE) { router.use('/change-email', changeEmail) } +if (!AUTO_ACTIVATE_NEW_USERS) { + router.get('/activate', activateAccount) +} + +if (ALLOW_USER_SELF_DELETE) { + router.post('/delete', deleteAccount) +} + router .get('/jwks', getJwks) .post('/login', loginAccount) .post('/logout', logout) .post('/register', registerAccount) .use('/token', token) - .use('/account', account) .use('/change-password', changePassword) export default router diff --git a/src/routes/auth/logout.ts b/src/routes/auth/logout.ts index 5d5ec26..3769ef2 100644 --- a/src/routes/auth/logout.ts +++ b/src/routes/auth/logout.ts @@ -17,6 +17,7 @@ interface HasuraData { async function logout({ body, cookies, signedCookies }: Request, res: Response): Promise { // clear cookie res.clearCookie('refresh_token') + res.clearCookie('permission_variables') // should we delete all refresh tokens to this user or not const { all } = await logoutSchema.validateAsync(body) @@ -35,20 +36,12 @@ async function logout({ body, cookies, signedCookies }: Request, res: Response): return res.status(204).send() } - if (!hasura_data) { + const account = hasura_data?.auth_refresh_tokens?.[0]?.account + + if (!account) { return res.status(204).send() } - const { auth_refresh_tokens } = hasura_data - - // if no accounts found - if (!auth_refresh_tokens.length) { - return res.status(204).send() - } - - // get logged out account - const { account } = auth_refresh_tokens[0] - // delete all refresh tokens for user try { await request(deleteAllAccountRefreshTokens, { diff --git a/src/routes/auth/mfa/disable.ts b/src/routes/auth/mfa/disable.ts index e055da1..2e59763 100644 --- a/src/routes/auth/mfa/disable.ts +++ b/src/routes/auth/mfa/disable.ts @@ -1,4 +1,8 @@ -import { asyncWrapper, selectAccountByUserId } from '@shared/helpers' +import { + asyncWrapper, + selectAccountByUserId, + getPermissionVariablesFromCookie +} from '@shared/helpers' import { Request, Response } from 'express' import { deleteOtpSecret } from '@shared/queries' @@ -6,12 +10,12 @@ import Boom from '@hapi/boom' import { authenticator } from 'otplib' import { mfaSchema } from '@shared/validation' import { request } from '@shared/request' -import { getClaims } from '@shared/jwt' -async function disableMfa({ headers, body }: Request, res: Response): Promise { - const { code } = await mfaSchema.validateAsync(body) +async function disableMfa(req: Request, res: Response): Promise { + const { code } = await mfaSchema.validateAsync(req.body) - const user_id = getClaims(headers.authorization)['x-hasura-user-id'] + const permission_variables = getPermissionVariablesFromCookie(req) + const user_id = permission_variables['user-id'] const { otp_secret, mfa_enabled } = await selectAccountByUserId(user_id) diff --git a/src/routes/auth/mfa/enable.ts b/src/routes/auth/mfa/enable.ts index 8fd2f80..cb46dc5 100644 --- a/src/routes/auth/mfa/enable.ts +++ b/src/routes/auth/mfa/enable.ts @@ -1,4 +1,8 @@ -import { asyncWrapper, selectAccountByUserId } from '@shared/helpers' +import { + asyncWrapper, + selectAccountByUserId, + getPermissionVariablesFromCookie +} from '@shared/helpers' import { Request, Response } from 'express' import { updateOtpStatus } from '@shared/queries' @@ -6,12 +10,12 @@ import Boom from '@hapi/boom' import { authenticator } from 'otplib' import { mfaSchema } from '@shared/validation' import { request } from '@shared/request' -import { getClaims } from '@shared/jwt' -async function enableMfa({ headers, body }: Request, res: Response): Promise { - const { code } = await mfaSchema.validateAsync(body) +async function enableMfa(req: Request, res: Response): Promise { + const { code } = await mfaSchema.validateAsync(req.body) - const user_id = getClaims(headers.authorization)['x-hasura-user-id'] + const permission_variables = getPermissionVariablesFromCookie(req) + const user_id = permission_variables['user-id'] const { otp_secret, mfa_enabled } = await selectAccountByUserId(user_id) diff --git a/src/routes/auth/mfa/generate.ts b/src/routes/auth/mfa/generate.ts index 2835989..4fb112c 100644 --- a/src/routes/auth/mfa/generate.ts +++ b/src/routes/auth/mfa/generate.ts @@ -1,14 +1,13 @@ import { Request, Response } from 'express' -import { asyncWrapper, createQR } from '@shared/helpers' - +import { asyncWrapper, createQR, getPermissionVariablesFromCookie } from '@shared/helpers' import { OTP_ISSUER } from '@shared/config' import { authenticator } from 'otplib' import { request } from '@shared/request' import { updateOtpSecret } from '@shared/queries' -import { getClaims } from '@shared/jwt' -async function generateMfa({ headers }: Request, res: Response): Promise { - const user_id = getClaims(headers.authorization)['x-hasura-user-id'] +async function generateMfa(req: Request, res: Response): Promise { + const permission_variables = getPermissionVariablesFromCookie(req) + const user_id = permission_variables['user-id'] /** * Generate OTP secret and key URI. diff --git a/src/routes/auth/token/refresh.ts b/src/routes/auth/token/refresh.ts index 31cb6f9..5feb6ab 100644 --- a/src/routes/auth/token/refresh.ts +++ b/src/routes/auth/token/refresh.ts @@ -8,7 +8,8 @@ import { newJwtExpiry, newRefreshExpiry, createHasuraJwt, - setRefreshTokenAsCookie + setCookie, + generatePermissionVariables } from '@shared/jwt' import { request } from '@shared/request' import { v4 as uuidv4 } from 'uuid' @@ -54,7 +55,9 @@ async function refreshToken({ cookies, signedCookies }: Request, res: Response): throw Boom.badImplementation('Unable to set new refresh token') } - setRefreshTokenAsCookie(res, new_refresh_token) + const permission_variables = JSON.stringify(generatePermissionVariables(account)) + + setCookie(res, new_refresh_token, permission_variables) return res.send({ jwt_token: createHasuraJwt(account), diff --git a/src/routes/auth/token/revoke.ts b/src/routes/auth/token/revoke.ts index fe2bff7..8b38cd4 100644 --- a/src/routes/auth/token/revoke.ts +++ b/src/routes/auth/token/revoke.ts @@ -1,12 +1,11 @@ import { Request, Response } from 'express' - -import { asyncWrapper } from '@shared/helpers' +import { asyncWrapper, getPermissionVariablesFromCookie } from '@shared/helpers' import { deleteAllAccountRefreshTokens } from '@shared/queries' import { request } from '@shared/request' -import { getClaims } from '@shared/jwt' -async function revokeToken({ headers }: Request, res: Response): Promise { - const user_id = getClaims(headers.authorization)['x-hasura-user-id'] +async function revokeToken(req: Request, res: Response): Promise { + const permission_variables = getPermissionVariablesFromCookie(req) + const user_id = permission_variables['user-id'] await request(deleteAllAccountRefreshTokens, { user_id }) diff --git a/src/routes/storage/delete.ts b/src/routes/storage/delete.ts index 0259ba8..0e833da 100644 --- a/src/routes/storage/delete.ts +++ b/src/routes/storage/delete.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from 'express' import { - StoragePermissions, + PathConfig, createContext, getHeadObject, getKey, @@ -16,7 +16,7 @@ export const deleteFile = async ( req: Request, res: Response, _next: NextFunction, - rules: Partial, + rules: Partial, isMetadataRequest = false ): Promise => { const headObject = await getHeadObject(req) diff --git a/src/routes/storage/get.ts b/src/routes/storage/get.ts index 5d70a58..2147fb6 100644 --- a/src/routes/storage/get.ts +++ b/src/routes/storage/get.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express' -import { StoragePermissions, createContext, getHeadObject, getKey, hasPermission } from './utils' +import { PathConfig, createContext, getHeadObject, getKey, hasPermission } from './utils' import Boom from '@hapi/boom' import { S3_BUCKET } from '@shared/config' @@ -9,7 +9,7 @@ export const getFile = async ( req: Request, res: Response, _next: NextFunction, - rules: Partial, + rules: Partial, isMetadataRequest = false ): Promise => { const key = getKey(req) @@ -24,7 +24,7 @@ export const getFile = async ( throw Boom.forbidden() } if (isMetadataRequest) { - return res.status(200).send(headObject) + return res.status(200).send({ key, ...headObject }) } else { const params = { Bucket: S3_BUCKET as string, diff --git a/src/routes/storage/index.ts b/src/routes/storage/index.ts index 18ba265..34727a9 100644 --- a/src/routes/storage/index.ts +++ b/src/routes/storage/index.ts @@ -1,60 +1,61 @@ -import { META_PREFIX, STORAGE_RULES, StoragePermissions, containsSomeRule } from './utils' +import { OBJECT_PREFIX, META_PREFIX, STORAGE_RULES, PathConfig, containsSomeRule } from './utils' import { NextFunction, Request, Response, Router } from 'express' import { deleteFile } from './delete' -import { getFile } from './get' import { listFile } from './list' +import { getFile } from './get' import { uploadFile } from './upload' const router = Router() const createSecureMiddleware = ( fn: Function, - rules: Partial, - isMetadataRequest: boolean, - metadataParams: object = {} + rules: Partial, + isMetadataRequest: boolean ) => (req: Request, res: Response, next: NextFunction): void => - fn(req, res, next, rules, isMetadataRequest, metadataParams).catch(next) + fn(req, res, next, rules, isMetadataRequest, rules.metadata).catch(next) const createRoutes = ( path: string, - rules: Partial, - isMetadataRequest = false, - metadataParams: object = {} + rules: Partial, + isMetadataRequest = false ): Router => { const middleware = Router() + + // write, create, update if (containsSomeRule(rules, ['write', 'create', 'update'])) { - middleware.post( - path, - createSecureMiddleware(uploadFile, rules, isMetadataRequest, metadataParams) - ) + middleware.post(path, createSecureMiddleware(uploadFile, rules, isMetadataRequest)) } - if (containsSomeRule(rules, ['read', 'get'])) { - middleware.get(path, createSecureMiddleware(getFile, rules, isMetadataRequest)) - } - if (containsSomeRule(rules, ['read', 'list'])) { - middleware.get( - `${path.substring(0, path.lastIndexOf('/'))}`, - createSecureMiddleware(listFile, rules, isMetadataRequest) - ) + + // read, get, list + if (containsSomeRule(rules, ['read', 'get', 'list'])) { + if (path.endsWith('/')) { + middleware.get(path, createSecureMiddleware(listFile, rules, isMetadataRequest)) + } else { + middleware.get(path, createSecureMiddleware(getFile, rules, isMetadataRequest)) + middleware.get( + path.substring(0, path.lastIndexOf('/')), + createSecureMiddleware(listFile, rules, isMetadataRequest) + ) + } } + + // write, delete if (containsSomeRule(rules, ['write', 'delete'])) { - middleware.delete( - path, - createSecureMiddleware(deleteFile, rules, isMetadataRequest, metadataParams) - ) + middleware.delete(path, createSecureMiddleware(deleteFile, rules, isMetadataRequest)) } + return middleware } for (const path in STORAGE_RULES.paths) { const rules = STORAGE_RULES.paths[path] - router.use(createRoutes(path, rules, false, rules.meta?.values)) - if ( - containsSomeRule(rules.meta, ['read', 'write', 'get', 'create', 'update', 'delete', 'list']) - ) { - router.use(META_PREFIX, createRoutes(path, rules.meta, true, rules.meta?.values)) - } + + // create object data paths + router.use(OBJECT_PREFIX, createRoutes(path, rules, false)) + + // create meta data paths + router.use(META_PREFIX, createRoutes(path, rules, true)) } export default router diff --git a/src/routes/storage/list.ts b/src/routes/storage/list.ts index bbef0cf..f405f9d 100644 --- a/src/routes/storage/list.ts +++ b/src/routes/storage/list.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express' -import { StoragePermissions, createContext, getKey, hasPermission } from './utils' +import { PathConfig, createContext, getKey, hasPermission } from './utils' import Boom from '@hapi/boom' import { S3_BUCKET } from '@shared/config' @@ -10,25 +10,20 @@ export const listFile = async ( req: Request, res: Response, _next: NextFunction, - rules: Partial, + rules: Partial, isMetadataRequest = false ): Promise => { const key = getKey(req) - try { - const context = createContext(req) - if (!hasPermission([rules.list, rules.read], context)) { - throw Boom.forbidden() - } - } catch (err) { - console.log( - 'List validation did not apply to a directory (without object context). Will anyway run validation for each object in the list.' - ) + const context = createContext(req) + if (!hasPermission([rules.list, rules.read], context)) { + throw Boom.forbidden() } const params = { Bucket: S3_BUCKET as string, Prefix: key.slice(0, -1) } const list = await s3.listObjectsV2(params).promise() + if (list.Contents) { const headObjectsList = ( await Promise.all( @@ -43,8 +38,16 @@ export const listFile = async ( })) ) ).filter((resource) => hasPermission([rules.list, rules.read], createContext(req, resource))) + if (isMetadataRequest) { - return res.status(200).send(headObjectsList.map((entry) => entry.head)) + return res.status(200).send( + headObjectsList.map((entry) => { + return { + Key: entry.key, + ...entry.head + } + }) + ) } else { const archive = archiver('zip') headObjectsList.forEach((entry) => { diff --git a/src/routes/storage/storage.test.ts b/src/routes/storage/storage.test.ts index cb70fe1..36f9664 100644 --- a/src/routes/storage/storage.test.ts +++ b/src/routes/storage/storage.test.ts @@ -18,8 +18,7 @@ it('should upload a new file', async () => { Metadata: { token, description } } } = await request - .post(`/storage/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) + .post(`/storage/o/user/${getUserId()}/${filePath}`) .query({ description: initialDescription }) .attach('file', filePath) expect(status).toEqual(200) @@ -30,49 +29,53 @@ it('should upload a new file', async () => { it('should update an existing file', async () => { const { status } = await request - .post(`/storage/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) + .post(`/storage/o/user/${getUserId()}/${filePath}`) .attach('file', filePath) expect(status).toEqual(200) }) it('should not update an hypothetical file of another hypothetical user', async () => { const { status } = await request - .post(`/storage/user/another-user/another-file`) - .set('Authorization', `Bearer ${account.token}`) + .post(`/storage/o/user/another-user/another-file`) .attach('file', filePath) expect(status).toEqual(403) }) -it('should get file from user authentication', async () => { - const { status, text } = await request - .get(`/storage/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) +it('should get file', async () => { + const { status, text } = await request.get(`/storage/o/user/${getUserId()}/${filePath}`) const fileData = await readFile(filePath, 'utf8') expect(status).toEqual(200) expect(text).toEqual(fileData) }) -it('should get file from the token stored in the file metadata', async () => { - const { status, text } = await request - .get(`/storage/user/${getUserId()}/${filePath}`) - .query({ token: fileToken }) - const fileData = await readFile(filePath, 'utf8') - expect(status).toEqual(200) - expect(text).toEqual(fileData) -}) +describe('Tests as an unauthenticated user', () => { + beforeAll(async () => { + await request.post(`/auth/logout`) + }) -it('should not get file without authentication nor token', async () => { - const { status } = await request.get(`/storage/user/${getUserId()}/${filePath}`) - expect(status).toEqual(403) -}) + afterAll(async () => { + await request.post('/auth/login').send({ email: account.email, password: account.password }) + }) -// TODO attempt to get the file from another authenticated user + it('should get file from the token stored in the file metadata while unauthenticated', async () => { + const { status, text } = await request + .get(`/storage/o/user/${getUserId()}/${filePath}`) + .query({ token: fileToken }) + const fileData = await readFile(filePath, 'utf8') + expect(status).toEqual(200) + expect(text).toEqual(fileData) + }) + + it('should not get file without authentication nor token', async () => { + const { status } = await request.get(`/storage/o/user/${getUserId()}/${filePath}`) + expect(status).toEqual(403) + }) + // TODO attempt to get the file from another authenticated user +}) it(`should update an existing file's metadata`, async () => { const { status } = await request - .post(`/storage/meta/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) + .post(`/storage/m/user/${getUserId()}/${filePath}`) .query({ description: newDescription }) expect(status).toEqual(200) }) @@ -83,44 +86,34 @@ it('should get file metadata', async () => { body: { Metadata: { filename, description } } - } = await request - .get(`/storage/meta/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) + } = await request.get(`/storage/m/user/${getUserId()}/${filePath}`) expect(status).toEqual(200) expect(filename).toEqual(filePath) expect(description).toEqual(newDescription) }) it('should get the headers of all the user files', async () => { - const { status, body } = await request - .get(`/storage/meta/user/${getUserId()}/`) - .set('Authorization', `Bearer ${account.token}`) + const { status, body } = await request.get(`/storage/m/user/${getUserId()}/`) expect(status).toEqual(200) expect(body).toBeArrayOfSize(1) }) it('should get a zip that contains all user files', async () => { - const { status, text } = await request - .get(`/storage/user/${getUserId()}/`) - .set('Authorization', `Bearer ${account.token}`) + const { status, text } = await request.get(`/storage/m/user/${getUserId()}/`) expect(status).toEqual(200) expect(text).toBeTruthy() // TODO unzip and compare the file(s) }) it('should delete file metadata', async () => { - const { status } = await request - .delete(`/storage/meta/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) + const { status } = await request.delete(`/storage/m/user/${getUserId()}/${filePath}`) expect(status).toEqual(204) const { status: getStatus, body: { Metadata: { filename, token, description } } - } = await request - .get(`/storage/meta/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) + } = await request.get(`/storage/m/user/${getUserId()}/${filePath}`) expect(getStatus).toEqual(200) expect(filename).toBeUndefined() expect(token).toBeString() @@ -128,8 +121,6 @@ it('should delete file metadata', async () => { }) it('should delete file', async () => { - const { status } = await request - .delete(`/storage/user/${getUserId()}/${filePath}`) - .set('Authorization', `Bearer ${account.token}`) + const { status } = await request.delete(`/storage/m/user/${getUserId()}/${filePath}`) expect(status).toEqual(204) }) diff --git a/src/routes/storage/upload.ts b/src/routes/storage/upload.ts index 5be2989..c7f0339 100644 --- a/src/routes/storage/upload.ts +++ b/src/routes/storage/upload.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, Response } from 'express' import { v4 as uuidv4 } from 'uuid' import { - StoragePermissions, + PathConfig, createContext, generateMetadata, getHeadObject, @@ -19,7 +19,7 @@ export const uploadFile = async ( req: Request, res: Response, _next: NextFunction, - rules: Partial, + rules: Partial, isMetadataRequest = false, metadata: object = {} ): Promise => { @@ -56,6 +56,7 @@ export const uploadFile = async ( try { await s3.upload(upload_params).promise() } catch (err) { + console.error(err) throw Boom.badImplementation('Impossible to create or update the object.') } } else if (!isNew) { diff --git a/src/routes/storage/utils.ts b/src/routes/storage/utils.ts index 325bda6..8a6612b 100644 --- a/src/routes/storage/utils.ts +++ b/src/routes/storage/utils.ts @@ -1,4 +1,3 @@ -import { getClaims } from '@shared/jwt' import safeEval, { FunctionFactory } from 'notevil' import { v4 as uuidv4 } from 'uuid' @@ -10,10 +9,12 @@ import fs from 'fs' import path from 'path' import { s3 } from '@shared/s3' import yaml from 'js-yaml' -import { Claims } from '@shared/types' +import { PermissionVariables } from '@shared/types' +import { getPermissionVariablesFromCookie } from '@shared/helpers' -export const META_PREFIX = '/meta' -export interface StoragePermissions { +export const OBJECT_PREFIX = '/o' +export const META_PREFIX = '/m' +export interface PathConfig { read: string write: string get: string @@ -21,14 +22,13 @@ export interface StoragePermissions { create: string update: string delete: string + metadata?: { [key: string]: string } } interface StorageRules { functions?: { [key: string]: string | { params: string[]; code: string } } paths: { - [key: string]: Partial & { - meta: Partial & { values?: { [key: string]: string } } - } + [key: string]: Partial } } @@ -37,11 +37,11 @@ interface StorageRequest { query: unknown method: string params?: string - auth?: Claims + auth?: PermissionVariables } export const containsSomeRule = ( - rulesDefinition: Partial = {}, + rulesDefinition: Partial = {}, rules: (string | undefined)[] ): boolean => Object.keys(rulesDefinition).some((rule) => rules.includes(rule)) @@ -86,10 +86,11 @@ export const createContext = ( ): object => { let auth try { - auth = getClaims(req.headers.authorization) - } catch { + auth = getPermissionVariablesFromCookie(req) + } catch (err) { auth = undefined } + const variables: StorageContext = { request: { path: req.path, @@ -100,12 +101,15 @@ export const createContext = ( ...req.params, resource: s3HeadObject } + const functions = storageFunctions(variables) + return { ...functions, ...variables } } -export const hasPermission = (rules: (string | undefined)[], context: object): boolean => - rules.some((rule) => rule && !!safeEval(rule, context)) +export const hasPermission = (rules: (string | undefined)[], context: object): boolean => { + return rules.some((rule) => rule && !!safeEval(rule, context)) +} export const generateMetadata = (metadataParams: object, context: object): object => Object.entries(metadataParams).reduce<{ [key: string]: unknown }>((aggr, [key, jsCode]) => { diff --git a/src/shared/helpers.ts b/src/shared/helpers.ts index bfe90c1..cf4dc7f 100644 --- a/src/shared/helpers.ts +++ b/src/shared/helpers.ts @@ -1,4 +1,4 @@ -import { HIBP_ENABLE } from './config' +import { HIBP_ENABLE, COOKIE_SECRET } from './config' import { NextFunction, Request, Response } from 'express' import { rotateTicket as rotateTicketQuery, @@ -13,7 +13,7 @@ import bcrypt from 'bcryptjs' import { pwnedPassword } from 'hibp' import { request } from './request' import { v4 as uuidv4 } from 'uuid' -import { AccountData, QueryAccountData } from './types' +import { AccountData, QueryAccountData, PermissionVariables } from './types' /** * Create QR code. @@ -112,3 +112,9 @@ export const rotateTicket = async (ticket: string): Promise => { }) return new_ticket } + +export const getPermissionVariablesFromCookie = (req: Request): PermissionVariables => { + const { permission_variables } = COOKIE_SECRET ? req.signedCookies : req.cookies + if (!permission_variables) throw Boom.unauthorized() + return JSON.parse(permission_variables) +} diff --git a/src/shared/jwt.ts b/src/shared/jwt.ts index 85ef7d5..56aaab1 100644 --- a/src/shared/jwt.ts +++ b/src/shared/jwt.ts @@ -7,7 +7,8 @@ import { JWT_CLAIMS_NAMESPACE, JWT_REFRESH_EXPIRES_IN, DEFAULT_USER_ROLE, - DEFAULT_ANONYMOUS_ROLE + DEFAULT_ANONYMOUS_ROLE, + JWT_CUSTOM_FIELDS } from './config' import { JWK, JWKS, JWT } from 'jose' @@ -20,6 +21,12 @@ import { v4 as uuidv4 } from 'uuid' import kebabCase from 'lodash.kebabcase' import { Claims, Token, AccountData, ClaimValueType } from './types' +interface InsertRefreshTokenData { + insert_auth_refresh_tokens_one: { + account: AccountData + } +} + const RSA_TYPES = ['RS256', 'RS384', 'RS512'] const SHA_TYPES = ['HS256', 'HS384', 'HS512'] @@ -58,6 +65,40 @@ if (RSA_TYPES.includes(JWT_ALGORITHM)) { export const newJwtExpiry = JWT_EXPIRES_IN * 60 * 1000 +/** + * Create an object that contains all the permission variables of the user, + * i.e. user-id, allowed-roles, default-role and the kebab-cased columns + * of the public.tables columns defined in JWT_CUSTOM_FIELDS + * @param jwt if true, add a 'x-hasura-' prefix to the property names, and stringifies the values (required by Hasura) + */ +export function generatePermissionVariables( + { default_role, account_roles = [], user }: AccountData, + jwt = false +): { [key: string]: ClaimValueType } { + const prefix = jwt ? 'x-hasura-' : '' + const role = user.is_anonymous ? DEFAULT_ANONYMOUS_ROLE : default_role || DEFAULT_USER_ROLE + const accountRoles = account_roles.map(({ role: roleName }) => roleName) + + if (!accountRoles.includes(role)) { + accountRoles.push(role) + } + + return { + [`${prefix}user-id`]: user.id, + [`${prefix}allowed-roles`]: accountRoles, + [`${prefix}default-role`]: role, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...JWT_CUSTOM_FIELDS.reduce<{ [key: string]: ClaimValueType }>((aggr: any, cursor) => { + aggr[`${prefix}${kebabCase(cursor)}`] = jwt + ? typeof user[cursor] === 'string' + ? user[cursor] + : JSON.stringify(user[cursor] ?? null) + : user[cursor] + return aggr + }, {}) + } +} + /** * * Creates a JWKS store. Only works with RSA algorithms. Raises an error otherwise * @returns JWKS store @@ -111,7 +152,11 @@ export function newRefreshExpiry(): number { * @param res Express Response * @param refresh_token Refresh token to be set */ -export const setRefreshTokenAsCookie = (res: Response, refresh_token: string): void => { +export const setCookie = ( + res: Response, + refresh_token: string, + permission_variables: string +): void => { // converting JWT_REFRESH_EXPIRES_IN from minutes to milliseconds const maxAge = JWT_REFRESH_EXPIRES_IN * 60 * 1000 @@ -121,6 +166,13 @@ export const setRefreshTokenAsCookie = (res: Response, refresh_token: string): v maxAge, signed: Boolean(COOKIE_SECRET) }) + + // set permission variables cookie + res.cookie('permission_variables', permission_variables, { + httpOnly: true, + maxAge, + signed: Boolean(COOKIE_SECRET) + }) } /** @@ -137,46 +189,26 @@ export const setRefreshToken = async ( if (!refresh_token) { refresh_token = uuidv4() } - await request(insertRefreshToken, { + + const insert_account_data = (await request(insertRefreshToken, { refresh_token_data: { account_id: accountId, refresh_token, expires_at: new Date(newRefreshExpiry()) } - }) + })) as InsertRefreshTokenData - setRefreshTokenAsCookie(res, refresh_token) + const { account } = insert_account_data.insert_auth_refresh_tokens_one + + const permission_variables = JSON.stringify(generatePermissionVariables(account)) + + setCookie(res, refresh_token, permission_variables) } /** * Create JWT token. - * @param id Required v4 UUID string. - * @param defaultRole Defaults to "user". - * @param roles Defaults to ["user"]. */ -export function createHasuraJwt({ default_role, account_roles = [], user }: AccountData): string { - const role = user.is_anonymous ? DEFAULT_ANONYMOUS_ROLE : default_role || DEFAULT_USER_ROLE - const accountRoles = account_roles.map(({ role: roleName }) => roleName) - - if (!accountRoles.includes(role)) { - accountRoles.push(role) - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, ...customFields } = user - return sign({ - [JWT_CLAIMS_NAMESPACE]: { - 'x-hasura-user-id': id, - 'x-hasura-allowed-roles': accountRoles, - 'x-hasura-default-role': role, - // Add custom fields based on the user fields fetched from the GQL query - ...Object.entries(customFields).reduce<{ [k: string]: ClaimValueType }>( - (aggr, [key, value]) => ({ - ...aggr, - [`x-hasura-${kebabCase(key)}`]: - typeof value === 'string' ? value : JSON.stringify(value ?? null) - }), - {} - ) - } +export const createHasuraJwt = (accountData: AccountData): string => + sign({ + [JWT_CLAIMS_NAMESPACE]: generatePermissionVariables(accountData, true) }) -} diff --git a/src/shared/queries.ts b/src/shared/queries.ts index c48e0f0..3cfb538 100644 --- a/src/shared/queries.ts +++ b/src/shared/queries.ts @@ -97,10 +97,13 @@ export const selectAccountByTicket = gql` export const insertRefreshToken = gql` mutation($refresh_token_data: auth_refresh_tokens_insert_input!) { - insert_auth_refresh_tokens(objects: [$refresh_token_data]) { - affected_rows + insert_auth_refresh_tokens_one(object: $refresh_token_data) { + account { + ...accountFragment + } } } + ${accountFragment} ` export const selectRefreshToken = gql` diff --git a/src/shared/types.ts b/src/shared/types.ts index 71893d3..19ee9d4 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -20,6 +20,16 @@ export interface Claims { [key: string]: ClaimValueType } +/** + * PermissionVariables interface. + */ +export interface PermissionVariables { + 'user-id': string + 'default-role': string + 'allowed-roles': string[] + [key: string]: ClaimValueType +} + /** * Token interface. */ diff --git a/src/test/test-utils.ts b/src/test/test-utils.ts index c72f24f..578fd5b 100644 --- a/src/test/test-utils.ts +++ b/src/test/test-utils.ts @@ -95,7 +95,7 @@ export const registerAccount = async (agent: SuperTest): Promise => { // * Delete the account - await agent.post('/auth/account/delete').set('Authorization', `Bearer ${account.token}`) + await agent.post('/auth/delete') // * Remove any message sent to this account await deleteEmailsOfAccount(account.email) } diff --git a/yarn.lock b/yarn.lock index d14c792..a08088d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5372,10 +5372,9 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" -hasura-cli@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hasura-cli/-/hasura-cli-1.2.0.tgz#a1ca61266cbc57141ce4b1d18b230b24b4b32106" - integrity sha512-WIpiELAP1rcOF7F5yIRUiWzfF3E6j6mRKxCgc97+OU/X6NiNqTdGYU6FwzEJQ45Iw0AddBPOiwgZFvynN8zUAA== +hasura-cli@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/hasura-cli/-/hasura-cli-1.2.1.tgz#f25d94d3c10b074ae41346b58e9fdee1529b7bfe" dependencies: axios "^0.19.0" chalk "^2.4.2"