Scope server with workspace (#157)

* Rename User to AuthUser to avoid naming conflict with user business entity

* Prevent query by workspace in graphql

* Make full user and workspace object available in graphql resolvers

* Add Seed to create companies and people accross two workspace

* Check workspace on all entities findMany, find, create, update)
This commit is contained in:
Charles Bochet
2023-05-30 20:40:04 +02:00
committed by GitHub
parent 0f9c6dede7
commit 3674365e6f
47 changed files with 380 additions and 483 deletions

View File

@ -8,8 +8,9 @@ import {
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Request, Response } from 'express';
import { Response } from 'express';
import { AuthService } from './services/auth.service';
import { GoogleRequest } from './strategies/google.auth.strategy';
@Controller('auth/google')
export class GoogleAuthController {
@ -24,10 +25,8 @@ export class GoogleAuthController {
@Get('redirect')
@UseGuards(AuthGuard('google'))
async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
const user = await this.authService.upsertUser(
req.user as { firstName: string; lastName: string; email: string },
);
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
const user = await this.authService.upsertUser(req.user);
if (!user) {
throw new HttpException(

View File

@ -0,0 +1,85 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Request } from 'express';
import { PrismaService } from 'src/database/prisma.service';
type OperationEntity = {
operation?: string;
entity?: string;
};
@Injectable()
export class CheckWorkspaceOwnership implements CanActivate {
constructor(private prismaService: PrismaService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().req;
const { operation, entity } = this.fetchOperationAndEntity(request);
const variables = request.body.variables;
const workspace = await request.workspace;
if (!entity || !operation) {
return false;
}
if (operation === 'updateOne') {
const object = await this.prismaService[entity].findUniqueOrThrow({
where: { id: variables.id },
});
if (!object) {
throw new HttpException(
{ reason: 'Record not found' },
HttpStatus.NOT_FOUND,
);
}
if (object.workspaceId !== workspace.id) {
throw new HttpException(
{ reason: 'Record not found' },
HttpStatus.NOT_FOUND,
);
}
return true;
}
if (operation === 'deleteMany') {
// TODO: write this logic
return true;
}
if (operation === 'findMany') {
return true;
}
if (operation === 'createOne') {
return true;
}
return false;
}
private fetchOperationAndEntity(request: Request): OperationEntity {
if (!request.body.operationName) {
return { operation: undefined, entity: undefined };
}
const regex =
/(updateOne|deleteMany|createOne|findMany)(Person|Company|User)/i;
const match = request.body.query.match(regex);
if (match) {
return {
operation: match[1],
entity: match[2].toLowerCase(),
};
}
return { operation: undefined, entity: undefined };
}
}

View File

@ -1,6 +1,8 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
@ -8,12 +10,15 @@ import { JwtService } from '@nestjs/jwt';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from 'src/database/prisma.service';
import { JwtPayload } from '../strategies/jwt.auth.strategy';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private prismaService: PrismaService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
@ -24,10 +29,34 @@ export class JwtAuthGuard implements CanActivate {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get('JWT_SECRET'),
});
request['user'] = payload;
const user = this.prismaService.user.findUniqueOrThrow({
where: { id: payload.userId },
});
if (!user) {
throw new HttpException(
{ reason: 'User does not exist' },
HttpStatus.FORBIDDEN,
);
}
const workspace = this.prismaService.workspace.findUniqueOrThrow({
where: { id: payload.workspaceId },
});
if (!workspace) {
throw new HttpException(
{ reason: 'Workspace does not exist' },
HttpStatus.FORBIDDEN,
);
}
request.user = user;
request.workspace = workspace;
} catch (exception) {
throw new UnauthorizedException();
}

View File

@ -8,6 +8,12 @@ import { RefreshTokenRepository } from 'src/entities/refresh-token/refresh-token
import { v4 } from 'uuid';
import { RefreshToken, User } from '@prisma/client';
export type UserPayload = {
firstName: string;
lastName: string;
email: string;
};
@Injectable()
export class AuthService {
constructor(
@ -18,11 +24,7 @@ export class AuthService {
private refreshTokenRepository: RefreshTokenRepository,
) {}
async upsertUser(rawUser: {
firstName: string;
lastName: string;
email: string;
}) {
async upsertUser(rawUser: UserPayload) {
if (!rawUser.email) {
throw new HttpException(
{ reason: 'Email is missing' },

View File

@ -3,6 +3,11 @@ import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
export type GoogleRequest = Request & {
user: { firstName: string; lastName: string; email: string };
};
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {

View File

@ -24,7 +24,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
});
}
async validate(payload: JwtPayload) {
async validate(payload: JwtPayload): Promise<JwtPayload> {
return { userId: payload.userId, workspaceId: payload.workspaceId };
}
}