Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/core/auth/auth.constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Role {
Admin = 'administrator',
User = 'Topcoder User',
}

Expand Down
23 changes: 8 additions & 15 deletions src/core/auth/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Logger } from 'src/shared/global';
import { Request } from 'express';
import { decode } from './guards.utils';

@Injectable()
export class AuthGuard implements CanActivate {
private readonly logger = new Logger(AuthGuard.name);

constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
this.logger.log('AuthGuard canActivate called...');
// Check if the route is marked as public...

return true;
export const authGuard = async (req: Request) => {
if (!(await decode(req.headers.authorization ?? ''))) {
return false;
}
}

return true;
};
27 changes: 27 additions & 0 deletions src/core/auth/guards/guards.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as jwt from 'jsonwebtoken';
// import { UnauthorizedException } from '@nestjs/common';
import { Logger } from 'src/shared/global';
import { getSigningKey } from '../jwt';

const logger = new Logger('guards.utils()');

export const decode = async (authHeader: string) => {
const [type, idToken] = authHeader?.split(' ') ?? [];

if (type !== 'Bearer' || !idToken) {
return false;
// throw new UnauthorizedException('Missing Authorization header!');
}

let decoded: jwt.JwtPayload | string;
try {
const signingKey = await getSigningKey(idToken);
decoded = jwt.verify(idToken, signingKey);
} catch (error) {
logger.error('Error verifying JWT', error);
return false;
// throw new UnauthorizedException('Invalid or expired JWT!');
}

return decoded;
};
2 changes: 2 additions & 0 deletions src/core/auth/guards/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './auth.guard';
export * from './m2m-scope.guard';
export * from './role.guard';
19 changes: 19 additions & 0 deletions src/core/auth/guards/m2m-scope.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Request } from 'express';
import { decode } from './guards.utils';
import { JwtPayload } from 'jsonwebtoken';
import { M2mScope } from '../auth.constants';

export const checkM2MScope =
(...requiredM2mScopes: M2mScope[]) =>
async (req: Request) => {
const decodedAuth = await decode(req.headers.authorization ?? '');

const authorizedScopes = ((decodedAuth as JwtPayload).scope ?? '').split(
' ',
);
if (!requiredM2mScopes.some((scope) => authorizedScopes.includes(scope))) {
return false;
}

return true;
};
23 changes: 23 additions & 0 deletions src/core/auth/guards/role.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Request } from 'express';
import { decode } from './guards.utils';
import { Role } from '../auth.constants';

export const checkHasUserRole =
(...requiredUserRoles: Role[]) =>
async (req: Request) => {
const decodedAuth = await decode(req.headers.authorization ?? '');

const decodedUserRoles = Object.keys(decodedAuth).reduce((roles, key) => {
if (key.match(/claims\/roles$/gi)) {
return decodedAuth[key] as string[];
}

return roles;
}, []);

if (!requiredUserRoles.some((role) => decodedUserRoles.includes(role))) {
return false;
}

return true;
};
86 changes: 71 additions & 15 deletions src/mcp/tools/challenges/queryChallenges.tool.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Injectable, Inject, UseGuards } from '@nestjs/common';
import { Injectable, Inject } from '@nestjs/common';
import { Tool } from '@tc/mcp-nest';
import { REQUEST } from '@nestjs/core';
import { QUERY_CHALLENGES_TOOL_PARAMETERS } from './queryChallenges.parameters';
import { TopcoderChallengesService } from 'src/shared/topcoder/challenges.service';
import { Logger } from 'src/shared/global';
import { QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA } from './queryChallenges.output';
import { AuthGuard } from 'src/core/auth/guards';
import {
authGuard,
checkHasUserRole,
checkM2MScope,
} from 'src/core/auth/guards';
import { M2mScope, Role } from 'src/core/auth/auth.constants';

@Injectable()
export class QueryChallengesTool {
Expand All @@ -16,19 +21,7 @@ export class QueryChallengesTool {
@Inject(REQUEST) private readonly request: any,
) {}

@Tool({
name: 'query-tc-challenges',
description:
'Returns a list of public Topcoder challenges based on the query parameters.',
parameters: QUERY_CHALLENGES_TOOL_PARAMETERS,
outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA,
annotations: {
title: 'Query Public Topcoder Challenges',
readOnlyHint: true,
},
})
@UseGuards(AuthGuard)
async queryChallenges(params) {
private async _queryChallenges(params) {
// Validate the input parameters
const validatedParams = QUERY_CHALLENGES_TOOL_PARAMETERS.safeParse(params);
if (!validatedParams.success) {
Expand Down Expand Up @@ -127,4 +120,67 @@ export class QueryChallengesTool {
};
}
}

@Tool({
name: 'query-tc-challenges-private',
description:
'Returns a list of public Topcoder challenges based on the query parameters.',
parameters: QUERY_CHALLENGES_TOOL_PARAMETERS,
outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA,
annotations: {
title: 'Query Public Topcoder Challenges',
readOnlyHint: true,
},
canActivate: authGuard,
})
async queryChallengesPrivate(params) {
return this._queryChallenges(params);
}

@Tool({
name: 'query-tc-challenges-protected',
description:
'Returns a list of public Topcoder challenges based on the query parameters.',
parameters: QUERY_CHALLENGES_TOOL_PARAMETERS,
outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA,
annotations: {
title: 'Query Public Topcoder Challenges',
readOnlyHint: true,
},
canActivate: checkHasUserRole(Role.Admin),
})
async queryChallengesProtected(params) {
return this._queryChallenges(params);
}

@Tool({
name: 'query-tc-challenges-m2m',
description:
'Returns a list of public Topcoder challenges based on the query parameters.',
parameters: QUERY_CHALLENGES_TOOL_PARAMETERS,
outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA,
annotations: {
title: 'Query Public Topcoder Challenges',
readOnlyHint: true,
},
canActivate: checkM2MScope(M2mScope.QueryPublicChallenges),
})
async queryChallengesM2m(params) {
return this._queryChallenges(params);
}

@Tool({
name: 'query-tc-challenges-public',
description:
'Returns a list of public Topcoder challenges based on the query parameters.',
parameters: QUERY_CHALLENGES_TOOL_PARAMETERS,
outputSchema: QUERY_CHALLENGES_TOOL_OUTPUT_SCHEMA,
annotations: {
title: 'Query Public Topcoder Challenges',
readOnlyHint: true,
},
})
async queryChallengesPublic(params) {
return this._queryChallenges(params);
}
}