From 7b3f23b77f0deaeff17bbcf594ac52eaad2a8283 Mon Sep 17 00:00:00 2001 From: gitcommitshow <56937085+gitcommitshow@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:38:48 +0530 Subject: [PATCH] feat: add api key auth for comment api endpoint --- .env.sample | 1 + app.js | 6 +++ docs/api-reference.md | 25 ++++++++- src/auth.js | 25 +++++++++ src/services/DocsAgent.js | 7 ++- test/e2e/api-key-auth.test.js | 96 +++++++++++++++++++++++++++++++++++ test/lifecycle.test.js | 5 ++ 7 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 test/e2e/api-key-auth.test.js diff --git a/.env.sample b/.env.sample index 1a8e12f..8679d24 100644 --- a/.env.sample +++ b/.env.sample @@ -3,6 +3,7 @@ LOGIN_USER=username LOGIN_PASSWORD=strongpassword DEFAULT_GITHUB_ORG=Git-Commit-Show ONE_CLA_PER_ORG=true +API_KEY=key_to_protect_endpoints_of_this_app API_POST_GITHUB_COMMENT=http://localhost:3000/api/comment #Put your webhook proxy for PR/issue comment here DOCS_AGENT_API_URL=docs_agent_api_base_url DOCS_AGENT_API_KEY=docs_agent_api_key_here diff --git a/app.js b/app.js index b3e9b91..7ffacde 100644 --- a/app.js +++ b/app.js @@ -14,6 +14,7 @@ import { getWebsiteAddress, } from "./src/helpers.js"; import DocsAgent from "./src/services/DocsAgent.js"; +import { validateApiKey } from "./src/auth.js"; try { const packageJson = await import("./package.json", { @@ -261,6 +262,11 @@ const server = http githubWebhookRequestHandler(req, res); break; case "POST /api/comment": + if (!validateApiKey(req)) { + res.writeHead(401); + res.write("API key required"); + return res.end(); + } routes.addCommentToGitHubIssueOrPR(req, res); break; case "GET /": diff --git a/docs/api-reference.md b/docs/api-reference.md index 2a02324..d115ca2 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -10,10 +10,20 @@ The API is served from the configured domain (set via `WEBSITE_ADDRESS` environm Most endpoints do not require authentication. However, some endpoints require specific authentication: +- **API endpoints**: Require API key authentication via request header (using `API_KEY` environment variable) - **Download endpoints**: Require username/password authentication via request body (using `LOGIN_USER` and `LOGIN_PASSWORD` environment variables) - **GitHub webhook endpoints**: Use GitHub webhook secret for verification - **GitHub API interactions**: Use GitHub App authentication +### API Key Authentication + +For endpoints requiring API key authentication, include one of the following headers: + +- `X-API-Key: ` +- `Authorization: Bearer ` + +The API key must match the value set in the `API_KEY` environment variable. If the API key is missing or invalid, the endpoint will return `401 Unauthorized`. + ## Endpoints ### Webhook Endpoints @@ -42,7 +52,7 @@ GitHub webhook endpoint for receiving GitHub events. - `issues.opened`: Adds welcome comment to new issues - `push`: Logs push events -**Note**: For "docs review" label, the app integrates with DocsAgent service if configured. This is limited to repositories specified in the `DOCS_REPOS` environment variable. +**Note**: For "docs review" label, the app integrates with DocsAgent service if configured. This is limited to repositories specified in the `DOCS_REPOS` environment variable. The DocsAgent service uses the `API_POST_GITHUB_COMMENT` environment variable (or defaults to `WEBSITE_ADDRESS/api/comment`) to post review results back to GitHub. --- @@ -208,6 +218,8 @@ Adds a comment to a GitHub issue or pull request. **Description**: Adds a comment to a specified GitHub issue or PR (used by external services like docs agent). +**Authentication**: Requires API key authentication via `X-API-Key` or `Authorization: Bearer ` header. + **Request Body** (JSON): ```json { @@ -221,12 +233,14 @@ Adds a comment to a GitHub issue or pull request. **Response**: - `200 OK`: "Comment added to GitHub issue or PR" - `400 Bad Request`: Missing required parameters +- `401 Unauthorized`: API key missing or invalid - `500 Internal Server Error`: Failed to add comment **Example**: ```bash curl -X POST http://localhost:3000/api/comment \ -H "Content-Type: application/json" \ + -H "X-API-Key: your-api-key" \ -d '{"owner":"myorg","repo":"myrepo","issue_number":123,"result":"Review completed"}' ``` @@ -283,6 +297,9 @@ The following environment variables affect API behavior: - `WEBHOOK_SECRET`: GitHub webhook secret for verification - `ENTERPRISE_HOSTNAME`: GitHub Enterprise hostname (if applicable) +### API Authentication +- `API_KEY`: API key for protecting API endpoints (e.g., `/api/comment`) + ### Download Endpoint Authentication - `LOGIN_USER`: Username for download authentication - `LOGIN_PASSWORD`: Password for download authentication @@ -297,6 +314,7 @@ The following environment variables affect API behavior: - `DOCS_AGENT_API_LINK_URL`: URL for docs linking endpoint - `DOCS_AGENT_API_TIMEOUT`: Timeout for DocsAgent API calls (default: 350000ms) - `DOCS_REPOS`: Comma-separated list of repositories eligible for docs review +- `API_POST_GITHUB_COMMENT`: Webhook URL for DocsAgent to post result back to (defaults to `our WEBSITE_ADDRESS/api/comment` where we have configured github issue/pr comments). Allows use case to use a proxy url for this webhook url in staging server. ### Development & Deployment - `DEFAULT_GITHUB_ORG`: Default GitHub organization @@ -305,4 +323,7 @@ The following environment variables affect API behavior: - `ONE_CLA_PER_ORG`: If "true", one CLA signature is valid for all repos in an org - `CODESANDBOX_HOST`: CodeSandbox host (for staging environments) - `HOSTNAME`: Hostname for the application -- `SMEE_URL`: Smee proxy URL for local development \ No newline at end of file +- `SMEE_URL`: Smee proxy URL for local development + +### Slack Integration +- `SLACK_DEFAULT_MESSAGE_CHANNEL_WEBHOOK_URL`: Slack webhook URL for sending notifications when PRs are labeled with "product review" \ No newline at end of file diff --git a/src/auth.js b/src/auth.js index 3178459..2cf27f8 100644 --- a/src/auth.js +++ b/src/auth.js @@ -14,4 +14,29 @@ export function isPasswordValid(username, password){ } loginAttempts[username] = (loginAttempts[username] || 0) + 1; return false +} + +export function validateApiKey(req) { + // Check if API_KEY environment variable is set + if (!process.env.API_KEY) { + console.error("API_KEY environment variable not configured"); + return false; + } + + // Check for X-API-Key header + const apiKeyHeader = req.headers['x-api-key']; + if (apiKeyHeader && apiKeyHeader === process.env.API_KEY) { + return true; + } + + // Check for Authorization: Bearer header + const authHeader = req.headers['authorization']; + if (authHeader && authHeader.startsWith('Bearer ')) { + const token = authHeader.substring(7); // Remove 'Bearer ' prefix + if (token === process.env.API_KEY) { + return true; + } + } + + return false; } \ No newline at end of file diff --git a/src/services/DocsAgent.js b/src/services/DocsAgent.js index 213184e..6b4c7ab 100644 --- a/src/services/DocsAgent.js +++ b/src/services/DocsAgent.js @@ -1,5 +1,10 @@ /** - * Service for interacting with external APIs to get next actions + * Service for interacting with docs agent external APIs to get next actions + * + * @example + * const docsAgent = new DocsAgent(); + * const result = await docsAgent.reviewDocs("content", "filepath", { webhookUrl: "webhookUrl", webhookMetadata: { owner: "owner", repo: "repo", issue_number: "issue_number" } }); + * console.log(result); */ export class DocsAgent { constructor() { diff --git a/test/e2e/api-key-auth.test.js b/test/e2e/api-key-auth.test.js new file mode 100644 index 0000000..c64107a --- /dev/null +++ b/test/e2e/api-key-auth.test.js @@ -0,0 +1,96 @@ +/** + * Integration tests for API key authentication functionality. + */ +import { expect, use } from 'chai'; +import chaiHttp from 'chai-http'; +import { describe, it, before, after } from 'mocha'; + +const chai = use(chaiHttp); +const SITE_URL = 'http://localhost:' + (process.env.PORT || 3000); + +describe('API Key Authentication', function () { + this.timeout(40000); + let agent; + + before(function () { + agent = chai.request.agent(SITE_URL); + // Not setting API key here because it's set in the lifecycle test + }); + + after(function () { + agent.close(); + }); + + describe('POST /api/comment endpoint', function () { + const validPayload = { + owner: 'test-org', + repo: 'test-repo', + issue_number: 123, + result: 'Test comment' + }; + + it('should return 401 when no API key is provided', async function () { + const res = await agent + .post('/api/comment') + .send(validPayload); + + expect(res).to.have.status(401); + expect(res.text).to.equal('API key required'); + }); + + it('should return 401 when invalid X-API-Key is provided', async function () { + const res = await agent + .post('/api/comment') + .set('X-API-Key', 'invalid-key') + .send(validPayload); + + expect(res).to.have.status(401); + expect(res.text).to.equal('API key required'); + }); + + it('should return 401 when invalid Authorization Bearer is provided', async function () { + const res = await agent + .post('/api/comment') + .set('Authorization', 'Bearer invalid-key') + .send(validPayload); + + expect(res).to.have.status(401); + expect(res.text).to.equal('API key required'); + }); + + it('should accept valid X-API-Key header', async function () { + const res = await agent + .post('/api/comment') + .set('X-API-Key', 'test-api-key') + .send(validPayload); + + // Should not return 401 (authentication passed) + // Note: This might return 400/500 due to GitHub API calls in test environment + // but the important thing is that it's not 401 + expect(res).to.not.have.status(401); + }); + + it('should accept valid Authorization Bearer header', async function () { + const res = await agent + .post('/api/comment') + .set('Authorization', 'Bearer test-api-key') + .send(validPayload); + + // Should not return 401 (authentication passed) + // Note: This might return 400/500 due to GitHub API calls in test environment + // but the important thing is that it's not 401 + expect(res).to.not.have.status(401); + }); + + it('should prioritize X-API-Key over Authorization header when both are present', async function () { + const res = await agent + .post('/api/comment') + .set('X-API-Key', 'test-api-key') + .set('Authorization', 'Bearer invalid-key') + .send(validPayload); + + // Should not return 401 (X-API-Key takes precedence) + expect(res).to.not.have.status(401); + }); + }); +}); diff --git a/test/lifecycle.test.js b/test/lifecycle.test.js index 37e5a4c..1d6cca0 100644 --- a/test/lifecycle.test.js +++ b/test/lifecycle.test.js @@ -12,6 +12,11 @@ before(async function () { if (process.env.RUN_E2E_TESTS === 'true') { try { + + if (!process.env.API_KEY) { + process.env.API_KEY = 'test-api-key'; + } + appProcess = spawn('node', ['app.js'], { env: { ...process.env, NODE_ENV: 'test' }, stdio: 'pipe'