diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index e6ee6cd8..6984421e 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -55,7 +55,8 @@ const App: FC = () => { return { headers: { ...headers, - authorization: token ? `Bearer ${token}` : '' + authorization: token ? `Bearer ${token}` : '', + organization: import.meta.env.VITE_ORGANIZATION_ID || '' } }; }); diff --git a/packages/gateway/src/app.module.ts b/packages/gateway/src/app.module.ts index 9743a07b..dbaa0ab7 100644 --- a/packages/gateway/src/app.module.ts +++ b/packages/gateway/src/app.module.ts @@ -23,6 +23,8 @@ import configuration from './config/configuration'; if (context.req && context.req.headers) { // Copy over authentication request.http!.headers.set('authorization', context.req.headers.authorization); + // Copy over the organization context + request.http!.headers.set('organization', context.req.headers.organization); } } }); diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index 5fd0016b..789113db 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -14,6 +14,7 @@ import { SharedModule } from './shared/shared.module'; import { JwtModule } from './jwt/jwt.module'; import { PermissionModule } from './permission/permission.module'; import { AuthModule } from './auth/auth.module'; +import { UserOrgModule } from './userorg/userorg.module'; @Module({ imports: [ @@ -44,7 +45,8 @@ import { AuthModule } from './auth/auth.module'; SharedModule, JwtModule, PermissionModule, - AuthModule + AuthModule, + UserOrgModule ] }) export class AppModule {} diff --git a/packages/server/src/dataset/dataset.module.ts b/packages/server/src/dataset/dataset.module.ts index 2bdcff3b..f616a624 100644 --- a/packages/server/src/dataset/dataset.module.ts +++ b/packages/server/src/dataset/dataset.module.ts @@ -7,13 +7,17 @@ import { DatasetPipe } from './pipes/dataset.pipe'; import { PermissionModule } from '../permission/permission.module'; import { JwtModule } from '../jwt/jwt.module'; import { ProjectModule } from '../project/project.module'; +import { OrganizationModule } from '../organization/organization.module'; +import { UserOrgModule } from '../userorg/userorg.module'; @Module({ imports: [ MongooseModule.forFeature([{ name: Dataset.name, schema: DatasetSchema }]), forwardRef(() => PermissionModule), JwtModule, - ProjectModule + ProjectModule, + OrganizationModule, + UserOrgModule ], providers: [DatasetResolver, DatasetService, DatasetPipe], exports: [DatasetService, DatasetPipe] diff --git a/packages/server/src/dataset/dataset.resolver.ts b/packages/server/src/dataset/dataset.resolver.ts index 30e79dcf..9cfd387a 100644 --- a/packages/server/src/dataset/dataset.resolver.ts +++ b/packages/server/src/dataset/dataset.resolver.ts @@ -15,9 +15,9 @@ import { DatasetPermissions } from '../permission/permissions/dataset'; import { ProjectPipe } from '../project/pipes/project.pipe'; import { Project } from '../project/project.model'; import { ProjectPermissions } from '../permission/permissions/project'; +import { OrganizationGuard } from '../organization/organization.guard'; -// TODO: Add authentication -@UseGuards(JwtAuthGuard) +@UseGuards(JwtAuthGuard, OrganizationGuard) @Resolver(() => Dataset) export class DatasetResolver { constructor( diff --git a/packages/server/src/entry/entry.module.ts b/packages/server/src/entry/entry.module.ts index 034d2896..04a04546 100644 --- a/packages/server/src/entry/entry.module.ts +++ b/packages/server/src/entry/entry.module.ts @@ -17,6 +17,8 @@ import { PermissionModule } from '../permission/permission.module'; import { JwtModule } from '../jwt/jwt.module'; import { MongooseMiddlewareService } from '../shared/service/mongoose-callback.service'; import { SharedModule } from '../shared/shared.module'; +import { OrganizationModule } from '../organization/organization.module'; +import { UserOrgModule } from '../userorg/userorg.module'; @Module({ imports: [ @@ -42,7 +44,9 @@ import { SharedModule } from '../shared/shared.module'; DatasetModule, GcpModule, PermissionModule, - JwtModule + JwtModule, + OrganizationModule, + UserOrgModule ], providers: [ EntryResolver, diff --git a/packages/server/src/entry/resolvers/entry.resolver.ts b/packages/server/src/entry/resolvers/entry.resolver.ts index 51d0c9c1..1d43c561 100644 --- a/packages/server/src/entry/resolvers/entry.resolver.ts +++ b/packages/server/src/entry/resolvers/entry.resolver.ts @@ -13,8 +13,9 @@ import { TokenContext } from '../../jwt/token.context'; import { OrganizationContext } from '../../organization/organization.context'; import { Organization } from '../../organization/organization.model'; import { EntryPipe } from '../pipes/entry.pipe'; +import { OrganizationGuard } from '../../organization/organization.guard'; -@UseGuards(JwtAuthGuard) +@UseGuards(JwtAuthGuard, OrganizationGuard) @Resolver(() => Entry) export class EntryResolver { constructor( diff --git a/packages/server/src/jwt/jwt.module.ts b/packages/server/src/jwt/jwt.module.ts index 8d326146..e59eac10 100644 --- a/packages/server/src/jwt/jwt.module.ts +++ b/packages/server/src/jwt/jwt.module.ts @@ -4,9 +4,10 @@ import { HttpModule } from '@nestjs/axios'; import { JwtAuthGuard } from './jwt.guard'; import { JwtStrategy } from './jwt.strategy'; import { OrganizationModule } from '../organization/organization.module'; +import { UserOrgModule } from '../userorg/userorg.module'; @Module({ - imports: [HttpModule, forwardRef(() => OrganizationModule)], + imports: [HttpModule, forwardRef(() => OrganizationModule), UserOrgModule], providers: [JwtService, JwtAuthGuard, JwtStrategy], exports: [JwtService] }) diff --git a/packages/server/src/jwt/jwt.strategy.ts b/packages/server/src/jwt/jwt.strategy.ts index 639f3ca2..75f597dc 100644 --- a/packages/server/src/jwt/jwt.strategy.ts +++ b/packages/server/src/jwt/jwt.strategy.ts @@ -1,21 +1,15 @@ -import { Injectable, BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-local'; import { TokenPayload } from './token.dto'; -import { OrganizationService } from '../organization/organization.service'; -import { Organization } from 'src/organization/organization.model'; import { Request } from 'express'; import { ParamsDictionary } from 'express-serve-static-core'; import { ParsedQs } from 'qs'; import { JwtService } from './jwt.service'; -interface JwtStrategyValidate extends TokenPayload { - organization: Organization; -} - @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private readonly organizationService: OrganizationService, private readonly jwtService: JwtService) { + constructor(private readonly jwtService: JwtService) { super(); } @@ -33,11 +27,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { // Validate the token const payload = await this.jwtService.validate(rawToken); if (!payload) { - this.fail({ meessage: 'Invalid Token' }, 400); + this.fail({ message: 'Invalid Token' }, 400); return; } - this.success(await this.validate(payload)); + const result = await this.validate(payload); + this.success(result); } /** @@ -45,16 +40,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) { * queried from the database and not part of the JWT token. This allows * the organization to then be pulled in via the organization context */ - async validate(payload: TokenPayload): Promise { - // TODO: Change out hardcoded project ID - const organization = await this.organizationService.findByProject('fe231d0b-5f01-4e52-9bc1-561e76b1e02d'); - if (!organization) { - throw new BadRequestException('Organization not found'); - } - + async validate(payload: TokenPayload): Promise { return { - ...payload, - organization: organization + ...payload }; } } diff --git a/packages/server/src/organization/organization.context.ts b/packages/server/src/organization/organization.context.ts index 954009d9..68c4aa48 100644 --- a/packages/server/src/organization/organization.context.ts +++ b/packages/server/src/organization/organization.context.ts @@ -3,5 +3,5 @@ import { GqlExecutionContext } from '@nestjs/graphql'; export const OrganizationContext = createParamDecorator((_data: unknown, ctx: ExecutionContext) => { const gqlCtx = GqlExecutionContext.create(ctx); - return gqlCtx.getContext().req.user.organization; + return gqlCtx.getContext().req.organization; }); diff --git a/packages/server/src/organization/organization.guard.ts b/packages/server/src/organization/organization.guard.ts new file mode 100644 index 00000000..84e554f7 --- /dev/null +++ b/packages/server/src/organization/organization.guard.ts @@ -0,0 +1,46 @@ +import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common'; +import { GqlExecutionContext } from '@nestjs/graphql'; +import { OrganizationService } from './organization.service'; +import { UserOrgService } from '../userorg/userorg.service'; + +@Injectable() +export class OrganizationGuard implements CanActivate { + constructor( + private readonly organizationService: OrganizationService, + private readonly userOrgService: UserOrgService + ) {} + + async canActivate(context: ExecutionContext): Promise { + const ctx = GqlExecutionContext.create(context); + + // Check for the organization in the headers + const organizationID = ctx.getContext().req.headers.organization; + if (organizationID == undefined || organizationID == 'undefined') { + return false; + } + if (typeof organizationID !== 'string') { + return false; + } + + // Check if the organization exists + const organization = await this.organizationService.findOne(organizationID); + if (!organization) { + return false; + } + + // Check to see if the user is in the organization + const user = ctx.getContext().req.user; + if (!user) { + return false; + } + const userOrg = await this.userOrgService.userIsInOrg(user.user_id, organizationID); + if (!userOrg) { + return false; + } + + // Add the organization to the request + ctx.getContext().req.organization = organization; + + return true; + } +} diff --git a/packages/server/src/organization/organization.module.ts b/packages/server/src/organization/organization.module.ts index be0c4528..ebcb9e70 100644 --- a/packages/server/src/organization/organization.module.ts +++ b/packages/server/src/organization/organization.module.ts @@ -4,9 +4,10 @@ import { OrganizationService } from './organization.service'; import { MongooseModule } from '@nestjs/mongoose'; import { Organization, OrganizationSchema } from './organization.model'; import { CreateOrganizationPipe } from './pipes/create.pipe'; +import { UserOrgModule } from '../userorg/userorg.module'; @Module({ - imports: [MongooseModule.forFeature([{ name: Organization.name, schema: OrganizationSchema }])], + imports: [MongooseModule.forFeature([{ name: Organization.name, schema: OrganizationSchema }]), UserOrgModule], providers: [OrganizationResolver, OrganizationService, CreateOrganizationPipe], exports: [OrganizationService] }) diff --git a/packages/server/src/permission/permission.module.ts b/packages/server/src/permission/permission.module.ts index bbe82ac0..023db01b 100644 --- a/packages/server/src/permission/permission.module.ts +++ b/packages/server/src/permission/permission.module.ts @@ -10,13 +10,17 @@ import { StudyPermissionResolver } from './resolvers/study.resolver'; import { DatasetPermissionResolver } from './resolvers/dataset.resolver'; import { DatasetModule } from '../dataset/dataset.module'; import { PermissionResolver } from './resolvers/permission.resolver'; +import { OrganizationModule } from '../organization/organization.module'; +import { UserOrgModule } from '../userorg/userorg.module'; @Module({ imports: [ forwardRef(() => ProjectModule), AuthModule, forwardRef(() => StudyModule), - forwardRef(() => DatasetModule) + forwardRef(() => DatasetModule), + OrganizationModule, + UserOrgModule ], providers: [ casbinProvider, diff --git a/packages/server/src/permission/resolvers/permission.resolver.ts b/packages/server/src/permission/resolvers/permission.resolver.ts index 2a17c610..9c5cc10e 100644 --- a/packages/server/src/permission/resolvers/permission.resolver.ts +++ b/packages/server/src/permission/resolvers/permission.resolver.ts @@ -11,8 +11,9 @@ import { OrganizationContext } from '../../organization/organization.context'; import { Organization } from '../../organization/organization.model'; import { UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from '../../jwt/jwt.guard'; +import { OrganizationGuard } from '../../organization/organization.guard'; -@UseGuards(JwtAuthGuard) +@UseGuards(JwtAuthGuard, OrganizationGuard) @Resolver() export class PermissionResolver { constructor( diff --git a/packages/server/src/project/project.module.ts b/packages/server/src/project/project.module.ts index fa584f5d..11c223bc 100644 --- a/packages/server/src/project/project.module.ts +++ b/packages/server/src/project/project.module.ts @@ -8,6 +8,8 @@ import { MongooseMiddlewareService } from 'src/shared/service/mongoose-callback. import { SharedModule } from 'src/shared/shared.module'; import { JwtModule } from '../jwt/jwt.module'; import { PermissionModule } from '../permission/permission.module'; +import { OrganizationModule } from '../organization/organization.module'; +import { UserOrgModule } from '../userorg/userorg.module'; @Module({ imports: [ @@ -29,7 +31,9 @@ import { PermissionModule } from '../permission/permission.module'; } ]), JwtModule, - forwardRef(() => PermissionModule) + forwardRef(() => PermissionModule), + OrganizationModule, + UserOrgModule ], providers: [ProjectResolver, ProjectService, ProjectPipe], exports: [ProjectPipe, ProjectService] diff --git a/packages/server/src/project/project.resolver.ts b/packages/server/src/project/project.resolver.ts index e20c1496..1bce80af 100644 --- a/packages/server/src/project/project.resolver.ts +++ b/packages/server/src/project/project.resolver.ts @@ -12,8 +12,9 @@ import { TokenPayload } from '../jwt/token.dto'; import { CASBIN_PROVIDER } from '../permission/casbin.provider'; import * as casbin from 'casbin'; import { ProjectPermissions } from '../permission/permissions/project'; +import { OrganizationGuard } from '../organization/organization.guard'; -@UseGuards(JwtAuthGuard) +@UseGuards(JwtAuthGuard, OrganizationGuard) @Resolver(() => Project) export class ProjectResolver { constructor( diff --git a/packages/server/src/userorg/userorg.model.ts b/packages/server/src/userorg/userorg.model.ts new file mode 100644 index 00000000..8d3bb8d7 --- /dev/null +++ b/packages/server/src/userorg/userorg.model.ts @@ -0,0 +1,13 @@ +import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; + +@Schema() +export class UserOrg { + @Prop() + user: string; + + @Prop() + org: string; +} + +export type UserOrgDocument = UserOrg & Document; +export const UserOrgSchema = SchemaFactory.createForClass(UserOrg); diff --git a/packages/server/src/userorg/userorg.module.ts b/packages/server/src/userorg/userorg.module.ts new file mode 100644 index 00000000..13337afb --- /dev/null +++ b/packages/server/src/userorg/userorg.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { UserOrgService } from './userorg.service'; +import { UserOrg, UserOrgSchema } from './userorg.model'; +import { MongooseModule } from '@nestjs/mongoose'; +import { UserOrgResolver } from './userorg.resolver'; + +@Module({ + imports: [MongooseModule.forFeature([{ name: UserOrg.name, schema: UserOrgSchema }])], + providers: [UserOrgService, UserOrgResolver], + exports: [UserOrgService] +}) +export class UserOrgModule {} diff --git a/packages/server/src/userorg/userorg.resolver.ts b/packages/server/src/userorg/userorg.resolver.ts new file mode 100644 index 00000000..dac0acf6 --- /dev/null +++ b/packages/server/src/userorg/userorg.resolver.ts @@ -0,0 +1,26 @@ +import { ID, Resolver, Query, Mutation, Args } from '@nestjs/graphql'; +import { UserOrgService } from './userorg.service'; +import { TokenContext } from '../jwt/token.context'; +import { TokenPayload } from '../jwt/token.dto'; +import { JwtAuthGuard } from '../jwt/jwt.guard'; +import { UseGuards } from '@nestjs/common'; + +@Resolver() +export class UserOrgResolver { + constructor(private readonly userOrgService: UserOrgService) {} + + @Query(() => Boolean) + async userIsInOrg(@Args('user') user: string, @Args('org') org: string): Promise { + return this.userOrgService.userIsInOrg(user, org); + } + + @UseGuards(JwtAuthGuard) + @Mutation(() => Boolean) + async addUserToOrg( + @Args('organization', { type: () => ID }) organization: string, + @TokenContext() user: TokenPayload + ): Promise { + await this.userOrgService.create(user.user_id, organization); + return true; + } +} diff --git a/packages/server/src/userorg/userorg.service.ts b/packages/server/src/userorg/userorg.service.ts new file mode 100644 index 00000000..6c17de42 --- /dev/null +++ b/packages/server/src/userorg/userorg.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { UserOrg, UserOrgDocument } from './userorg.model'; + +@Injectable() +export class UserOrgService { + constructor(@InjectModel(UserOrg.name) private userOrgModel: Model) {} + + async create(user: string, org: string): Promise { + const existing = await this.find(user, org); + if (existing) { + return existing; + } + return this.userOrgModel.create({ user, org }); + } + + async findUserForOrg(org: string): Promise { + return this.userOrgModel.find({ org }); + } + + async userIsInOrg(user: string, org: string): Promise { + return !!(await this.find(user, org)); + } + + async find(user: string, org: string): Promise { + return this.userOrgModel.findOne({ user, org }); + } +}