From a584fc91b5e7cb99bef31097925749fdf242348f Mon Sep 17 00:00:00 2001 From: Akhil Reddy Date: Fri, 20 Dec 2024 13:17:53 +0530 Subject: [PATCH] Add authorization --- src/auth.ts | 43 ++++++++++++++++++++- src/organization/organization.controller.ts | 20 ++++------ src/organization/organization.route.ts | 3 ++ src/organization/organization.schema.ts | 6 ++- src/organization/organization.service.ts | 10 +++-- src/server.ts | 11 ++---- src/tenant/{tenant.ts => tenant.schema.ts} | 0 src/tokens/token.controller.ts | 14 +++---- src/tokens/token.route.ts | 8 +++- src/tokens/token.schema.ts | 6 ++- src/tokens/token.service.ts | 3 +- src/user/user.controller.ts | 19 ++++----- src/user/user.route.ts | 4 ++ src/user/user.schema.ts | 7 +++- src/user/user.service.ts | 10 +++-- src/utils/claims.ts | 6 +-- 16 files changed, 112 insertions(+), 58 deletions(-) rename src/tenant/{tenant.ts => tenant.schema.ts} (100%) diff --git a/src/auth.ts b/src/auth.ts index b16e59b..6a50580 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -4,10 +4,25 @@ import { getToken } from "./tokens/token.service"; import { Claim } from "./utils/claims"; export type AuthenticatedUser = { - userId?: String; + userId?: string; + tenantId: string; claims: Array; }; +declare module "fastify" { + export interface FastifyRequest { + user: AuthenticatedUser; + } + + export interface FastifyInstance { + authorize: (req: FastifyRequest, res: FastifyReply) => Promise; + } + + export interface FastifyContextConfig { + requiredClaims: Claim[]; + } +} + export async function authHandler(req: FastifyRequest, res: FastifyReply) { if (!req.headers.authorization) return res.code(401).send(); @@ -21,6 +36,32 @@ export async function authHandler(req: FastifyRequest, res: FastifyReply) { if (!valid) return res.code(401).send({ error: "invalid token" }); req.user = { + tenantId: tokenInDb.tenantId, claims: tokenInDb.claims as Array, }; } + +export function hasValidClaims( + user: AuthenticatedUser, + requiredClaims: Claim[] +): boolean { + let isValid = true; + + for (const claim of requiredClaims) { + if (!user.claims.includes(claim)) { + isValid = false; + break; + } + } + + return isValid; +} + +export async function authorize(req: FastifyRequest, res: FastifyReply) { + const { requiredClaims } = req.routeOptions.config; + const authUser = req.user; + if (!hasValidClaims(authUser, requiredClaims)) + return res + .code(401) + .send({ error: "Missing permissions", params: requiredClaims }); +} diff --git a/src/organization/organization.controller.ts b/src/organization/organization.controller.ts index 92ef61e..ca109a5 100644 --- a/src/organization/organization.controller.ts +++ b/src/organization/organization.controller.ts @@ -2,28 +2,24 @@ import { FastifyRequest, FastifyReply } from "fastify"; import { CreateOrgInput } from "./organization.schema"; import { createOrg, getOrg } from "./organization.service"; -export async function createOrgHandler( - req: FastifyRequest<{ Body: CreateOrgInput }>, - res: FastifyReply -) { - const input = req.body; +export async function createOrgHandler(req: FastifyRequest, res: FastifyReply) { + const input = req.body as CreateOrgInput; try { - const org = await createOrg(input); + const authUser = req.user; + const org = await createOrg(input, authUser.tenantId); return res.code(201).send(org); } catch (err) { return err; } } -export async function getOrgHandler( - req: FastifyRequest<{ Params: { orgId: string } }>, - res: FastifyReply -) { - const { orgId } = req.params; +export async function getOrgHandler(req: FastifyRequest, res: FastifyReply) { + const { orgId } = req.params as { orgId: string }; try { - const org = await getOrg(orgId); + const authUser = req.user; + const org = await getOrg(orgId, authUser.tenantId); if (org === null) return res.code(404).send({ error: "resource not found" }); diff --git a/src/organization/organization.route.ts b/src/organization/organization.route.ts index 7bf2a69..c373e1d 100644 --- a/src/organization/organization.route.ts +++ b/src/organization/organization.route.ts @@ -12,6 +12,8 @@ export default function organizationRoutes(fastify: FastifyInstance) { 201: $org("createOrgResponse"), }, }, + config: { requiredClaims: ["org:write"] }, + preHandler: [fastify.authorize], }, createOrgHandler ); @@ -27,6 +29,7 @@ export default function organizationRoutes(fastify: FastifyInstance) { }, }, }, + config: { requiredClaims: ["org:read"] }, }, getOrgHandler ); diff --git a/src/organization/organization.schema.ts b/src/organization/organization.schema.ts index b7563a0..63f3d07 100644 --- a/src/organization/organization.schema.ts +++ b/src/organization/organization.schema.ts @@ -5,7 +5,10 @@ import { z } from "zod"; export const orgModel = mongoose.model( "organization", new mongoose.Schema({ - tenantId: String, + tenantId: { + type: String, + required: true, + }, pid: { type: String, unique: true, @@ -13,7 +16,6 @@ export const orgModel = mongoose.model( name: String, domain: { type: String, - unique: true, }, avatar: String, type: String, diff --git a/src/organization/organization.service.ts b/src/organization/organization.service.ts index 5cff0e6..f29c09c 100644 --- a/src/organization/organization.service.ts +++ b/src/organization/organization.service.ts @@ -1,9 +1,9 @@ import { generateId } from "../utils/id"; import { CreateOrgInput, orgModel } from "./organization.schema"; -export async function createOrg(input: CreateOrgInput) { +export async function createOrg(input: CreateOrgInput, tenantId: string) { const org = await orgModel.create({ - tenantId: "abc", + tenantId: tenantId, pid: generateId(), createdAt: new Date(), ...input, @@ -12,6 +12,8 @@ export async function createOrg(input: CreateOrgInput) { return org; } -export async function getOrg(orgId: string) { - return await orgModel.findOne({ pid: orgId }); +export async function getOrg(orgId: string, tenantId: string) { + return await orgModel.findOne({ + $and: [{ tenantId: tenantId }, { pid: orgId }], + }); } diff --git a/src/server.ts b/src/server.ts index 93f2678..ce633de 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,13 +6,7 @@ import { userSchemas } from "./user/user.schema"; import { orgSchemas } from "./organization/organization.schema"; import { tokenSchemas } from "./tokens/token.schema"; import { errorHandler } from "./utils/errors"; -import { AuthenticatedUser, authHandler } from "./auth"; - -declare module "fastify" { - export interface FastifyRequest { - user: AuthenticatedUser; - } -} +import { authHandler, authorize } from "./auth"; const app = fastify({ logger: true }); @@ -20,9 +14,10 @@ app.get("/health", (req, res) => { return { status: "OK" }; }); -app.register(routes, { prefix: "/api/v1" }); +app.decorate("authorize", authorize); app.setErrorHandler(errorHandler); app.addHook("onRequest", authHandler); +app.register(routes, { prefix: "/api/v1" }); for (const schema of [...userSchemas, ...orgSchemas, ...tokenSchemas]) { app.addSchema(schema); diff --git a/src/tenant/tenant.ts b/src/tenant/tenant.schema.ts similarity index 100% rename from src/tenant/tenant.ts rename to src/tenant/tenant.schema.ts diff --git a/src/tokens/token.controller.ts b/src/tokens/token.controller.ts index 5604eb5..0008724 100644 --- a/src/tokens/token.controller.ts +++ b/src/tokens/token.controller.ts @@ -3,24 +3,22 @@ import { CreateTokenInput } from "./token.schema"; import { createToken, getToken } from "./token.service"; export async function createTokenHandler( - req: FastifyRequest<{ Body: CreateTokenInput }>, + req: FastifyRequest, res: FastifyReply ) { - const input = req.body; + const input = req.body as CreateTokenInput; try { - const result = await createToken(input); + const authUser = req.user; + const result = await createToken(input, authUser.tenantId); return res.code(201).send(result); } catch (err) { return err; } } -export async function getTokenHandler( - req: FastifyRequest<{ Params: { tokenId: string } }>, - res: FastifyReply -) { - const { tokenId } = req.params; +export async function getTokenHandler(req: FastifyRequest, res: FastifyReply) { + const { tokenId } = req.params as { tokenId: string }; try { const token = await getToken(tokenId); diff --git a/src/tokens/token.route.ts b/src/tokens/token.route.ts index 9bacf10..e8c2e68 100644 --- a/src/tokens/token.route.ts +++ b/src/tokens/token.route.ts @@ -12,13 +12,19 @@ export async function tokenRoutes(fastify: FastifyInstance) { 201: $token("createTokenResponse"), }, }, + config: { requiredClaims: ["token:write"] }, + preHandler: [fastify.authorize], }, createTokenHandler ); fastify.get( "/:tokenId", - { schema: { response: { 200: $token("getTokenResponse") } } }, + { + schema: { response: { 200: $token("getTokenResponse") } }, + config: { requiredClaims: ["token:read"] }, + preHandler: [fastify.authorize], + }, getTokenHandler ); } diff --git a/src/tokens/token.schema.ts b/src/tokens/token.schema.ts index 4fb8677..3da8a8c 100644 --- a/src/tokens/token.schema.ts +++ b/src/tokens/token.schema.ts @@ -1,12 +1,14 @@ import { buildJsonSchemas } from "fastify-zod"; import mongoose from "mongoose"; import { z } from "zod"; -import { Claim } from "../utils/claims"; export const tokenModel = mongoose.model( "token", new mongoose.Schema({ - tenantId: String, + tenantId: { + type: String, + required: true, + }, pid: { type: String, unique: true, diff --git a/src/tokens/token.service.ts b/src/tokens/token.service.ts index abd051f..9e1e720 100644 --- a/src/tokens/token.service.ts +++ b/src/tokens/token.service.ts @@ -2,12 +2,13 @@ import bcrypt from "bcrypt"; import { generateId, generateToken } from "../utils/id"; import { CreateTokenInput, tokenModel } from "./token.schema"; -export async function createToken(input: CreateTokenInput) { +export async function createToken(input: CreateTokenInput, tenantId: string) { const tokenId = generateId(); const newToken = await generateToken(); const tokenHash = await bcrypt.hash(newToken, 10); const tokenInDb = await tokenModel.create({ + tenantId: tenantId, pid: tokenId, hash: tokenHash, createdAt: new Date(), diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index fb889c7..9be91d5 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -3,29 +3,26 @@ import { createUser, getUser } from "./user.service"; import { CreateUserInput } from "./user.schema"; export async function createUserHandler( - req: FastifyRequest<{ - Body: CreateUserInput; - }>, + req: FastifyRequest, res: FastifyReply ) { - const body = req.body; + const body = req.body as CreateUserInput; try { - const user = await createUser(body); + const authUser = req.user; + const user = await createUser(body, authUser.tenantId); return res.code(201).send(user); } catch (err) { return err; } } -export async function getUserHandler( - req: FastifyRequest<{ Params: { userId: string } }>, - res: FastifyReply -) { - const { userId } = req.params; +export async function getUserHandler(req: FastifyRequest, res: FastifyReply) { + const { userId } = req.params as { userId: string }; try { - const user = await getUser(userId); + const authUser = req.user; + const user = await getUser(userId, authUser.tenantId); if (user == null) return res.code(404).send({ error: "resource not found" }); diff --git a/src/user/user.route.ts b/src/user/user.route.ts index 86511b3..4de15b1 100644 --- a/src/user/user.route.ts +++ b/src/user/user.route.ts @@ -12,6 +12,8 @@ export default async function userRoutes(fastify: FastifyInstance) { 201: $user("createUserResponse"), }, }, + config: { requiredClaims: ["user:write"] }, + preHandler: [fastify.authorize], }, createUserHandler ); @@ -24,6 +26,8 @@ export default async function userRoutes(fastify: FastifyInstance) { 200: $user("createUserResponse"), }, }, + config: { requiredClaims: ["user:read"] }, + preHandler: [fastify.authorize], }, getUserHandler ); diff --git a/src/user/user.schema.ts b/src/user/user.schema.ts index 0551419..406ae2c 100644 --- a/src/user/user.schema.ts +++ b/src/user/user.schema.ts @@ -5,16 +5,21 @@ import { z } from "zod"; export const userModel = mongoose.model( "user", new mongoose.Schema({ - tenantId: String, + tenantId: { + type: String, + required: true, + }, pid: { type: String, unique: true, + required: true, }, firstName: String, lastName: String, email: { type: String, unique: true, + required: true, }, avatar: String, status: String, diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 6f4f02d..b2c8aae 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -1,9 +1,9 @@ import { generateId } from "../utils/id"; import { CreateUserInput, userModel } from "./user.schema"; -export async function createUser(input: CreateUserInput) { +export async function createUser(input: CreateUserInput, tenantId: string) { const user = await userModel.create({ - tenantId: "abc", + tenantId: tenantId, pid: generateId(), createdAt: new Date(), ...input, @@ -12,7 +12,9 @@ export async function createUser(input: CreateUserInput) { return user; } -export async function getUser(userId: string) { - const user = await userModel.findOne({ pid: userId }); +export async function getUser(userId: string, tenantId: string) { + const user = await userModel.findOne({ + $and: [{ tenantId: tenantId }, { pid: userId }], + }); return user; } diff --git a/src/utils/claims.ts b/src/utils/claims.ts index a7d8882..e374004 100644 --- a/src/utils/claims.ts +++ b/src/utils/claims.ts @@ -10,6 +10,6 @@ export type Claim = | "permit:delete" | "file:upload" | "file:download" - | "api:read" - | "api:write" - | "api:delete"; + | "token:read" + | "token:write" + | "token:delete";