Skip to content

Commit c5e79f6

Browse files
committed
feat: Enhance GitHub integration with multi-owner
1 parent d407e52 commit c5e79f6

22 files changed

Lines changed: 572 additions & 173 deletions

.env.example

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ NODE_ENV=development
88
# Backend server port
99
PORT=3001
1010

11-
# Frontend URL (for OAuth callback redirects)
11+
# Frontend URL (for OAuth callback redirects when AUTH_ENABLED=true)
1212
FRONTEND_URL=http://localhost:5173
1313

1414
# ===================================
@@ -24,30 +24,41 @@ FRONTEND_URL=http://localhost:5173
2424
LOG_LEVEL=INFO
2525

2626
# ===================================
27-
# GitHub Integration (Required)
27+
# GitHub scanner (machine identity)
2828
# ===================================
2929

30-
# GitHub Personal Access Token for API access
31-
# Required scopes: repo, read:org, read:user
30+
# Personal access token for API scans (orgs and/or user-owned repos accessible to token)
31+
# Scopes vary; typically: repo, read:org, read:user
3232
# Create at: https://github.com/settings/tokens
3333
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3434

35-
# GitHub organization to scan
36-
GITHUB_ORG=your-organization
35+
# Comma-separated GitHub login or organization slugs to scan (multi-owner)
36+
# Example (org + personal): GITHUB_TARGETS=my-org,myusername,other-org
37+
# Leave unset if you only use GITHUB_ORG below.
38+
GITHUB_TARGETS=
39+
40+
# Legacy: single owner when GITHUB_TARGETS is unset (still supported)
41+
GITHUB_ORG=your-organization-or-username
3742

3843
# ===================================
39-
# GitHub OAuth App (Required for Authentication)
44+
# GitHub OAuth App (human login — only when AUTH_ENABLED=true)
4045
# ===================================
4146

42-
# GitHub OAuth App credentials for user authentication
47+
AUTH_ENABLED=true
48+
4349
# Create a GitHub OAuth App at: https://github.com/settings/developers
4450
# Homepage URL: http://localhost:5173
4551
# Authorization callback URL: http://localhost:3001/api/auth/callback
52+
# When AUTH_ENABLED=false, leave client id/secret empty or omit.
4653
GITHUB_AUTH_CLIENT_ID=Ov23xxxxxxxxxxxxxxxxxxxxxxxxxxxx
4754
GITHUB_AUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
4855

49-
# Session secret for cookie encryption (use a long random string)
50-
# Generate with: openssl rand -base64 32
56+
# Optional: GitHub team slug within each organization in GITHUB_TARGETS / GITHUB_ORG.
57+
# When set, login requires membership in that team for each org target.
58+
# When unset, any member of each organization target may sign in.
59+
GITHUB_AUTH_TEAM_SLUG=
60+
61+
# Session secret for cookie encryption (always required — sessions still used when OAuth off)
5162
SESSION_SECRET=your-random-secret-key-min-32-chars-long
5263

5364
# ===================================
@@ -72,11 +83,8 @@ SCAN_INTERVAL_MINUTES=60
7283
# Rate Limiting (Optional)
7384
# ===================================
7485

75-
# Maximum number of repositories to scan per organization scan
76-
# Set to 0 for unlimited (default)
86+
# Maximum number of repositories per full scan (0 = unlimited)
7787
MAX_SCAN_LIMIT=0
7888

79-
# Specific repositories to scan (comma-separated list)
80-
# Takes precedence over MAX_SCAN_LIMIT if set
81-
# Example: repo1,repo2,repo3
89+
# Optional: limit to repo short names ("a","b") or full names ("org/a","org/b") when set
8290
SCAN_REPOS=

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to the Renovate Bot Dashboard project.
44

55
## [Unreleased] - 2025-12-03
66

7+
### GitHub configuration and scanning
8+
9+
- **Multi-owner scanning**: `GITHUB_TARGETS` accepts a comma-separated list of organization logins and/or GitHub user logins. `GITHUB_ORG` remains supported as a single-owner fallback when `GITHUB_TARGETS` is unset.
10+
- **Optional OAuth**: `AUTH_ENABLED=false` allows running without GitHub OAuth client credentials; `requireAuth` and `/api/auth/status` treat the instance as accessible without a GitHub login session.
11+
- **Authorization**: OAuth callback enforces the existing org team check for each configured **organization** target; **user** targets skip team membership.
12+
- **API / UI**: Settings responses include `github.targets` and `auth.enabled`. Helm and Docker Compose pass `GITHUB_TARGETS`, `AUTH_ENABLED`, and related variables.
13+
714
### 🔐 Security Improvements
815

916
#### Package Vulnerabilities Fixed
@@ -41,9 +48,7 @@ All notable changes to the Renovate Bot Dashboard project.
4148

4249
#### Authentication System
4350
- **GitHub OAuth SSO** - Mandatory authentication for all users
44-
- **Team-Based Access Control** - Only authorized team members can access
45-
- Default: `team_cloud_and_platforms` in `prom-candp` organization
46-
- Configurable in `backend/src/routes/auth.routes.ts`
51+
- **Team-Based Access Control** - Only authorized GitHub users can access (team enforced when `GITHUB_AUTH_TEAM_SLUG` is set; otherwise organization membership)
4752
- **Login/Logout Flow** with proper session management
4853
- **Protected Routes** - All API endpoints require authentication
4954
- **User Profile** displayed in header with avatar

README.md

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,22 @@ pnpm install
9595

9696
```bash
9797
# Copy example environment file
98-
cp backend/.env.example backend/.env
98+
cp .env.example .env
9999
```
100100

101-
Edit `backend/.env` with your credentials:
101+
Edit `.env` with your credentials:
102102

103103
```env
104-
# GitHub Configuration
104+
# GitHub Configuration (scanner PAT)
105105
GITHUB_TOKEN=ghp_your_personal_access_token_here
106-
GITHUB_ORG=your-organization-name
106+
# Owners to scan — comma-separated orgs and/or users, OR legacy single value:
107+
GITHUB_TARGETS=my-org,my-github-username
108+
# GITHUB_ORG=single-org-or-user
109+
110+
# Optional: set to false for local/demo without OAuth app
111+
AUTH_ENABLED=true
107112
108-
# GitHub OAuth (create at: https://github.com/settings/developers)
113+
# GitHub OAuth (when AUTH_ENABLED=true — create at: https://github.com/settings/developers)
109114
GITHUB_AUTH_CLIENT_ID=your_oauth_client_id
110115
GITHUB_AUTH_CLIENT_SECRET=your_oauth_client_secret
111116
@@ -177,17 +182,17 @@ GRANT ALL PRIVILEGES ON DATABASE renovate_dashboard TO renovate;
177182

178183
```bash
179184
# Copy example environment file
180-
cp backend/.env.example backend/.env
185+
cp .env.example .env
181186
```
182187

183-
Edit `backend/.env`:
188+
Edit `.env`:
184189

185190
```env
186191
# GitHub Configuration
187192
GITHUB_TOKEN=ghp_your_personal_access_token_here
188-
GITHUB_ORG=your-organization-name
193+
GITHUB_TARGETS=my-org,my-github-username
189194
190-
# GitHub OAuth
195+
AUTH_ENABLED=true
191196
GITHUB_AUTH_CLIENT_ID=your_oauth_client_id
192197
GITHUB_AUTH_CLIENT_SECRET=your_oauth_client_secret
193198
@@ -366,10 +371,12 @@ pnpm run db:migrate:prod # Deploy migrations (no prompts)
366371

367372
| Variable | Description | How to get |
368373
| --------------------------- | ----------------------------------------- | ---------------------------------------------------------- |
369-
| `GITHUB_TOKEN` | GitHub PAT with `repo`, `read:org` scopes | [Create token](https://github.com/settings/tokens) |
370-
| `GITHUB_ORG` | GitHub organization to monitor | Your org name |
371-
| `GITHUB_AUTH_CLIENT_ID` | OAuth App Client ID | [Create OAuth App](https://github.com/settings/developers) |
372-
| `GITHUB_AUTH_CLIENT_SECRET` | OAuth App Client Secret | Same OAuth App |
374+
| `GITHUB_TOKEN` | GitHub PAT (`repo`, `read:org`, etc.) | [Create token](https://github.com/settings/tokens) |
375+
| `GITHUB_TARGETS` and/or `GITHUB_ORG` | Owners to scan (comma-separated org/user logins, or single `GITHUB_ORG`) | Your org or username on GitHub |
376+
| `AUTH_ENABLED` | When `true`, GitHub OAuth is required for API access | Set `false` only for trusted local/demo use |
377+
| `GITHUB_AUTH_CLIENT_ID` | OAuth App Client ID (if `AUTH_ENABLED=true`) | [Create OAuth App](https://github.com/settings/developers) |
378+
| `GITHUB_AUTH_CLIENT_SECRET` | OAuth App Client Secret (if `AUTH_ENABLED=true`) | Same OAuth App |
379+
| `GITHUB_AUTH_TEAM_SLUG` | Optional GitHub team slug for org targets; if unset, org members may sign in ||
373380
| `SESSION_SECRET` | Random string for session encryption | Generate: `openssl rand -base64 32` |
374381

375382
#### Storage Configuration
@@ -407,9 +414,12 @@ pnpm run db:migrate:prod # Deploy migrations (no prompts)
407414

408415
### Team-Based Access Control
409416

410-
By default, only users in the `team_cloud_and_platforms` team under the `prom-candp` organization can access the dashboard. To change this, modify:
417+
For each configured **organization** target, OAuth checks GitHub access before issuing a session:
418+
419+
- If **`GITHUB_AUTH_TEAM_SLUG`** is set, the user must be in that team within the organization.
420+
- If it is unset, any **member of the organization** may sign in.
411421

412-
- `backend/src/routes/auth.routes.ts` (lines 10-11)
422+
Personal (user) targets do not apply this check.
413423

414424
### Notification Triggers
415425

backend/prisma/seed.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ async function main() {
1111
update: {},
1212
create: {
1313
id: 'app-settings',
14-
githubOrg: process.env.GITHUB_ORG || 'your-organization',
14+
githubOrg:
15+
process.env.GITHUB_TARGETS?.trim()?.split(',').map((t) => t.trim()).filter(Boolean).join(',') ||
16+
process.env.GITHUB_ORG?.trim() ||
17+
'your-organization',
1518
scanIntervalMinutes: 60,
1619
},
1720
});

backend/src/config/env.ts

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,61 @@
11
import dotenv from 'dotenv';
2-
import { z } from 'zod';
32
import path from 'path';
3+
import { z } from 'zod';
44

55
// Load .env files - try multiple locations
66
const envPaths = [
77
path.resolve(process.cwd(), '../.env'), // From backend/ to root
8-
path.resolve(process.cwd(), '.env'), // From root
8+
path.resolve(process.cwd(), '.env'), // From root
99
];
1010

1111
for (const envPath of envPaths) {
1212
dotenv.config({ path: envPath });
1313
}
1414

15-
const envSchema = z.object({
15+
/** Comma-separated org or user login slugs, or legacy single org via GITHUB_ORG only */
16+
export function parseGithubTargetLoginList(raw: {
17+
GITHUB_TARGETS?: string;
18+
GITHUB_ORG?: string;
19+
}): string[] {
20+
const targets = raw.GITHUB_TARGETS?.trim();
21+
if (targets) {
22+
return targets.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
23+
}
24+
const legacy = raw.GITHUB_ORG?.trim();
25+
if (legacy) {
26+
return [legacy];
27+
}
28+
return [];
29+
}
30+
31+
const authEnabledSchema = z.enum(['true', 'false']).default('true');
32+
33+
const envSchemaBase = z.object({
1634
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
1735
PORT: z.string().default('3001'),
1836
DATABASE_URL: z.string().optional(),
19-
37+
2038
// Logging configuration
2139
LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE']).default('INFO'),
22-
23-
// GitHub API access (for reading repositories and data)
24-
GITHUB_TOKEN: z.string(),
25-
GITHUB_ORG: z.string(),
26-
27-
// GitHub OAuth App (for user authentication)
28-
GITHUB_AUTH_CLIENT_ID: z.string(),
29-
GITHUB_AUTH_CLIENT_SECRET: z.string(),
30-
31-
// Session configuration
32-
SESSION_SECRET: z.string(),
33-
40+
41+
// GitHub API token (scanner / server-to-server operations)
42+
GITHUB_TOKEN: z.string().min(1),
43+
44+
/** Comma-separated logins or org names to scan */
45+
GITHUB_TARGETS: z.string().optional(),
46+
/** Legacy single org / user slug (fallback if GITHUB_TARGETS unset) */
47+
GITHUB_ORG: z.string().optional(),
48+
49+
/** When false, OAuth is not used; API accepts requests without logged-in GitHub identity */
50+
AUTH_ENABLED: authEnabledSchema,
51+
52+
GITHUB_AUTH_CLIENT_ID: z.string().optional(),
53+
GITHUB_AUTH_CLIENT_SECRET: z.string().optional(),
54+
/** GitHub team slug within each organization target; if unset, OAuth allows any member of the org */
55+
GITHUB_AUTH_TEAM_SLUG: z.string().optional(),
56+
57+
SESSION_SECRET: z.string().min(1),
58+
3459
FRONTEND_URL: z.string().default('http://localhost:5173'),
3560

3661
// Storage mode: 'database' or 'memory' (default: memory for easy startup)
@@ -50,12 +75,41 @@ const envSchema = z.object({
5075
USE_REDIS: z.enum(['true', 'false']).default('false'),
5176
});
5277

78+
const envSchema = envSchemaBase.superRefine((data, ctx) => {
79+
const targets = parseGithubTargetLoginList(data);
80+
if (targets.length === 0) {
81+
ctx.addIssue({
82+
code: z.ZodIssueCode.custom,
83+
message: 'Set GITHUB_TARGETS (comma-separated) and/or GITHUB_ORG to at least one owner',
84+
path: ['GITHUB_TARGETS'],
85+
});
86+
}
87+
88+
if (data.AUTH_ENABLED === 'true') {
89+
if (!data.GITHUB_AUTH_CLIENT_ID?.trim()) {
90+
ctx.addIssue({
91+
code: z.ZodIssueCode.custom,
92+
message: 'GITHUB_AUTH_CLIENT_ID is required when AUTH_ENABLED=true',
93+
path: ['GITHUB_AUTH_CLIENT_ID'],
94+
});
95+
}
96+
if (!data.GITHUB_AUTH_CLIENT_SECRET?.trim()) {
97+
ctx.addIssue({
98+
code: z.ZodIssueCode.custom,
99+
message: 'GITHUB_AUTH_CLIENT_SECRET is required when AUTH_ENABLED=true',
100+
path: ['GITHUB_AUTH_CLIENT_SECRET'],
101+
});
102+
}
103+
}
104+
});
105+
53106
export const env = envSchema.parse(process.env);
54107

55108
// Determine storage mode: use database only if explicitly set AND DATABASE_URL is provided
56-
const effectiveStorageMode = env.STORAGE_MODE === 'database' && env.DATABASE_URL
57-
? 'database'
58-
: 'memory';
109+
const effectiveStorageMode =
110+
env.STORAGE_MODE === 'database' && env.DATABASE_URL ? 'database' : 'memory';
111+
112+
const githubTargets = parseGithubTargetLoginList(env);
59113

60114
export const config = {
61115
nodeEnv: env.NODE_ENV,
@@ -67,11 +121,17 @@ export const config = {
67121
},
68122
github: {
69123
token: env.GITHUB_TOKEN,
70-
org: env.GITHUB_ORG,
124+
/** Normalized list of org and/or user slugs to scan */
125+
targets: githubTargets,
126+
/** @deprecated first target only — use `targets` */
127+
org: githubTargets[0] ?? '',
71128
},
72129
auth: {
73-
clientId: env.GITHUB_AUTH_CLIENT_ID,
74-
clientSecret: env.GITHUB_AUTH_CLIENT_SECRET,
130+
enabled: env.AUTH_ENABLED === 'true',
131+
clientId: env.GITHUB_AUTH_CLIENT_ID?.trim() ?? '',
132+
clientSecret: env.GITHUB_AUTH_CLIENT_SECRET?.trim() ?? '',
133+
/** Trimmed team slug, or empty string to enforce org membership only */
134+
teamSlug: env.GITHUB_AUTH_TEAM_SLUG?.trim() ?? '',
75135
sessionSecret: env.SESSION_SECRET,
76136
},
77137
frontendUrl: env.FRONTEND_URL,
@@ -83,7 +143,9 @@ export const config = {
83143
},
84144
scan: {
85145
specificRepos: env.SCAN_REPOS
86-
? env.SCAN_REPOS.split(',').map(r => r.trim()).filter(r => r.length > 0)
146+
? env.SCAN_REPOS.split(',')
147+
.map((r) => r.trim())
148+
.filter((r) => r.length > 0)
87149
: undefined,
88150
},
89151
redis: {

backend/src/middleware/auth.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { Request, Response, NextFunction } from 'express';
1+
import { NextFunction, Request, Response } from 'express';
2+
3+
import { config } from '../config/env.js';
24

35
// Custom error class for auth errors
46
export class AuthError extends Error {
@@ -13,6 +15,10 @@ export class AuthError extends Error {
1315

1416
// Middleware to check if user is authenticated
1517
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
18+
if (!config.auth.enabled) {
19+
next();
20+
return;
21+
}
1622
if (!req.session.user) {
1723
res.status(401).json({
1824
error: 'Unauthorized',

0 commit comments

Comments
 (0)