Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ''
}
};
});
Expand Down
2 changes: 2 additions & 0 deletions packages/gateway/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
});
Expand Down
4 changes: 3 additions & 1 deletion packages/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -44,7 +45,8 @@ import { AuthModule } from './auth/auth.module';
SharedModule,
JwtModule,
PermissionModule,
AuthModule
AuthModule,
UserOrgModule
]
})
export class AppModule {}
6 changes: 5 additions & 1 deletion packages/server/src/dataset/dataset.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/dataset/dataset.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/entry/entry.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -42,7 +44,9 @@ import { SharedModule } from '../shared/shared.module';
DatasetModule,
GcpModule,
PermissionModule,
JwtModule
JwtModule,
OrganizationModule,
UserOrgModule
],
providers: [
EntryResolver,
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/entry/resolvers/entry.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/jwt/jwt.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
})
Expand Down
26 changes: 7 additions & 19 deletions packages/server/src/jwt/jwt.strategy.ts
Copy link
Collaborator

@mannyakosah mannyakosah Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you wanna add a TODO comment of what the function "validate" is supposed to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the organization logic shifted, it is actually all it has to do now. It seems funky, but you'll see similar functionality in NestJS's official documentation. The validation is just for additional logic beyond the JWT validation.

Original file line number Diff line number Diff line change
@@ -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();
}

Expand All @@ -33,28 +27,22 @@ 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);
}

/**
* Need to add the organization at this step since the organization is
* 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<JwtStrategyValidate> {
// 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<TokenPayload> {
return {
...payload,
organization: organization
...payload
};
}
}
2 changes: 1 addition & 1 deletion packages/server/src/organization/organization.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
46 changes: 46 additions & 0 deletions packages/server/src/organization/organization.guard.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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;
}
}
3 changes: 2 additions & 1 deletion packages/server/src/organization/organization.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
})
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/permission/permission.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/project/project.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -29,7 +31,9 @@ import { PermissionModule } from '../permission/permission.module';
}
]),
JwtModule,
forwardRef(() => PermissionModule)
forwardRef(() => PermissionModule),
OrganizationModule,
UserOrgModule
],
providers: [ProjectResolver, ProjectService, ProjectPipe],
exports: [ProjectPipe, ProjectService]
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/project/project.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
13 changes: 13 additions & 0 deletions packages/server/src/userorg/userorg.model.ts
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 12 additions & 0 deletions packages/server/src/userorg/userorg.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
26 changes: 26 additions & 0 deletions packages/server/src/userorg/userorg.resolver.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
return this.userOrgService.userIsInOrg(user, org);
}

@UseGuards(JwtAuthGuard)
@Mutation(() => Boolean)
async addUserToOrg(
@Args('organization', { type: () => ID }) organization: string,
@TokenContext() user: TokenPayload
): Promise<boolean> {
await this.userOrgService.create(user.user_id, organization);
return true;
}
}
29 changes: 29 additions & 0 deletions packages/server/src/userorg/userorg.service.ts
Original file line number Diff line number Diff line change
@@ -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<UserOrgDocument>) {}

async create(user: string, org: string): Promise<UserOrg> {
const existing = await this.find(user, org);
if (existing) {
return existing;
}
return this.userOrgModel.create({ user, org });
}

async findUserForOrg(org: string): Promise<UserOrg[]> {
return this.userOrgModel.find({ org });
}

async userIsInOrg(user: string, org: string): Promise<boolean> {
return !!(await this.find(user, org));
}

async find(user: string, org: string): Promise<UserOrg | null> {
return this.userOrgModel.findOne({ user, org });
}
}