From f70ce55eb37b989f667053b13fd1f220158a56a9 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Mon, 25 Dec 2023 14:15:23 -0800 Subject: [PATCH 1/2] Added /gitops-graphql - replaced appAuth with componentAuth --- .env-cmdrc-template | 3 ++- .github/workflows/master.yml | 1 + .github/workflows/re-release.yml | 1 + .github/workflows/release.yml | 1 + docker-compose.yml | 1 + src/api-docs/paths/path-client.js | 6 ++--- src/api-docs/swagger-document.js | 2 +- src/app.js | 6 +++-- src/client/resolvers.js | 2 +- src/middleware/auth.js | 17 +++++++++++++- src/routers/client-api.js | 8 +++---- tests/gitops.test.js | 39 +++++++++++++++++++++++++++++++ 12 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 tests/gitops.test.js diff --git a/.env-cmdrc-template b/.env-cmdrc-template index 9a0da96..291f708 100644 --- a/.env-cmdrc-template +++ b/.env-cmdrc-template @@ -37,6 +37,7 @@ "SWITCHER_API_DOMAIN": "MOCK_SWITCHER_API_DOMAIN", "SWITCHER_API_ENVIRONMENT": "default", - "SWITCHER_SLACK_JWT_SECRET": "MOCK_SWITCHER_SLACK_JWT_SECRET" + "SWITCHER_SLACK_JWT_SECRET": "MOCK_SWITCHER_SLACK_JWT_SECRET", + "SWITCHER_GITOPS_JWT_SECRET": "MOCK_SWITCHER_GITOPS_JWT_SECRET" } } \ No newline at end of file diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index c40b964..fe45aa7 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -44,6 +44,7 @@ jobs: JWT_CLIENT_TOKEN_EXP_TIME: 5m JWT_SECRET: ${{ secrets.JWT_SECRET }} SWITCHER_SLACK_JWT_SECRET: ${{ secrets.SWITCHER_SLACK_JWT_SECRET }} + SWITCHER_GITOPS_JWT_SECRET: ${{ secrets.SWITCHER_GITOPS_JWT_SECRET }} GOOGLE_RECAPTCHA_SECRET: ${{ secrets.GOOGLE_RECAPTCHA_SECRET }} GOOGLE_SKIP_AUTH: false MAX_STRATEGY_OPERATION: 100 diff --git a/.github/workflows/re-release.yml b/.github/workflows/re-release.yml index e223d04..743a49b 100644 --- a/.github/workflows/re-release.yml +++ b/.github/workflows/re-release.yml @@ -45,6 +45,7 @@ jobs: JWT_CLIENT_TOKEN_EXP_TIME: 5m JWT_SECRET: ${{ secrets.JWT_SECRET }} SWITCHER_SLACK_JWT_SECRET: ${{ secrets.SWITCHER_SLACK_JWT_SECRET }} + SWITCHER_GITOPS_JWT_SECRET: ${{ secrets.SWITCHER_GITOPS_JWT_SECRET }} GOOGLE_RECAPTCHA_SECRET: ${{ secrets.GOOGLE_RECAPTCHA_SECRET }} GOOGLE_SKIP_AUTH: false MAX_STRATEGY_OPERATION: 100 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b85f7f..b20bc3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,7 @@ jobs: JWT_CLIENT_TOKEN_EXP_TIME: 5m JWT_SECRET: ${{ secrets.JWT_SECRET }} SWITCHER_SLACK_JWT_SECRET: ${{ secrets.SWITCHER_SLACK_JWT_SECRET }} + SWITCHER_GITOPS_JWT_SECRET: ${{ secrets.SWITCHER_GITOPS_JWT_SECRET }} GOOGLE_RECAPTCHA_SECRET: ${{ secrets.GOOGLE_RECAPTCHA_SECRET }} GOOGLE_SKIP_AUTH: false MAX_STRATEGY_OPERATION: 100 diff --git a/docker-compose.yml b/docker-compose.yml index a6b8fba..d00b6fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,6 +76,7 @@ services: - SWITCHER_API_ENVIRONMENT=${SWITCHER_API_ENVIRONMENT} - SWITCHER_SLACK_JWT_SECRET=${SWITCHER_SLACK_JWT_SECRET} + - SWITCHER_GITOPS_JWT_SECRET=${SWITCHER_GITOPS_JWT_SECRET} depends_on: - mongodb volumes: diff --git a/src/api-docs/paths/path-client.js b/src/api-docs/paths/path-client.js index d8800b4..81e4452 100644 --- a/src/api-docs/paths/path-client.js +++ b/src/api-docs/paths/path-client.js @@ -7,7 +7,7 @@ export default { post: { tags: ['Client API'], description: 'Execute criteria query against the API settings', - security: [{ appAuth: [] }], + security: [{ componentAuth: [] }], parameters: [ queryParameter('key', 'Switcher Key', true, 'string'), queryParameter('showReason', 'Show criteria execution reason (default: true)', false, 'boolean'), @@ -70,7 +70,7 @@ export default { get: { tags: ['Client API'], description: 'Check if snapshot version is up to date', - security: [{ appAuth: [] }], + security: [{ componentAuth: [] }], parameters: [ pathParameter('version', 'Snapshot version', true) ], @@ -98,7 +98,7 @@ export default { post: { tags: ['Client API'], description: 'Check if switcher keys are valid', - security: [{ appAuth: [] }], + security: [{ componentAuth: [] }], requestBody: { content: { 'application/json': { diff --git a/src/api-docs/swagger-document.js b/src/api-docs/swagger-document.js index feb5f0a..4616b33 100644 --- a/src/api-docs/swagger-document.js +++ b/src/api-docs/swagger-document.js @@ -52,7 +52,7 @@ export default { scheme: 'bearer', bearerFormat: 'JWT' }, - appAuth: { + componentAuth: { type: 'http', scheme: 'bearer' }, diff --git a/src/app.js b/src/app.js index fdbf737..1684396 100644 --- a/src/app.js +++ b/src/app.js @@ -21,7 +21,7 @@ import teamRouter from './routers/team'; import permissionRouter from './routers/permission'; import slackRouter from './routers/slack'; import schema from './client/schema'; -import { appAuth, auth, resourcesAuth, slackAuth } from './middleware/auth'; +import { componentAuth, auth, resourcesAuth, slackAuth, gitopsAuth } from './middleware/auth'; import { clientLimiter, defaultLimiter } from './middleware/limiter'; import { createServer } from './app-server'; @@ -59,11 +59,13 @@ const handler = (req, res, next) => createHandler({ schema, context: req })(req, res, next); // Component: Client API -app.use('/graphql', appAuth, clientLimiter, handler); +app.use('/graphql', componentAuth, clientLimiter, handler); // Admin: Client API app.use('/adm-graphql', auth, defaultLimiter, handler); // Slack: Client API app.use('/slack-graphql', slackAuth, handler); +// GitOps: Client API +app.use('/gitops-graphql', gitopsAuth, handler); /** * API Docs and Health Check diff --git a/src/client/resolvers.js b/src/client/resolvers.js index 886afa4..032ad08 100644 --- a/src/client/resolvers.js +++ b/src/client/resolvers.js @@ -113,7 +113,7 @@ export async function resolveDomain(_id, name, activated, context) { } else { args._id = _id; } - // When Component + // When Component / GitOps } else if (context.domain) { args._id = context.domain; } diff --git a/src/middleware/auth.js b/src/middleware/auth.js index 5c419d1..fb3c123 100644 --- a/src/middleware/auth.js +++ b/src/middleware/auth.js @@ -6,6 +6,7 @@ import Admin from '../models/admin'; import Component from '../models/component'; import { getRateLimit } from '../external/switcher-api-facade'; import { responseExceptionSilent } from '../exceptions'; +import { EnvType } from '../models/environment'; export async function auth(req, res, next) { try { @@ -54,7 +55,7 @@ export async function authRefreshToken(req, res, next) { } } -export async function appAuth(req, res, next) { +export async function componentAuth(req, res, next) { try { const token = req.header('Authorization').replace('Bearer ', ''); const decoded = jwt.verify(token, process.env.JWT_SECRET); @@ -86,6 +87,20 @@ export async function slackAuth(req, res, next) { } } +export async function gitopsAuth(req, res, next) { + try { + const token = req.header('Authorization').replace('Bearer ', ''); + const decoded = jwt.verify(token, process.env.SWITCHER_GITOPS_JWT_SECRET); + + req.token = token; + req.domain = decoded.subject; + req.environment = EnvType.DEFAULT; + next(); + } catch (err) { + responseExceptionSilent(res, err, 401, 'Invalid API token.'); + } +} + export function resourcesAuth() { return basicAuth({ users: { diff --git a/src/routers/client-api.js b/src/routers/client-api.js index 95870e5..b9f3101 100644 --- a/src/routers/client-api.js +++ b/src/routers/client-api.js @@ -1,7 +1,7 @@ import express from 'express'; import jwt from 'jsonwebtoken'; import { checkConfig, checkConfigComponent, validate } from '../middleware/validators'; -import { appAuth, appGenerateCredentials } from '../middleware/auth'; +import { componentAuth, appGenerateCredentials } from '../middleware/auth'; import { resolveCriteria, checkDomain } from '../client/resolvers'; import { getConfigs } from '../services/config'; import { body, check, query } from 'express-validator'; @@ -13,7 +13,7 @@ const router = new express.Router(); // GET /check?key=KEY&showReason=true // GET /check?key=KEY&showStrategy=true // GET /check?key=KEY&bypassMetric=true -router.post('/criteria', appAuth, clientLimiter, [ +router.post('/criteria', componentAuth, clientLimiter, [ query('key').isLength({ min: 1 }), body('entry.*.input').isString() ], validate, checkConfig, checkConfigComponent, async (req, res) => { @@ -43,7 +43,7 @@ router.post('/criteria', appAuth, clientLimiter, [ } }); -router.get('/criteria/snapshot_check/:version', appAuth, clientLimiter, async (req, res) => { +router.get('/criteria/snapshot_check/:version', componentAuth, clientLimiter, async (req, res) => { try { const domain = await checkDomain(req.domain); const version = req.params.version; @@ -62,7 +62,7 @@ router.get('/criteria/snapshot_check/:version', appAuth, clientLimiter, async (r } }); -router.post('/criteria/switchers_check', appAuth, clientLimiter, [ +router.post('/criteria/switchers_check', componentAuth, clientLimiter, [ check('switchers', 'Switcher Key is required').isArray().isLength({ min: 1 }) ], validate, async (req, res) => { try { diff --git a/tests/gitops.test.js b/tests/gitops.test.js new file mode 100644 index 0000000..0639daa --- /dev/null +++ b/tests/gitops.test.js @@ -0,0 +1,39 @@ +import mongoose from 'mongoose'; +import request from 'supertest'; +import jwt from 'jsonwebtoken'; +import app from '../src/app'; +import * as graphqlUtils from './graphql-utils'; +import { + setupDatabase, + domainId +} from './fixtures/db_client'; + +afterAll(async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + await mongoose.disconnect(); +}); + +const generateToken = (expiresIn) => { + return jwt.sign(({ + iss: 'GitOps Service', + sub: '/resource', + subject: domainId.toString(), + }), process.env.SWITCHER_GITOPS_JWT_SECRET, { + expiresIn + }); +}; + +describe('GitOps', () => { + beforeAll(setupDatabase); + + test('GITOPS_SUITE - Should return snapshot payload from GraphQL API', async () => { + const token = generateToken('30s'); + const req = await request(app) + .post('/gitops-graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.domainQuery([['_id', domainId]], true, true, true)); + + expect(req.statusCode).toBe(200); + expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected102)); + }); +}); \ No newline at end of file From ee6ea4a7f1ed744f36934518973f389f0be7818c Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Mon, 25 Dec 2023 14:26:34 -0800 Subject: [PATCH 2/2] Added token expiration test --- tests/gitops.test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/gitops.test.js b/tests/gitops.test.js index 0639daa..5bac37e 100644 --- a/tests/gitops.test.js +++ b/tests/gitops.test.js @@ -36,4 +36,14 @@ describe('GitOps', () => { expect(req.statusCode).toBe(200); expect(JSON.parse(req.text)).toMatchObject(JSON.parse(graphqlUtils.expected102)); }); + + test('GITOPS_SUITE - Should return error when token is expired', async () => { + const token = generateToken('0s'); + const req = await request(app) + .post('/gitops-graphql') + .set('Authorization', `Bearer ${token}`) + .send(graphqlUtils.domainQuery([['_id', domainId]], true, true, true)); + + expect(req.statusCode).toBe(401); + }); }); \ No newline at end of file