mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
chore(api): prevent non-staff exam authz token gen on staging (#61786)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
@@ -20,7 +20,8 @@ jest.mock('../../utils/env', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...jest.requireActual('../../utils/env'),
|
||||
FCC_ENABLE_EXAM_ENVIRONMENT: 'true'
|
||||
FCC_ENABLE_EXAM_ENVIRONMENT: 'true',
|
||||
DEPLOYMENT_ENV: 'production'
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,19 @@ import { getMsTranscriptApiUrl } from './user';
|
||||
const mockedFetch = jest.fn();
|
||||
jest.spyOn(globalThis, 'fetch').mockImplementation(mockedFetch);
|
||||
|
||||
let mockDeploymentEnv = 'staging';
|
||||
jest.mock('../../utils/env', () => {
|
||||
const actualEnv = jest.requireActual('../../utils/env');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...actualEnv,
|
||||
get DEPLOYMENT_ENV() {
|
||||
return mockDeploymentEnv;
|
||||
},
|
||||
JWT_SECRET: actualEnv.JWT_SECRET
|
||||
};
|
||||
});
|
||||
|
||||
// This is used to build a test user.
|
||||
const testUserData: Prisma.userCreateInput = {
|
||||
...createUserInput(defaultUserEmail),
|
||||
@@ -1273,6 +1286,14 @@ Thanks and regards,
|
||||
});
|
||||
|
||||
describe('/user/exam-environment/token', () => {
|
||||
beforeEach(() => {
|
||||
mockDeploymentEnv = 'production';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
mockDeploymentEnv = 'staging';
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fastifyTestInstance.prisma.examEnvironmentAuthorizationToken.deleteMany(
|
||||
{
|
||||
@@ -1335,6 +1356,44 @@ Thanks and regards,
|
||||
);
|
||||
expect(tokens).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('POST does not generate a new token in non-production environments for non-staff', async () => {
|
||||
// Override deployment environment for this test
|
||||
mockDeploymentEnv = 'development';
|
||||
const response = await superPost('/user/exam-environment/token');
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
test('POST does generate a new token in non-production environments for staff', async () => {
|
||||
// Override deployment environment for this test
|
||||
mockDeploymentEnv = 'staging';
|
||||
await fastifyTestInstance.prisma.user.update({
|
||||
where: {
|
||||
id: defaultUserId
|
||||
},
|
||||
data: { email: 'camperbot@freecodecamp.org' }
|
||||
});
|
||||
|
||||
const response = await superPost('/user/exam-environment/token');
|
||||
const { examEnvironmentAuthorizationToken } = response.body;
|
||||
|
||||
const decodedToken = jwt.decode(examEnvironmentAuthorizationToken);
|
||||
|
||||
expect(decodedToken).toStrictEqual({
|
||||
examEnvironmentAuthorizationToken:
|
||||
expect.stringMatching(/^[a-z0-9]{24}$/),
|
||||
iat: expect.any(Number)
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
jwt.verify(examEnvironmentAuthorizationToken, 'wrong-secret')
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
jwt.verify(examEnvironmentAuthorizationToken, JWT_SECRET)
|
||||
).not.toThrow();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,11 +27,12 @@ import {
|
||||
getPoints,
|
||||
ProgressTimestamp
|
||||
} from '../../utils/progress';
|
||||
import { JWT_SECRET } from '../../utils/env';
|
||||
import { DEPLOYMENT_ENV, JWT_SECRET } from '../../utils/env';
|
||||
import {
|
||||
getExamAttemptHandler,
|
||||
getExamAttemptsHandler
|
||||
} from '../../exam-environment/routes/exam-environment';
|
||||
import { ERRORS } from '../../exam-environment/utils/errors';
|
||||
|
||||
/**
|
||||
* Helper function to get the api url from the shared transcript link.
|
||||
@@ -498,7 +499,7 @@ export const userRoutes: FastifyPluginCallbackTypebox = (
|
||||
done();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line jsdoc/require-param
|
||||
// eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns
|
||||
/**
|
||||
* Generate a new authorization token for the given user, and invalidates any existing tokens.
|
||||
*
|
||||
@@ -515,6 +516,24 @@ async function examEnvironmentTokenHandler(
|
||||
if (!userId) {
|
||||
throw new Error('Unreachable. User should be authenticated.');
|
||||
}
|
||||
|
||||
// In non-production environments, only staff are allowed to generate a token
|
||||
if (
|
||||
DEPLOYMENT_ENV !== 'production' &&
|
||||
(!req.user?.email?.endsWith('@freecodecamp.org') ||
|
||||
!req.user?.emailVerified)
|
||||
) {
|
||||
logger.info(
|
||||
`User not allowed to generate authorization token on ${DEPLOYMENT_ENV}.`
|
||||
);
|
||||
void reply.code(403);
|
||||
return reply.send(
|
||||
ERRORS.FCC_ERR_EXAM_ENVIRONMENT(
|
||||
`User not allowed to generate authorization token in ${DEPLOYMENT_ENV} environment.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Delete (invalidate) any existing tokens for the user.
|
||||
await this.prisma.examEnvironmentAuthorizationToken.deleteMany({
|
||||
where: {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Type } from '@fastify/type-provider-typebox';
|
||||
import { STANDARD_ERROR } from '../../exam-environment/utils/errors';
|
||||
|
||||
export const userExamEnvironmentToken = {
|
||||
response: {
|
||||
200: Type.Object({
|
||||
201: Type.Object({
|
||||
examEnvironmentAuthorizationToken: Type.String()
|
||||
})
|
||||
}),
|
||||
403: STANDARD_ERROR
|
||||
// default: STANDARD_ERROR
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ If so, ensure that the environment variable JEST_WORKER_ID is set.`
|
||||
|
||||
assert.ok(process.env.HOME_LOCATION);
|
||||
assert.ok(isAllowedEnv(_FREECODECAMP_NODE_ENV));
|
||||
assert.ok(process.env.DEPLOYMENT_ENV);
|
||||
assert.ok(isAllowedProvider(_EMAIL_PROVIDER));
|
||||
assert.ok(process.env.AUTH0_CLIENT_ID);
|
||||
assert.ok(process.env.AUTH0_CLIENT_SECRET);
|
||||
@@ -191,6 +192,7 @@ export const FCC_ENABLE_SENTRY_ROUTES = undefinedOrBool(
|
||||
process.env.FCC_ENABLE_SENTRY_ROUTES
|
||||
);
|
||||
export const FREECODECAMP_NODE_ENV = _FREECODECAMP_NODE_ENV;
|
||||
export const DEPLOYMENT_ENV = process.env.DEPLOYMENT_ENV;
|
||||
export const SENTRY_DSN =
|
||||
process.env.SENTRY_DSN === 'dsn_from_sentry_dashboard'
|
||||
? ''
|
||||
|
||||
Reference in New Issue
Block a user