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
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand Down Expand Up @@ -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 /":
Expand Down
25 changes: 23 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <your-api-key>`
- `Authorization: Bearer <your-api-key>`

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
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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 <key>` header.

**Request Body** (JSON):
```json
{
Expand All @@ -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"}'
```

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
- `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"
25 changes: 25 additions & 0 deletions src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <key> 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;
}
7 changes: 6 additions & 1 deletion src/services/DocsAgent.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
96 changes: 96 additions & 0 deletions test/e2e/api-key-auth.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
5 changes: 5 additions & 0 deletions test/lifecycle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down