From f2c1182b180f6e9b74bf58e67a08b2c3218434d2 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 4 Feb 2025 13:11:04 +0900 Subject: [PATCH 01/93] feat(auth): add NotAuthorized and NotFound pages --- src/ui/views/Extras/NotAuthorized.jsx | 39 +++++++++++++++++++++++++++ src/ui/views/Extras/NotFound.jsx | 36 +++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/ui/views/Extras/NotAuthorized.jsx create mode 100644 src/ui/views/Extras/NotFound.jsx diff --git a/src/ui/views/Extras/NotAuthorized.jsx b/src/ui/views/Extras/NotAuthorized.jsx new file mode 100644 index 000000000..f08c478b1 --- /dev/null +++ b/src/ui/views/Extras/NotAuthorized.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import Card from '../../components/Card/Card'; +import CardBody from '../../components/Card/CardBody'; +import GridContainer from '../../components/Grid/GridContainer'; +import GridItem from '../../components/Grid/GridItem'; +import { Button } from '@material-ui/core'; +import LockIcon from '@material-ui/icons/Lock'; + +const NotAuthorized = () => { + const navigate = useNavigate(); + + return ( + + + + + +

403 - Not Authorized

+

+ You do not have permission to access this page. Contact your administrator for more + information, or try logging in with a different account. +

+ +
+
+
+
+ ); +}; + +export default NotAuthorized; diff --git a/src/ui/views/Extras/NotFound.jsx b/src/ui/views/Extras/NotFound.jsx new file mode 100644 index 000000000..d548200de --- /dev/null +++ b/src/ui/views/Extras/NotFound.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import Card from '../../components/Card/Card'; +import CardBody from '../../components/Card/CardBody'; +import GridContainer from '../../components/Grid/GridContainer'; +import GridItem from '../../components/Grid/GridItem'; +import { Button } from '@material-ui/core'; +import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; + +const NotFound = () => { + const navigate = useNavigate(); + + return ( + + + + + +

404 - Page Not Found

+

The page you are looking for does not exist. It may have been moved or deleted.

+ +
+
+
+
+ ); +}; + +export default NotFound; From 256523f4a0704c5cd10cac0f0014aa5ffdf8aa61 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 4 Feb 2025 13:56:32 +0900 Subject: [PATCH 02/93] feat(auth): add AuthProvider and PrivateRoute wrapper --- src/ui/auth/AuthProvider.tsx | 49 +++++++++++++++++++ .../components/PrivateRoute/PrivateRoute.tsx | 27 ++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/ui/auth/AuthProvider.tsx create mode 100644 src/ui/components/PrivateRoute/PrivateRoute.tsx diff --git a/src/ui/auth/AuthProvider.tsx b/src/ui/auth/AuthProvider.tsx new file mode 100644 index 000000000..1da89df51 --- /dev/null +++ b/src/ui/auth/AuthProvider.tsx @@ -0,0 +1,49 @@ +import React, { createContext, useContext, useState, useEffect } from 'react'; +import { getUserInfo } from '../services/auth'; + +// Interface for when we convert to TypeScript +// interface AuthContextType { +// user: any; +// setUser: (user: any) => void; +// refreshUser: () => Promise; +// isLoading: boolean; +// } + +const AuthContext = createContext(undefined); + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const refreshUser = async () => { + console.log('Refreshing user'); + try { + const data = await getUserInfo(); + setUser(data); + console.log('User refreshed:', data); + } catch (error) { + console.error('Error refreshing user:', error); + setUser(null); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + refreshUser(); + }, []); + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/PrivateRoute/PrivateRoute.tsx new file mode 100644 index 000000000..05da922ce --- /dev/null +++ b/src/ui/components/PrivateRoute/PrivateRoute.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../../auth/AuthProvider'; + +const PrivateRoute = ({ component: Component, adminOnly = false }) => { + const { user, isLoading } = useAuth(); + console.debug('PrivateRoute', { user, isLoading, adminOnly }); + + if (isLoading) { + console.debug('Auth is loading, waiting'); + return
Loading...
; // TODO: Add loading spinner + } + + if (!user) { + console.debug('User not logged in, redirecting to login page'); + return ; + } + + if (adminOnly && !user.isAdmin) { + console.debug('User is not an admin, redirecting to not authorized page'); + return ; + } + + return ; +}; + +export default PrivateRoute; From 516ed7fe5fb81656af9387aac09a00651d1c2883 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 4 Feb 2025 14:02:55 +0900 Subject: [PATCH 03/93] chore(auth): rename userLoggedIn endpoint --- src/service/routes/auth.js | 2 +- src/ui/services/user.js | 2 +- test/testLogin.test.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/service/routes/auth.js b/src/service/routes/auth.js index d79855905..744b17798 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.js @@ -142,7 +142,7 @@ router.post('/gitAccount', async (req, res) => { } }); -router.get('/userLoggedIn', async (req, res) => { +router.get('/me', async (req, res) => { if (req.user) { const user = JSON.parse(JSON.stringify(req.user)); if (user && user.password) delete user.password; diff --git a/src/ui/services/user.js b/src/ui/services/user.js index 04a2fdccb..cab1dc3ea 100644 --- a/src/ui/services/user.js +++ b/src/ui/services/user.js @@ -77,7 +77,7 @@ const updateUser = async (data) => { }; const getUserLoggedIn = async (setIsLoading, setIsAdmin, setIsError, setAuth) => { - const url = new URL(`${baseUrl}/api/auth/userLoggedIn`); + const url = new URL(`${baseUrl}/api/auth/me`); await axios(url.toString(), { withCredentials: true }) .then((response) => { diff --git a/test/testLogin.test.js b/test/testLogin.test.js index 812e4f755..833184e0b 100644 --- a/test/testLogin.test.js +++ b/test/testLogin.test.js @@ -43,7 +43,7 @@ describe('auth', async () => { }); it('should now be able to access the user login metadata', async function () { - const res = await chai.request(app).get('/api/auth/userLoggedIn').set('Cookie', `${cookie}`); + const res = await chai.request(app).get('/api/auth/me').set('Cookie', `${cookie}`); res.should.have.status(200); }); From 47e95d440d2fd410d28228a874ea701577814e62 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 4 Feb 2025 14:08:54 +0900 Subject: [PATCH 04/93] chore(auth): refactor routes to use PrivateRoute guard --- src/index.jsx | 21 ++++++++++++++------- src/routes.jsx | 42 +++++++++++++++++++++-------------------- src/ui/services/auth.js | 22 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 27 deletions(-) create mode 100644 src/ui/services/auth.js diff --git a/src/index.jsx b/src/index.jsx index 4aca4983b..316d7dda2 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -2,21 +2,28 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { createBrowserHistory } from 'history'; import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom'; +import { AuthProvider } from './ui/auth/AuthProvider'; // core components import Admin from './ui/layouts/Admin'; import Login from './ui/views/Login/Login'; import './ui/assets/css/material-dashboard-react.css'; +import NotAuthorized from './ui/views/Extras/NotAuthorized'; +import NotFound from './ui/views/Extras/NotFound'; const hist = createBrowserHistory(); ReactDOM.render( - - - } /> - } /> - } /> - - , + + + + } /> + } /> + } /> + } /> + } /> + + + , document.getElementById('root'), ); diff --git a/src/routes.jsx b/src/routes.jsx index 526b452aa..ed12a7241 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -16,6 +16,8 @@ */ +import React from 'react'; +import PrivateRoute from './ui/components/PrivateRoute/PrivateRoute'; import Person from '@material-ui/icons/Person'; import OpenPushRequests from './ui/views/OpenPushRequests/OpenPushRequests'; import PushDetails from './ui/views/PushDetails/PushDetails'; @@ -33,15 +35,23 @@ const dashboardRoutes = [ path: '/repo', name: 'Repositories', icon: RepoIcon, - component: RepoList, + component: (props) => , layout: '/admin', visible: true, }, + { + path: '/repo/:id', + name: 'Repo Details', + icon: Person, + component: (props) => , + layout: '/admin', + visible: false, + }, { path: '/push', name: 'Dashboard', icon: Dashboard, - component: OpenPushRequests, + component: (props) => , layout: '/admin', visible: true, }, @@ -49,7 +59,7 @@ const dashboardRoutes = [ path: '/push/:id', name: 'Open Push Requests', icon: Person, - component: PushDetails, + component: (props) => , layout: '/admin', visible: false, }, @@ -57,34 +67,26 @@ const dashboardRoutes = [ path: '/profile', name: 'My Account', icon: AccountCircle, - component: User, + component: (props) => , layout: '/admin', visible: true, }, { - path: '/user/:id', - name: 'User', - icon: Person, - component: User, + path: '/user', + name: 'Users', + icon: Group, + component: (props) => , layout: '/admin', - visible: false, + visible: true, }, { - path: '/repo/:id', - name: 'Repo Details', + path: '/user/:id', + name: 'User', icon: Person, - component: RepoDetails, + component: (props) => , layout: '/admin', visible: false, }, - { - path: '/user', - name: 'Users', - icon: Group, - component: UserList, - layout: '/admin', - visible: true, - }, ]; export default dashboardRoutes; diff --git a/src/ui/services/auth.js b/src/ui/services/auth.js new file mode 100644 index 000000000..e1155e9f5 --- /dev/null +++ b/src/ui/services/auth.js @@ -0,0 +1,22 @@ +const baseUrl = import.meta.env.VITE_API_URI + ? `${import.meta.env.VITE_API_URI}` + : `${location.origin}`; + +/** + * Gets the current user's information + * @return {Promise} The user's information + */ +export const getUserInfo = async () => { + try { + const response = await fetch(`${baseUrl}/api/auth/me`, { + credentials: 'include', // Sends cookies + }); + + if (!response.ok) throw new Error(`Failed to fetch user info: ${response.statusText}`); + + return await response.json(); + } catch (error) { + console.error('Error fetching user info:', error); + return null; + } +}; From 893e0b132fd419ac35881103231df57005865b58 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 7 Feb 2025 16:05:35 +0900 Subject: [PATCH 05/93] fix(auth): temporary fix for username edit redirect --- src/ui/views/User/User.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/views/User/User.jsx b/src/ui/views/User/User.jsx index c8b46ebe5..e78bf9d89 100644 --- a/src/ui/views/User/User.jsx +++ b/src/ui/views/User/User.jsx @@ -60,7 +60,7 @@ export default function Dashboard() { try { data.gitAccount = escapeHTML(gitAccount); await updateUser(data); - navigate(`/admin/user/${data.username}`); + navigate(`/admin/profile`); } catch { setIsError(true); } From d049463d403d6a94a2b70c6116e36bc13cdc623f Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 7 Feb 2025 16:14:08 +0900 Subject: [PATCH 06/93] fix(auth): access user admin status correctly --- src/ui/components/PrivateRoute/PrivateRoute.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/PrivateRoute/PrivateRoute.tsx index 05da922ce..4e7a2f4bf 100644 --- a/src/ui/components/PrivateRoute/PrivateRoute.tsx +++ b/src/ui/components/PrivateRoute/PrivateRoute.tsx @@ -16,7 +16,7 @@ const PrivateRoute = ({ component: Component, adminOnly = false }) => { return ; } - if (adminOnly && !user.isAdmin) { + if (adminOnly && !user.admin) { console.debug('User is not an admin, redirecting to not authorized page'); return ; } From db04af6d2bd41371a218c9e0b8c6f72cd5b201b4 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 11 Feb 2025 12:46:24 +0900 Subject: [PATCH 07/93] chore(auth): refactor /admin routes into /dashboard --- cypress/e2e/repo.cy.js | 4 ++-- src/index.jsx | 6 +++--- .../processors/push-action/blockForAuth.js | 2 +- src/routes.jsx | 18 +++++++++--------- src/service/routes/auth.js | 2 +- src/ui/components/Sidebar/Sidebar.jsx | 2 +- src/ui/views/Login/Login.jsx | 8 ++++---- .../components/PushesTable.jsx | 2 +- src/ui/views/PushDetails/PushDetails.jsx | 10 +++++----- .../PushDetails/components/AttestationView.jsx | 4 ++-- src/ui/views/RepoDetails/RepoDetails.jsx | 6 +++--- .../views/RepoList/Components/RepoOverview.jsx | 2 +- .../views/RepoList/Components/Repositories.jsx | 2 +- src/ui/views/User/User.jsx | 4 ++-- src/ui/views/UserList/Components/UserList.jsx | 2 +- 15 files changed, 37 insertions(+), 37 deletions(-) diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index 32c7d1cab..ea4bbce43 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -1,6 +1,6 @@ describe('Repo', () => { beforeEach(() => { - cy.visit('/admin/repo'); + cy.visit('/dashboard/repo'); // prevent failures on 404 request and uncaught promises cy.on('uncaught:exception', () => false); @@ -18,7 +18,7 @@ describe('Repo', () => { cy // find the entry for finos/test-repo - .get('a[href="/admin/repo/test-repo"]') + .get('a[href="/dashboard/repo/test-repo"]') // take it's parent row .closest('tr') // find the nearby span containing Code we can click to open the tooltip diff --git a/src/index.jsx b/src/index.jsx index 316d7dda2..04505ff5f 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -5,7 +5,7 @@ import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-d import { AuthProvider } from './ui/auth/AuthProvider'; // core components -import Admin from './ui/layouts/Admin'; +import Dashboard from './ui/layouts/Dashboard'; import Login from './ui/views/Login/Login'; import './ui/assets/css/material-dashboard-react.css'; import NotAuthorized from './ui/views/Extras/NotAuthorized'; @@ -17,10 +17,10 @@ ReactDOM.render( - } /> + } /> } /> } /> - } /> + } /> } /> diff --git a/src/proxy/processors/push-action/blockForAuth.js b/src/proxy/processors/push-action/blockForAuth.js index cc99d6b92..5b0b2db56 100644 --- a/src/proxy/processors/push-action/blockForAuth.js +++ b/src/proxy/processors/push-action/blockForAuth.js @@ -10,7 +10,7 @@ const exec = async (req, action) => { '\n\n\n' + `\x1B[32mGitProxy has received your push ✅\x1B[0m\n\n` + '🔗 Shareable Link\n\n' + - `\x1B[34m${url}/admin/push/${action.id}\x1B[0m` + + `\x1B[34m${url}/dashboard/push/${action.id}\x1B[0m` + '\n\n\n'; step.setAsyncBlock(message); diff --git a/src/routes.jsx b/src/routes.jsx index ed12a7241..a1204b735 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -36,7 +36,7 @@ const dashboardRoutes = [ name: 'Repositories', icon: RepoIcon, component: (props) => , - layout: '/admin', + layout: '/dashboard', visible: true, }, { @@ -44,7 +44,7 @@ const dashboardRoutes = [ name: 'Repo Details', icon: Person, component: (props) => , - layout: '/admin', + layout: '/dashboard', visible: false, }, { @@ -52,7 +52,7 @@ const dashboardRoutes = [ name: 'Dashboard', icon: Dashboard, component: (props) => , - layout: '/admin', + layout: '/dashboard', visible: true, }, { @@ -60,7 +60,7 @@ const dashboardRoutes = [ name: 'Open Push Requests', icon: Person, component: (props) => , - layout: '/admin', + layout: '/dashboard', visible: false, }, { @@ -68,23 +68,23 @@ const dashboardRoutes = [ name: 'My Account', icon: AccountCircle, component: (props) => , - layout: '/admin', + layout: '/dashboard', visible: true, }, { - path: '/user', + path: '/admin/user', name: 'Users', icon: Group, component: (props) => , - layout: '/admin', + layout: '/dashboard', visible: true, }, { - path: '/user/:id', + path: '/admin/user/:id', name: 'User', icon: Person, component: (props) => , - layout: '/admin', + layout: '/dashboard', visible: false, }, ]; diff --git a/src/service/routes/auth.js b/src/service/routes/auth.js index 744b17798..79103e305 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.js @@ -67,7 +67,7 @@ router.get('/oidc/callback', (req, res, next) => { return res.status(401).end(); } console.log('Logged in successfully. User:', user); - return res.redirect(`${uiHost}:${uiPort}/admin/profile`); + return res.redirect(`${uiHost}:${uiPort}/dashboard/profile`); }); })(req, res, next); }); diff --git a/src/ui/components/Sidebar/Sidebar.jsx b/src/ui/components/Sidebar/Sidebar.jsx index 174e31a4e..2ebef0fa5 100644 --- a/src/ui/components/Sidebar/Sidebar.jsx +++ b/src/ui/components/Sidebar/Sidebar.jsx @@ -73,7 +73,7 @@ export default function Sidebar(props) { ); const brand = (
- +
; + return ; } if (success) { - return ; + return ; } return ( @@ -169,8 +169,8 @@ export default function UserProfile() { diff --git a/src/ui/views/OpenPushRequests/components/PushesTable.jsx b/src/ui/views/OpenPushRequests/components/PushesTable.jsx index a01f169eb..a39c4f451 100644 --- a/src/ui/views/OpenPushRequests/components/PushesTable.jsx +++ b/src/ui/views/OpenPushRequests/components/PushesTable.jsx @@ -23,7 +23,7 @@ export default function PushesTable(props) { const [isError, setIsError] = useState(false); const navigate = useNavigate(); - const openPush = (push) => navigate(`/admin/push/${push}`, { replace: true }); + const openPush = (push) => navigate(`/dashboard/push/${push}`, { replace: true }); useEffect(() => { const query = {}; diff --git a/src/ui/views/PushDetails/PushDetails.jsx b/src/ui/views/PushDetails/PushDetails.jsx index 6f02d717b..dda54f76e 100644 --- a/src/ui/views/PushDetails/PushDetails.jsx +++ b/src/ui/views/PushDetails/PushDetails.jsx @@ -48,20 +48,20 @@ export default function Dashboard() { const authorise = async (attestationData) => { await authorisePush(id, setMessage, setUserAllowedToApprove, attestationData); if (isUserAllowedToApprove) { - navigate('/admin/push/'); + navigate('/dashboard/push/'); } }; const reject = async () => { await rejectPush(id, setMessage, setUserAllowedToReject); if (isUserAllowedToReject) { - navigate('/admin/push/'); + navigate('/dashboard/push/'); } }; const cancel = async () => { await cancelPush(id, setAuth, setIsError); - navigate(`/admin/push/`); + navigate(`/dashboard/push/`); }; if (isLoading) return
Loading...
; @@ -178,7 +178,7 @@ export default function Dashboard() { htmlColor='green' /> - +

- + {data.attestation.reviewer.gitAccount} {' '} approved this contribution diff --git a/src/ui/views/PushDetails/components/AttestationView.jsx b/src/ui/views/PushDetails/components/AttestationView.jsx index 70540ca76..9ccbfc8a8 100644 --- a/src/ui/views/PushDetails/components/AttestationView.jsx +++ b/src/ui/views/PushDetails/components/AttestationView.jsx @@ -62,7 +62,7 @@ export default function AttestationView(props) {

Prior to making this code contribution publicly accessible via GitHub, this code contribution was reviewed and approved by{' '} - + {props.data.reviewer.gitAccount} . As a reviewer, it was their responsibility to confirm that open sourcing this @@ -72,7 +72,7 @@ export default function AttestationView(props) {

- + {props.data.reviewer.gitAccount} {' '} approved this contribution{' '} diff --git a/src/ui/views/RepoDetails/RepoDetails.jsx b/src/ui/views/RepoDetails/RepoDetails.jsx index 9c91c1b68..56d80e278 100644 --- a/src/ui/views/RepoDetails/RepoDetails.jsx +++ b/src/ui/views/RepoDetails/RepoDetails.jsx @@ -51,7 +51,7 @@ export default function RepoDetails() { const removeRepository = async (name) => { await deleteRepo(name); - navigate('/admin/repo', { replace: true }); + navigate('/dashboard/repo', { replace: true }); }; const refresh = () => getRepo(setIsLoading, setData, setAuth, setIsError, repoName); @@ -151,7 +151,7 @@ export default function RepoDetails() { return ( - {row} + {row} {user.admin && ( @@ -196,7 +196,7 @@ export default function RepoDetails() { return ( - {row} + {row} {user.admin && ( diff --git a/src/ui/views/RepoList/Components/RepoOverview.jsx b/src/ui/views/RepoList/Components/RepoOverview.jsx index a431dc721..f35c85e15 100644 --- a/src/ui/views/RepoList/Components/RepoOverview.jsx +++ b/src/ui/views/RepoList/Components/RepoOverview.jsx @@ -591,7 +591,7 @@ export default function Repositories(props) {

- + {props.data.project}/{props.data.name} diff --git a/src/ui/views/RepoList/Components/Repositories.jsx b/src/ui/views/RepoList/Components/Repositories.jsx index 4970858ec..543f7df10 100644 --- a/src/ui/views/RepoList/Components/Repositories.jsx +++ b/src/ui/views/RepoList/Components/Repositories.jsx @@ -21,7 +21,7 @@ export default function Repositories(props) { const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const navigate = useNavigate(); - const openRepo = (repo) => navigate(`/admin/repo/${repo}`, { replace: true }); + const openRepo = (repo) => navigate(`/dashboard/repo/${repo}`, { replace: true }); const { user } = useContext(UserContext); useEffect(() => { diff --git a/src/ui/views/User/User.jsx b/src/ui/views/User/User.jsx index e78bf9d89..44fa64e1c 100644 --- a/src/ui/views/User/User.jsx +++ b/src/ui/views/User/User.jsx @@ -52,7 +52,7 @@ export default function Dashboard() { if (isLoading) return
Loading...
; if (isError) return
Something went wrong ...
; - if (!auth && window.location.pathname === '/admin/profile') { + if (!auth && window.location.pathname === '/dashboard/profile') { return ; } @@ -60,7 +60,7 @@ export default function Dashboard() { try { data.gitAccount = escapeHTML(gitAccount); await updateUser(data); - navigate(`/admin/profile`); + navigate(`/dashboard/profile`); } catch { setIsError(true); } diff --git a/src/ui/views/UserList/Components/UserList.jsx b/src/ui/views/UserList/Components/UserList.jsx index b1273cb58..988431125 100644 --- a/src/ui/views/UserList/Components/UserList.jsx +++ b/src/ui/views/UserList/Components/UserList.jsx @@ -25,7 +25,7 @@ export default function UserList(props) { const [isError, setIsError] = useState(false); const navigate = useNavigate(); - const openUser = (username) => navigate(`/admin/user/${username}`, { replace: true }); + const openUser = (username) => navigate(`/dashboard/admin/user/${username}`, { replace: true }); useEffect(() => { const query = {}; From 9736801f4c947d5ecc552d78cecc5dd1bdefb6eb Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 11 Feb 2025 12:48:29 +0900 Subject: [PATCH 08/93] chore(auth): rename Admin files into Dashboard --- .../layouts/{adminStyle.js => dashboardStyle.js} | 0 .../{AdminNavbarLinks.jsx => DashboardNavbarLinks.jsx} | 4 ++-- src/ui/components/Navbars/Navbar.jsx | 4 ++-- src/ui/layouts/{Admin.jsx => Dashboard.jsx} | 10 +++++----- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/ui/assets/jss/material-dashboard-react/layouts/{adminStyle.js => dashboardStyle.js} (100%) rename src/ui/components/Navbars/{AdminNavbarLinks.jsx => DashboardNavbarLinks.jsx} (97%) rename src/ui/layouts/{Admin.jsx => Dashboard.jsx} (93%) diff --git a/src/ui/assets/jss/material-dashboard-react/layouts/adminStyle.js b/src/ui/assets/jss/material-dashboard-react/layouts/dashboardStyle.js similarity index 100% rename from src/ui/assets/jss/material-dashboard-react/layouts/adminStyle.js rename to src/ui/assets/jss/material-dashboard-react/layouts/dashboardStyle.js diff --git a/src/ui/components/Navbars/AdminNavbarLinks.jsx b/src/ui/components/Navbars/DashboardNavbarLinks.jsx similarity index 97% rename from src/ui/components/Navbars/AdminNavbarLinks.jsx rename to src/ui/components/Navbars/DashboardNavbarLinks.jsx index 1821f52c1..a3e6e4177 100644 --- a/src/ui/components/Navbars/AdminNavbarLinks.jsx +++ b/src/ui/components/Navbars/DashboardNavbarLinks.jsx @@ -19,7 +19,7 @@ import { getCookie } from '../../utils'; const useStyles = makeStyles(styles); -export default function AdminNavbarLinks() { +export default function DashboardNavbarLinks() { const classes = useStyles(); const navigate = useNavigate(); const [openProfile, setOpenProfile] = React.useState(null); @@ -44,7 +44,7 @@ export default function AdminNavbarLinks() { }; const showProfile = () => { - navigate('/admin/profile', { replace: true }); + navigate('/dashboard/profile', { replace: true }); }; const logout = () => { diff --git a/src/ui/components/Navbars/Navbar.jsx b/src/ui/components/Navbars/Navbar.jsx index e3925bc8f..44fd6bd08 100644 --- a/src/ui/components/Navbars/Navbar.jsx +++ b/src/ui/components/Navbars/Navbar.jsx @@ -7,7 +7,7 @@ import Toolbar from '@material-ui/core/Toolbar'; import IconButton from '@material-ui/core/IconButton'; import Hidden from '@material-ui/core/Hidden'; import Menu from '@material-ui/icons/Menu'; -import AdminNavbarLinks from './AdminNavbarLinks'; +import DashboardNavbarLinks from './DashboardNavbarLinks'; import styles from '../../assets/jss/material-dashboard-react/components/headerStyle'; const useStyles = makeStyles(styles); @@ -42,7 +42,7 @@ export default function Header(props) {
- + diff --git a/src/ui/layouts/Admin.jsx b/src/ui/layouts/Dashboard.jsx similarity index 93% rename from src/ui/layouts/Admin.jsx rename to src/ui/layouts/Dashboard.jsx index b08c6bab0..abcc053af 100644 --- a/src/ui/layouts/Admin.jsx +++ b/src/ui/layouts/Dashboard.jsx @@ -7,7 +7,7 @@ import Navbar from '../components/Navbars/Navbar'; import Footer from '../components/Footer/Footer'; import Sidebar from '../components/Sidebar/Sidebar'; import routes from '../../routes'; -import styles from '../assets/jss/material-dashboard-react/layouts/adminStyle'; +import styles from '../assets/jss/material-dashboard-react/layouts/dashboardStyle'; import logo from '../assets/img/git-proxy.png'; import { UserContext } from '../../context'; import { getUser } from '../services/user'; @@ -18,18 +18,18 @@ let refresh = false; const switchRoutes = ( {routes.map((prop, key) => { - if (prop.layout === '/admin') { + if (prop.layout === '/dashboard') { return } key={key} />; } return null; })} - } /> + } /> ); const useStyles = makeStyles(styles); -export default function Admin({ ...rest }) { +export default function Dashboard({ ...rest }) { // styles const classes = useStyles(); // ref to help us initialize PerfectScrollbar on windows devices @@ -43,7 +43,7 @@ export default function Admin({ ...rest }) { setMobileOpen(!mobileOpen); }; const getRoute = () => { - return window.location.pathname !== '/admin/maps'; + return window.location.pathname !== '/dashboard/maps'; }; const resizeFunction = () => { if (window.innerWidth >= 960) { From e7ec1f0bd3843b54772acfe9d84efb7115d22592 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 11 Feb 2025 13:35:56 +0900 Subject: [PATCH 09/93] test(auth): Add default login redirect E2E test --- cypress/e2e/login.cy.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js index 590506f62..16da32f83 100644 --- a/cypress/e2e/login.cy.js +++ b/cypress/e2e/login.cy.js @@ -19,6 +19,18 @@ describe('Login page', () => { cy.get('[data-test="login"]').should('exist'); }); + it('should redirect to repo list on valid login', () => { + cy.intercept('GET', '**/api/auth/me').as('getUser'); + + cy.get('[data-test="username"]').type('admin'); + cy.get('[data-test="password"]').type('admin'); + cy.get('[data-test="login"]').click(); + + cy.wait('@getUser'); + + cy.url().should('include', '/dashboard/repo'); + }) + describe('OIDC login button', () => { it('should exist', () => { cy.get('[data-test="oidc-login"]').should('exist'); From 3702acde201d39bdf73d1af0e0a84113a33b94b6 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 11 Feb 2025 13:39:00 +0900 Subject: [PATCH 10/93] fix(auth): fix redirect on local login --- src/ui/views/Login/Login.jsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/ui/views/Login/Login.jsx b/src/ui/views/Login/Login.jsx index 2bd697133..0be3781a6 100644 --- a/src/ui/views/Login/Login.jsx +++ b/src/ui/views/Login/Login.jsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; // @material-ui/core components import FormControl from '@material-ui/core/FormControl'; import InputLabel from '@material-ui/core/InputLabel'; @@ -12,10 +13,10 @@ import CardHeader from '../../components/Card/CardHeader'; import CardBody from '../../components/Card/CardBody'; import CardFooter from '../../components/Card/CardFooter'; import axios from 'axios'; -import { Navigate } from 'react-router-dom'; import logo from '../../assets/img/git-proxy.png'; import { Badge, CircularProgress, Snackbar } from '@material-ui/core'; import { getCookie } from '../../utils'; +import { useAuth } from '../../auth/AuthProvider'; const loginUrl = `${import.meta.env.VITE_API_URI}/api/auth/login`; @@ -27,6 +28,9 @@ export default function UserProfile() { const [gitAccountError, setGitAccountError] = useState(false); const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const { refreshUser } = useAuth(); + function validateForm() { return ( username.length > 0 && username.length < 100 && password.length > 0 && password.length < 200 @@ -57,8 +61,8 @@ export default function UserProfile() { .then(function () { window.sessionStorage.setItem('git.proxy.login', 'success'); setMessage('Success!'); - setSuccess(true); setIsLoading(false); + refreshUser().then(() => navigate('/dashboard/repo')); }) .catch(function (error) { if (error.response.status === 307) { @@ -75,13 +79,6 @@ export default function UserProfile() { event.preventDefault(); } - if (gitAccountError) { - return ; - } - if (success) { - return ; - } - return (
Date: Tue, 11 Feb 2025 13:39:55 +0900 Subject: [PATCH 11/93] fix(auth): improve error handling on repos page --- src/ui/views/RepoList/Components/RepoOverview.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/views/RepoList/Components/RepoOverview.jsx b/src/ui/views/RepoList/Components/RepoOverview.jsx index f35c85e15..9e98886df 100644 --- a/src/ui/views/RepoList/Components/RepoOverview.jsx +++ b/src/ui/views/RepoList/Components/RepoOverview.jsx @@ -581,6 +581,9 @@ export default function Repositories(props) { .get(`https://api.github.com/repos/${props.data.project}/${props.data.name}`) .then((res) => { setGitHub(res.data); + }) + .catch((err) => { + console.error(`Error fetching GitHub repository ${props.data.project}/${props.data.name}: ${err}`); }); }; From 24ca06f2f558b8072080244ffd2d0b442c132600 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 11 Feb 2025 13:51:41 +0900 Subject: [PATCH 12/93] test(auth): fix failing test (login before accessing page) --- cypress/e2e/repo.cy.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index ea4bbce43..d27bf09c2 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -1,5 +1,16 @@ describe('Repo', () => { beforeEach(() => { + // Log in with default admin user + cy.visit('/login'); + + cy.intercept('GET', '**/api/auth/me').as('getUser'); + + cy.get('[data-test="username"]').type('admin'); + cy.get('[data-test="password"]').type('admin'); + cy.get('[data-test="login"]').click(); + + cy.wait('@getUser'); + cy.visit('/dashboard/repo'); // prevent failures on 404 request and uncaught promises From 2146bc0a8a430509904a2e9988a4a519190a8900 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 11 Feb 2025 14:28:16 +0900 Subject: [PATCH 13/93] test(auth): fix login command and simplify login flow --- cypress/e2e/repo.cy.js | 11 +---------- cypress/support/commands.js | 6 +++++- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index d27bf09c2..411397128 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -1,15 +1,6 @@ describe('Repo', () => { beforeEach(() => { - // Log in with default admin user - cy.visit('/login'); - - cy.intercept('GET', '**/api/auth/me').as('getUser'); - - cy.get('[data-test="username"]').type('admin'); - cy.get('[data-test="password"]').type('admin'); - cy.get('[data-test="login"]').click(); - - cy.wait('@getUser'); + cy.login('admin', 'admin'); cy.visit('/dashboard/repo'); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index aa3b052c2..751eabdfa 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -29,9 +29,13 @@ Cypress.Commands.add('login', (username, password) => { cy.session([username, password], () => { cy.visit('/login'); + cy.intercept('GET', '**/api/auth/me').as('getUser'); + cy.get('[data-test=username]').type(username); cy.get('[data-test=password]').type(password); cy.get('[data-test=login]').click(); - cy.url().should('contain', '/admin/profile'); + + cy.wait('@getUser'); + cy.url().should('include', '/dashboard/repo'); }); }); From 884f0a60f52b959cf7e6dbb05160b32e61aab0ec Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 16 Feb 2025 16:31:43 +0900 Subject: [PATCH 14/93] feat(auth): refactor and improve existing auth methods --- src/service/passport/activeDirectory.js | 90 ++++++++++++++----------- src/service/passport/local.js | 80 +++++++++++----------- src/service/passport/oidc.js | 34 +++++----- src/service/routes/auth.js | 4 +- 4 files changed, 109 insertions(+), 99 deletions(-) diff --git a/src/service/passport/activeDirectory.js b/src/service/passport/activeDirectory.js index 466f57b16..372868133 100644 --- a/src/service/passport/activeDirectory.js +++ b/src/service/passport/activeDirectory.js @@ -1,16 +1,19 @@ -const configure = () => { - const passport = require('passport'); - const ActiveDirectoryStrategy = require('passport-activedirectory'); - const config = require('../../config').getAuthentication(); - const adConfig = config.adConfig; +const ActiveDirectoryStrategy = require('passport-activedirectory'); +const ldaphelper = require('./ldaphelper'); + +const configure = (passport) => { const db = require('../../db'); - const userGroup = config.userGroup; - const adminGroup = config.adminGroup; - const domain = config.domain; + + // We can refactor this by normalizing auth strategy config and pass it directly into the configure() function, + // ideally when we convert this to TS. + const authMethods = require('../../config').getAuthMethods(); + const config = authMethods.find((method) => method.type.toLowerCase() === "activeDirectory"); + const adConfig = config.adConfig; + + const { userGroup, adminGroup, domain } = config; console.log(`AD User Group: ${userGroup}, AD Admin Group: ${adminGroup}`); - const ldaphelper = require('./ldaphelper'); passport.use( new ActiveDirectoryStrategy( { @@ -19,42 +22,47 @@ const configure = () => { ldap: adConfig, }, async function (req, profile, ad, done) { - profile.username = profile._json.sAMAccountName.toLowerCase(); - profile.email = profile._json.mail; - profile.id = profile.username; - req.user = profile; - - console.log( - `passport.activeDirectory: resolved login ${ - profile._json.userPrincipalName - }, profile=${JSON.stringify(profile)}`, - ); - // First check to see if the user is in the usergroups - const isUser = await ldaphelper.isUserInAdGroup(profile.username, domain, userGroup); - - if (!isUser) { - const message = `User it not a member of ${userGroup}`; - return done(message, null); - } + try { + profile.username = profile._json.sAMAccountName?.toLowerCase(); + profile.email = profile._json.mail; + profile.id = profile.username; + req.user = profile; - // Now check if the user is an admin - const isAdmin = await ldaphelper.isUserInAdGroup(profile.username, domain, adminGroup); + console.log( + `passport.activeDirectory: resolved login ${ + profile._json.userPrincipalName + }, profile=${JSON.stringify(profile)}`, + ); + // First check to see if the user is in the usergroups + const isUser = await ldaphelper.isUserInAdGroup(profile.username, domain, userGroup); - profile.admin = isAdmin; - console.log(`passport.activeDirectory: ${profile.username} admin=${isAdmin}`); + if (!isUser) { + const message = `User it not a member of ${userGroup}`; + return done(message, null); + } - const user = { - username: profile.username, - admin: isAdmin, - email: profile._json.mail, - displayName: profile.displayName, - title: profile._json.title, - }; + // Now check if the user is an admin + const isAdmin = await ldaphelper.isUserInAdGroup(profile.username, domain, adminGroup); - await db.updateUser(user); + profile.admin = isAdmin; + console.log(`passport.activeDirectory: ${profile.username} admin=${isAdmin}`); - return done(null, user); - }, + const user = { + username: profile.username, + admin: isAdmin, + email: profile._json.mail, + displayName: profile.displayName, + title: profile._json.title, + }; + + await db.updateUser(user); + + return done(null, user); + } catch (err) { + console.log(`Error authenticating AD user: ${err.message}`); + return done(err, null); + } + } ), ); @@ -69,4 +77,4 @@ const configure = () => { return passport; }; -module.exports.configure = configure; +module.exports = { configure }; diff --git a/src/service/passport/local.js b/src/service/passport/local.js index 6bcce7e7e..ebe592b2e 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.js @@ -1,53 +1,53 @@ -const bcrypt = require('bcryptjs'); -/* eslint-disable max-len */ -const configure = async () => { - const passport = require('passport'); - const Strategy = require('passport-local').Strategy; - const db = require('../../db'); +const bcrypt = require("bcryptjs"); +const LocalStrategy = require("passport-local").Strategy; +const db = require("../../db"); +const configure = async (passport) => { passport.use( - new Strategy((username, password, cb) => { - db.findUser(username) - .then(async (user) => { - if (!user) { - return cb(null, false); - } - - const passwordCorrect = await bcrypt.compare(password, user.password); - - if (!passwordCorrect) { - return cb(null, false); - } - return cb(null, user); - }) - .catch((err) => { - return cb(err); - }); - }), + new LocalStrategy(async (username, password, done) => { + try { + const user = await db.findUser(username); + if (!user) { + return done(null, false, { message: "Incorrect username." }); + } + + const passwordCorrect = await bcrypt.compare(password, user.password); + if (!passwordCorrect) { + return done(null, false, { message: "Incorrect password." }); + } + + return done(null, user); + } catch (err) { + return done(err); + } + }) ); - passport.serializeUser(function (user, cb) { - cb(null, user.username); + passport.serializeUser((user, done) => { + done(null, user.username); }); - passport.deserializeUser(function (username, cb) { - db.findUser(username) - .then((user) => { - cb(null, user); - }) - .catch((err) => { - db(err, null); - }); + passport.deserializeUser(async (username, done) => { + try { + const user = await db.findUser(username); + done(null, user); + } catch (err) { + done(err, null); + } }); - const admin = await db.findUser('admin'); + passport.type = "local"; + return passport; +}; +/** + * Create the default admin user if it doesn't exist + */ +const createDefaultAdmin = async () => { + const admin = await db.findUser("admin"); if (!admin) { - await db.createUser('admin', 'admin', 'admin@place.com', 'none', true, true, true, true); + await db.createUser("admin", "admin", "admin@place.com", "none", true, true, true, true); } - - passport.type = 'local'; - return passport; }; -module.exports.configure = configure; +module.exports = { configure, createDefaultAdmin }; diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index 8e49866f5..e402725e6 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -1,20 +1,22 @@ -const configure = async () => { - const passport = require('passport'); - const { Strategy: OIDCStrategy } = require('passport-openidconnect'); +const { Strategy: OIDCStrategy } = require('passport-openidconnect'); + +const configure = async (passport) => { const db = require('../../db'); - const config = require('../../config').getAuthentication(); - const oidcConfig = config.oidcConfig; + // We can refactor this by normalizing auth strategy config and pass it directly into the configure() function, + // ideally when we convert this to TS. + const authMethods = require('../../config').getAuthMethods(); + const oidcConfig = authMethods.find((method) => method.type.toLowerCase() === "openidconnect")?.oidcConfig; passport.use( - new OIDCStrategy(oidcConfig, async function verify(issuer, profile, cb) { + new OIDCStrategy(oidcConfig, async function verify(issuer, profile, done) { try { const user = await db.findUserByOIDC(profile.id); if (!user) { const email = safelyExtractEmail(profile); if (!email) { - return cb(new Error('No email found in OIDC profile')); + return done(new Error('No email found in OIDC profile')); } const username = getUsername(email); @@ -33,25 +35,25 @@ const configure = async () => { newUser.oidcId, ); - return cb(null, newUser); + return done(null, newUser); } - return cb(null, user); + return done(null, user); } catch (err) { - return cb(err); + return done(err); } }), ); - passport.serializeUser((user, cb) => { - cb(null, user.oidcId || user.username); + passport.serializeUser((user, done) => { + done(null, user.oidcId || user.username); }); - passport.deserializeUser(async (id, cb) => { + passport.deserializeUser(async (id, done) => { try { const user = (await db.findUserByOIDC(id)) || (await db.findUser(id)); - cb(null, user); + done(null, user); } catch (err) { - cb(err); + done(err); } }); @@ -59,7 +61,7 @@ const configure = async () => { return passport; }; -module.exports.configure = configure; +module.exports = { configure }; /** * Extracts email from OIDC profile. diff --git a/src/service/routes/auth.js b/src/service/routes/auth.js index 79103e305..33b5fae35 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.js @@ -28,7 +28,7 @@ router.get('/', (req, res) => { }); }); -router.post('/login', passport.authenticate(passportType), async (req, res) => { +router.post('/login', passport.authenticate('local'), async (req, res) => { try { const currentUser = { ...req.user }; delete currentUser.password; @@ -48,7 +48,7 @@ router.post('/login', passport.authenticate(passportType), async (req, res) => { } }); -router.get('/oidc', passport.authenticate(passportType)); +router.get('/oidc', passport.authenticate('openidconnect')); router.get('/oidc/callback', (req, res, next) => { passport.authenticate(passportType, (err, user, info) => { From ce6023b381eeefd505b3235d6c09c41902682d1e Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 16 Feb 2025 16:35:52 +0900 Subject: [PATCH 15/93] feat(auth): configure passport with all enabled auth methods --- src/config/index.js | 24 +++++++++++--------- src/service/passport/index.js | 42 ++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 78184d413..ddc4545f5 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -70,27 +70,29 @@ const getDatabase = () => { throw Error('No database cofigured!'); }; -// Gets the configuared data sink, defaults to filesystem -const getAuthentication = () => { +/** + * Get the list of enabled authentication methods + * @returns {Array} List of enabled authentication methods + */ +const getAuthMethods = () => { if (_userSettings !== null && _userSettings.authentication) { _authentication = _userSettings.authentication; } - for (const ix in _authentication) { - if (!ix) continue; - const auth = _authentication[ix]; - if (auth.enabled) { - return auth; - } + + const enabledAuthMethods = _authentication.filter(auth => auth.enabled); + + if (enabledAuthMethods.length === 0) { + throw new Error("No authentication method enabled."); } - throw Error('No authentication cofigured!'); + return enabledAuthMethods; }; // Log configuration to console const logConfiguration = () => { console.log(`authorisedList = ${JSON.stringify(getAuthorisedList())}`); console.log(`data sink = ${JSON.stringify(getDatabase())}`); - console.log(`authentication = ${JSON.stringify(getAuthentication())}`); + console.log(`authentication = ${JSON.stringify(getAuthMethods())}`); }; const getAPIs = () => { @@ -202,7 +204,7 @@ exports.getProxyUrl = getProxyUrl; exports.getAuthorisedList = getAuthorisedList; exports.getDatabase = getDatabase; exports.logConfiguration = logConfiguration; -exports.getAuthentication = getAuthentication; +exports.getAuthMethods = getAuthMethods; exports.getTempPasswordConfig = getTempPasswordConfig; exports.getCookieSecret = getCookieSecret; exports.getSessionMaxAgeHours = getSessionMaxAgeHours; diff --git a/src/service/passport/index.js b/src/service/passport/index.js index 9a16ef2fc..5b7d593ff 100644 --- a/src/service/passport/index.js +++ b/src/service/passport/index.js @@ -1,31 +1,33 @@ +const passport = require("passport"); const local = require('./local'); const activeDirectory = require('./activeDirectory'); const oidc = require('./oidc'); const config = require('../../config'); -const authenticationConfig = config.getAuthentication(); -let _passport; + +const authStrategies = { + local: local, + activedirectory: activeDirectory, + openidconnect: oidc, +}; const configure = async () => { - const type = authenticationConfig.type.toLowerCase(); + passport.initialize(); - switch (type) { - case 'activedirectory': - _passport = await activeDirectory.configure(); - break; - case 'local': - _passport = await local.configure(); - break; - case 'openidconnect': - _passport = await oidc.configure(); - break; - default: - throw Error(`uknown authentication type ${type}`); + const authMethods = config.getAuthMethods(); + + for (const auth of authMethods) { + const strategy = authStrategies[auth.type.toLowerCase()]; + if (strategy && typeof strategy.configure === "function") { + await strategy.configure(passport); + } } - _passport.type = authenticationConfig.type; - return _passport; + + if (authMethods.some(auth => auth.type.toLowerCase() === "local")) { + await local.createDefaultAdmin(); + } + + return passport; }; module.exports.configure = configure; -module.exports.getPassport = () => { - return _passport; -}; +module.exports.getPassport = () => passport; From 68fa11544851c7eaf7bb5e7eea60fba105d4a49b Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sun, 16 Feb 2025 16:36:51 +0900 Subject: [PATCH 16/93] test(auth): fix tests for multiple auth methods --- test/testConfig.test.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/testConfig.test.js b/test/testConfig.test.js index 125ee7b44..e803adb69 100644 --- a/test/testConfig.test.js +++ b/test/testConfig.test.js @@ -11,8 +11,9 @@ describe('default configuration', function () { it('should use default values if no user-settings.json file exists', function () { const config = require('../src/config'); config.logConfiguration(); + const enabledMethods = defaultSettings.authentication.filter(method => method.enabled); - expect(config.getAuthentication()).to.be.eql(defaultSettings.authentication[0]); + expect(config.getAuthMethods()).to.deep.equal(enabledMethods); expect(config.getDatabase()).to.be.eql(defaultSettings.sink[0]); expect(config.getTempPasswordConfig()).to.be.eql(defaultSettings.tempPassword); expect(config.getAuthorisedList()).to.be.eql(defaultSettings.authorisedList); @@ -47,9 +48,10 @@ describe('user configuration', function () { fs.writeFileSync(tempUserFile, JSON.stringify(user)); const config = require('../src/config'); + const enabledMethods = defaultSettings.authentication.filter(method => method.enabled); expect(config.getAuthorisedList()).to.be.eql(user.authorisedList); - expect(config.getAuthentication()).to.be.eql(defaultSettings.authentication[0]); + expect(config.getAuthMethods()).to.deep.equal(enabledMethods); expect(config.getDatabase()).to.be.eql(defaultSettings.sink[0]); expect(config.getTempPasswordConfig()).to.be.eql(defaultSettings.tempPassword); }); @@ -66,9 +68,13 @@ describe('user configuration', function () { fs.writeFileSync(tempUserFile, JSON.stringify(user)); const config = require('../src/config'); + const authMethods = config.getAuthMethods(); + const googleAuth = authMethods.find(method => method.type === 'google'); - expect(config.getAuthentication()).to.be.eql(user.authentication[0]); - expect(config.getAuthentication()).to.not.be.eql(defaultSettings.authentication[0]); + expect(googleAuth).to.not.be.undefined; + expect(googleAuth.enabled).to.be.true; + expect(config.getAuthMethods()).to.deep.include(user.authentication[0]); + expect(config.getAuthMethods()).to.not.be.eql(defaultSettings.authentication); expect(config.getDatabase()).to.be.eql(defaultSettings.sink[0]); expect(config.getTempPasswordConfig()).to.be.eql(defaultSettings.tempPassword); }); @@ -85,10 +91,11 @@ describe('user configuration', function () { fs.writeFileSync(tempUserFile, JSON.stringify(user)); const config = require('../src/config'); + const enabledMethods = defaultSettings.authentication.filter(method => method.enabled); expect(config.getDatabase()).to.be.eql(user.sink[0]); expect(config.getDatabase()).to.not.be.eql(defaultSettings.sink[0]); - expect(config.getAuthentication()).to.be.eql(defaultSettings.authentication[0]); + expect(config.getAuthMethods()).to.deep.equal(enabledMethods); expect(config.getTempPasswordConfig()).to.be.eql(defaultSettings.tempPassword); }); From 627c0eade8f36b9dfc4127fb5a58551d1ac7b3ca Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 21 Feb 2025 14:17:20 +0900 Subject: [PATCH 17/93] fix(auth): refactor how auth strategies are loaded into API route middleware --- src/service/passport/local.js | 5 +++-- src/service/routes/auth.js | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/service/passport/local.js b/src/service/passport/local.js index ebe592b2e..746434c6c 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.js @@ -2,6 +2,8 @@ const bcrypt = require("bcryptjs"); const LocalStrategy = require("passport-local").Strategy; const db = require("../../db"); +const type = "local"; + const configure = async (passport) => { passport.use( new LocalStrategy(async (username, password, done) => { @@ -36,7 +38,6 @@ const configure = async (passport) => { } }); - passport.type = "local"; return passport; }; @@ -50,4 +51,4 @@ const createDefaultAdmin = async () => { } }; -module.exports = { configure, createDefaultAdmin }; +module.exports = { configure, createDefaultAdmin, type }; diff --git a/src/service/routes/auth.js b/src/service/routes/auth.js index e146e3d87..b6bce5fa6 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.js @@ -1,8 +1,8 @@ const express = require('express'); const router = new express.Router(); const passport = require('../passport').getPassport(); +const authStrategies = require('../passport').authStrategies; const db = require('../../db'); -const passportType = passport.type; const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 3000 } = process.env; router.get('/', (req, res) => { @@ -22,7 +22,7 @@ router.get('/', (req, res) => { }); }); -router.post('/login', passport.authenticate('local'), async (req, res) => { +router.post('/login', passport.authenticate(authStrategies['local'].type), async (req, res) => { try { const currentUser = { ...req.user }; delete currentUser.password; @@ -42,10 +42,10 @@ router.post('/login', passport.authenticate('local'), async (req, res) => { } }); -router.get('/oidc', passport.authenticate('openidconnect')); +router.get('/oidc', passport.authenticate(authStrategies['openidconnect'].type)); router.get('/oidc/callback', (req, res, next) => { - passport.authenticate(passportType, (err, user, info) => { + passport.authenticate(authStrategies['openidconnect'].type, (err, user, info) => { if (err) { console.error('Authentication error:', err); return res.status(401).end(); From 534beeb1514c82da8ce8d2b635ece7fb64d8d390 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 1 Mar 2025 15:32:13 +0900 Subject: [PATCH 18/93] feat(auth): add JWT strategy and fix --- src/service/passport/jwt.js | 26 ++++++++++++++++++++++++++ src/service/passport/oidc.js | 5 ++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/service/passport/jwt.js diff --git a/src/service/passport/jwt.js b/src/service/passport/jwt.js new file mode 100644 index 000000000..f2b2dadb1 --- /dev/null +++ b/src/service/passport/jwt.js @@ -0,0 +1,26 @@ +const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); + +const type = "jwt"; + +const configure = (passport) => { + const JWT_SECRET = process.env.JWT_SECRET; + + if (!JWT_SECRET) { + console.log('JWT secret not provided. Skipping JWT strategy registration.'); + return; // Skip JWT registration if not configured + } + + const opts = { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: JWT_SECRET, + }; + + passport.use('jwt', new JwtStrategy(opts, (jwtPayload, done) => { + if (!jwtPayload) return done(null, false); + return done(null, jwtPayload); + })); + + console.log('JWT strategy registered successfully.'); +}; + +module.exports = { configure, type }; diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index baf65a5a7..6681a6a8b 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -1,11 +1,10 @@ const openIdClient = require('openid-client'); const { Strategy } = require('openid-client/passport'); -const passport = require('passport'); const db = require('../../db'); let type; -const configure = async () => { +const configure = async (passport) => { const authMethods = require('../../config').getAuthMethods(); const oidcConfig = authMethods.find((method) => method.type.toLowerCase() === "openidconnect")?.oidcConfig; const { issuer, clientID, clientSecret, callbackURL, scope } = oidcConfig; @@ -26,7 +25,7 @@ const configure = async () => { const userInfo = await openIdClient.fetchUserInfo(config, tokenSet.access_token, expectedSub); handleUserAuthentication(userInfo, done); }); - + // currentUrl must be overridden to match the callback URL strategy.currentUrl = (request) => { const callbackUrl = new URL(callbackURL); From 8b1a17363a81ccdc40d925e86e3630dcc616c8f3 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 1 Mar 2025 15:33:23 +0900 Subject: [PATCH 19/93] feat(auth): add dynamicAuthHandler middleware --- package-lock.json | 275 +++++++++++++++++++-- package.json | 4 +- src/service/passport/dynamicAuthHandler.js | 36 +++ src/service/passport/index.js | 3 + src/service/routes/repo.js | 3 +- 5 files changed, 303 insertions(+), 18 deletions(-) create mode 100644 src/service/passport/dynamicAuthHandler.js diff --git a/package-lock.json b/package-lock.json index 65bf8342e..74cc6994e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,10 +38,12 @@ "moment": "^2.29.4", "mongodb": "^5.0.0", "nodemailer": "^6.6.1", - "openid-client": "^6.2.0", + "open": "^10.1.0", + "openid-client": "^6.3.1", "parse-diff": "^0.11.1", "passport": "^0.7.0", "passport-activedirectory": "^1.0.4", + "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "perfect-scrollbar": "^1.5.5", "prop-types": "15.8.1", @@ -4393,6 +4395,27 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5358,6 +5381,34 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -5398,6 +5449,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -5598,6 +5661,15 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7879,6 +7951,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7943,6 +8030,24 @@ "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -8224,6 +8329,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -8501,9 +8621,9 @@ } }, "node_modules/jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -8650,6 +8770,40 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -8800,6 +8954,27 @@ "dev": true, "license": "MIT" }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9026,11 +9201,40 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" }, "node_modules/lodash.kebabcase": { "version": "4.1.1", @@ -9055,8 +9259,7 @@ "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/lodash.snakecase": { "version": "4.1.1", @@ -9977,9 +10180,9 @@ } }, "node_modules/oauth4webapi": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.2.0.tgz", - "integrity": "sha512-2sYwQXuuzGKOHpnM7QL9BssDrly5gKCgJKTyrhmFIHzJRj0fFsr6GVJEdesmrX6NpMg2u63V4hJwRsZE6PUSSA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.0.tgz", + "integrity": "sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -10131,14 +10334,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/openid-client": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.2.0.tgz", - "integrity": "sha512-pvLVkLcRWNU7YuKKTto376rgL//+rn3ca0XRqsrQVN30lVlpXBPHhSLcGoM/hPbux5p+Ha4tdoz96eEYpyguOQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.3.1.tgz", + "integrity": "sha512-l+uRCCM+KaGKQmCWjrjlFHXa1husuc72OPCCyGB7VjHeEMVldfwsn4Pfb/5Xk51FRqbRakMbwfIUPiAMQDHaSw==", "license": "MIT", "dependencies": { - "jose": "^5.9.6", - "oauth4webapi": "^3.2.0" + "jose": "^5.10.0", + "oauth4webapi": "^3.3.0" }, "funding": { "url": "https://github.com/sponsors/panva" @@ -10342,6 +10563,16 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, "node_modules/passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -11163,6 +11394,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index cb4ffa98d..40956086b 100644 --- a/package.json +++ b/package.json @@ -59,10 +59,12 @@ "moment": "^2.29.4", "mongodb": "^5.0.0", "nodemailer": "^6.6.1", - "openid-client": "^6.2.0", + "open": "^10.1.0", + "openid-client": "^6.3.1", "parse-diff": "^0.11.1", "passport": "^0.7.0", "passport-activedirectory": "^1.0.4", + "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "perfect-scrollbar": "^1.5.5", "prop-types": "15.8.1", diff --git a/src/service/passport/dynamicAuthHandler.js b/src/service/passport/dynamicAuthHandler.js new file mode 100644 index 000000000..98a557887 --- /dev/null +++ b/src/service/passport/dynamicAuthHandler.js @@ -0,0 +1,36 @@ +const passport = require('./index').getPassport(); + +/** + * Dynamic authentication middleware that supports JWT and session auth. + * If JWT strategy is set up, it will be prioritized. Otherwise, it will fallback to session auth. + * If either strategy is successful, it calls the next middleware in the chain. + * @param {*} passport the passport instance + * @returns a middleware function that handles authentication dynamically + */ +const dynamicAuthHandler = () => { + return (req, res, next) => { + console.log(`Dynamic Auth triggered - Requested URL: ${req.originalUrl}`); + const hasJwtStrategy = !!passport._strategy('jwt'); + console.log('hasJwtStrategy: ' + hasJwtStrategy); + if (hasJwtStrategy) { + // Try JWT authentication first + passport.authenticate('jwt', { session: false }, (err, user, info) => { + console.log(`JWT Auth triggered - User: ${user} - Error: ${err}`); + if (err) return next(err); + if (user) { + req.user = user; // JWT authenticated user + return next(); + } + // Fallback to session if available + if (req.isAuthenticated && req.isAuthenticated()) return next(); + return res.status(401).json({ message: 'Unauthorized: Valid token or session required.' }); + })(req, res, next); + } else { + // Default to session auth + if (req.isAuthenticated && req.isAuthenticated()) return next(); + return res.status(401).json({ message: 'Unauthorized: No active session.' }); + } + }; +} + +module.exports = dynamicAuthHandler; diff --git a/src/service/passport/index.js b/src/service/passport/index.js index ce62719d2..7126ae10f 100644 --- a/src/service/passport/index.js +++ b/src/service/passport/index.js @@ -2,6 +2,7 @@ const passport = require("passport"); const local = require('./local'); const activeDirectory = require('./activeDirectory'); const oidc = require('./oidc'); +const jwt = require('./jwt'); const config = require('../../config'); // Allows obtaining strategy config function and type @@ -10,12 +11,14 @@ const authStrategies = { local: local, activedirectory: activeDirectory, openidconnect: oidc, + jwt: jwt, }; const configure = async () => { passport.initialize(); const authMethods = config.getAuthMethods(); + console.log(`authMethods: ${JSON.stringify(authMethods)}`); for (const auth of authMethods) { const strategy = authStrategies[auth.type.toLowerCase()]; diff --git a/src/service/routes/repo.js b/src/service/routes/repo.js index 1f6698e3b..36a7b88f2 100644 --- a/src/service/routes/repo.js +++ b/src/service/routes/repo.js @@ -2,8 +2,9 @@ const express = require('express'); const router = new express.Router(); const db = require('../../db'); const { getProxyURL } = require('../urls'); +const dynamicAuthHandler = require('../passport/dynamicAuthHandler'); -router.get('/', async (req, res) => { +router.get('/', dynamicAuthHandler(), async (req, res) => { const proxyURL = getProxyURL(req); const query = { type: 'push', From 6265bbd60de69bd3ab527a334b42ea351a8147cc Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 13:28:56 +0900 Subject: [PATCH 20/93] feat(auth): update JWT auth middleware --- src/service/passport/dynamicAuthHandler.js | 36 ------- src/service/passport/jwtAuthHandler.js | 106 +++++++++++++++++++++ 2 files changed, 106 insertions(+), 36 deletions(-) delete mode 100644 src/service/passport/dynamicAuthHandler.js create mode 100644 src/service/passport/jwtAuthHandler.js diff --git a/src/service/passport/dynamicAuthHandler.js b/src/service/passport/dynamicAuthHandler.js deleted file mode 100644 index 98a557887..000000000 --- a/src/service/passport/dynamicAuthHandler.js +++ /dev/null @@ -1,36 +0,0 @@ -const passport = require('./index').getPassport(); - -/** - * Dynamic authentication middleware that supports JWT and session auth. - * If JWT strategy is set up, it will be prioritized. Otherwise, it will fallback to session auth. - * If either strategy is successful, it calls the next middleware in the chain. - * @param {*} passport the passport instance - * @returns a middleware function that handles authentication dynamically - */ -const dynamicAuthHandler = () => { - return (req, res, next) => { - console.log(`Dynamic Auth triggered - Requested URL: ${req.originalUrl}`); - const hasJwtStrategy = !!passport._strategy('jwt'); - console.log('hasJwtStrategy: ' + hasJwtStrategy); - if (hasJwtStrategy) { - // Try JWT authentication first - passport.authenticate('jwt', { session: false }, (err, user, info) => { - console.log(`JWT Auth triggered - User: ${user} - Error: ${err}`); - if (err) return next(err); - if (user) { - req.user = user; // JWT authenticated user - return next(); - } - // Fallback to session if available - if (req.isAuthenticated && req.isAuthenticated()) return next(); - return res.status(401).json({ message: 'Unauthorized: Valid token or session required.' }); - })(req, res, next); - } else { - // Default to session auth - if (req.isAuthenticated && req.isAuthenticated()) return next(); - return res.status(401).json({ message: 'Unauthorized: No active session.' }); - } - }; -} - -module.exports = dynamicAuthHandler; diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js new file mode 100644 index 000000000..a112d8cb9 --- /dev/null +++ b/src/service/passport/jwtAuthHandler.js @@ -0,0 +1,106 @@ +const axios = require("axios"); +const jwt = require("jsonwebtoken"); +const jwkToPem = require("jwk-to-pem"); + +/** + * Obtain the JSON Web Key Set (JWKS) from the OIDC authority. + * @param {string} authorityUrl the OIDC authority URL. e.g. https://login.microsoftonline.com/{tenantId} + * @returns {Promise} the JWKS keys + */ +async function getJwks(authorityUrl) { + try { + const { data } = await axios.get(`${authorityUrl}/.well-known/openid-configuration`); + const jwksUri = data.jwks_uri; + + const { data: jwks } = await axios.get(jwksUri); + return jwks.keys; + } catch (error) { + console.error("Error fetching JWKS:", error); + throw new Error("Failed to fetch JWKS"); + } +} + +/** + * Validate a JWT token using the OIDC configuration. + * @param {*} token the JWT token + * @param {*} authorityUrl the OIDC authority URL + * @param {*} clientID the OIDC client ID + * @param {*} expectedAudience the expected audience for the token + * @returns {Promise} the verified payload or an error + */ +async function validateJwt(token, authorityUrl, clientID, expectedAudience) { + try { + const jwks = await getJwks(authorityUrl); + + const decodedHeader = await jwt.decode(token, { complete: true }); + if (!decodedHeader || !decodedHeader.header || !decodedHeader.header.kid) { + throw new Error("Invalid JWT: Missing key ID (kid)"); + } + + const { kid } = decodedHeader.header; + const jwk = jwks.find((key) => key.kid === kid); + if (!jwk) { + throw new Error("No matching key found in JWKS"); + } + + const pubKey = jwkToPem(jwk); + + const verifiedPayload = jwt.verify(token, pubKey, { + algorithms: ["RS256"], + issuer: authorityUrl, + audience: expectedAudience, + }); + + if (verifiedPayload.azp !== clientID) { + throw new Error("JWT client ID does not match"); + } + + return { verifiedPayload }; + } catch (error) { + const errorMessage = `JWT validation failed: ${error.message}`; + console.error(errorMessage); + return { error: errorMessage }; + } +} + +const jwtAuthHandler = () => { + return async (req, res, next) => { + if (process.env.OIDC_AUTH_ENABLED !== "true") { + return next(); + } + + if (req.isAuthenticated()) { + console.log('request is already authenticated'); + return next(); + } + + const token = req.header("Authorization"); + if (!token) { + return res.status(401).send("No token provided"); + } + + const clientID = process.env.OIDC_CLIENT_ID; + const authorityUrl = process.env.OIDC_AUTHORITY; + const expectedAudience = process.env.OIDC_AUDIENCE || clientID; + + if (!authorityUrl) { + return res.status(500).send("OIDC authority URL is not configured"); + } + + if (!clientID) { + return res.status(500).send("OIDC client ID is not configured"); + } + + const tokenParts = token.split(" "); + const { verifiedPayload, error } = await validateJwt(tokenParts[1], authorityUrl, expectedAudience, clientID); + if (error) { + return res.status(401).send(error); + } + + req.user = verifiedPayload; + console.log('request authenticated through JWT') + return next(); + } +} + +module.exports = jwtAuthHandler; From 5202f7eecc86c441a8ca81952a2b672c022be88c Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 13:42:49 +0900 Subject: [PATCH 21/93] feat(auth): add jwk-to-pem library for jwt validation --- package-lock.json | 60 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 61 insertions(+) diff --git a/package-lock.json b/package-lock.json index 74cc6994e..005f0d811 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "history": "5.3.0", "isomorphic-git": "^1.27.1", "jsonschema": "^1.4.1", + "jwk-to-pem": "^2.0.7", "load-plugin": "^6.0.0", "lodash": "^4.17.21", "lusca": "^1.7.0", @@ -4315,6 +4316,12 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -5682,6 +5689,21 @@ "dev": true, "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -7439,6 +7461,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -7511,6 +7543,17 @@ "@babel/runtime": "^7.7.6" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hogan.js": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", @@ -8965,6 +9008,17 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwk-to-pem": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz", + "integrity": "sha512-cSVphrmWr6reVchuKQZdfSs4U9c5Y4hwZggPoz6cbVnTpAVgGRpEuQng86IyqLeGZlhTh+c4MAreB6KbdQDKHQ==", + "license": "Apache-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "elliptic": "^6.6.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -9561,6 +9615,12 @@ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index 40956086b..a6b25aa16 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "history": "5.3.0", "isomorphic-git": "^1.27.1", "jsonschema": "^1.4.1", + "jwk-to-pem": "^2.0.7", "load-plugin": "^6.0.0", "lodash": "^4.17.21", "lusca": "^1.7.0", From e8d08780c3366a46e46f2eb21b73ecfec3261579 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 13:57:18 +0900 Subject: [PATCH 22/93] feat(auth): convert envs to proxy.config.json entries (jwt) --- src/service/passport/jwtAuthHandler.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index a112d8cb9..8208cc464 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -65,7 +65,11 @@ async function validateJwt(token, authorityUrl, clientID, expectedAudience) { const jwtAuthHandler = () => { return async (req, res, next) => { - if (process.env.OIDC_AUTH_ENABLED !== "true") { + const authMethods = require('../../config').getAuthMethods(); + const jwtAuthMethod = authMethods.find((method) => method.type.toLowerCase() === "jwt"); + const { clientID, authorityURL, expectedAudience } = jwtAuthMethod?.jwtConfig; + + if (jwtAuthMethod.enabled) { return next(); } @@ -79,11 +83,9 @@ const jwtAuthHandler = () => { return res.status(401).send("No token provided"); } - const clientID = process.env.OIDC_CLIENT_ID; - const authorityUrl = process.env.OIDC_AUTHORITY; - const expectedAudience = process.env.OIDC_AUDIENCE || clientID; + let audience = expectedAudience || clientID; - if (!authorityUrl) { + if (!authorityURL) { return res.status(500).send("OIDC authority URL is not configured"); } @@ -92,13 +94,12 @@ const jwtAuthHandler = () => { } const tokenParts = token.split(" "); - const { verifiedPayload, error } = await validateJwt(tokenParts[1], authorityUrl, expectedAudience, clientID); + const { verifiedPayload, error } = await validateJwt(tokenParts[1], authorityURL, audience, clientID); if (error) { return res.status(401).send(error); } req.user = verifiedPayload; - console.log('request authenticated through JWT') return next(); } } From c5b45b2b2dfbfb631cd1eb8cffb44a9ca3128f64 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 14:07:19 +0900 Subject: [PATCH 23/93] feat(auth): add missing proxy.config.json auth methods --- proxy.config.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/proxy.config.json b/proxy.config.json index 14d016e4d..4e6863450 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -49,6 +49,29 @@ "baseDN": "", "searchBase": "" } + }, + { + "type": "", + "enabled": false, + "oidcConfig": { + "issuer": "", + "clientID": "", + "clientSecret": "", + "authorizationURL": "", + "tokenURL": "", + "userInfoURL": "", + "callbackURL": "", + "scope": "" + } + }, + { + "type": "jwt", + "enabled": false, + "jwtConfig": { + "clientID": "", + "authorityURL": "", + "expecetedAudience": "" + } } ], "api": { From b0c500c00673d02a0ef9921621489d34e674f364 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 14:36:10 +0900 Subject: [PATCH 24/93] fix(auth): fix undefined errors --- proxy.config.json | 2 +- src/service/passport/jwtAuthHandler.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/proxy.config.json b/proxy.config.json index 4e6863450..dadcf7346 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -51,7 +51,7 @@ } }, { - "type": "", + "type": "oidc", "enabled": false, "oidcConfig": { "issuer": "", diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index 8208cc464..af97e8640 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -67,9 +67,7 @@ const jwtAuthHandler = () => { return async (req, res, next) => { const authMethods = require('../../config').getAuthMethods(); const jwtAuthMethod = authMethods.find((method) => method.type.toLowerCase() === "jwt"); - const { clientID, authorityURL, expectedAudience } = jwtAuthMethod?.jwtConfig; - - if (jwtAuthMethod.enabled) { + if (!jwtAuthMethod) { return next(); } @@ -83,6 +81,7 @@ const jwtAuthHandler = () => { return res.status(401).send("No token provided"); } + const { clientID, authorityURL, expectedAudience } = jwtAuthMethod?.jwtConfig; let audience = expectedAudience || clientID; if (!authorityURL) { From 1446fcddbc979f0815580cee1db4fa694aceef52 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 14:37:42 +0900 Subject: [PATCH 25/93] fix(auth): fix mislabeled auth method --- proxy.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy.config.json b/proxy.config.json index dadcf7346..07d3b1af6 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -51,7 +51,7 @@ } }, { - "type": "oidc", + "type": "openidconnect", "enabled": false, "oidcConfig": { "issuer": "", From 85b3fede87bade91a7184406b5f4c39c24ddc821 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 14:46:52 +0900 Subject: [PATCH 26/93] feat(auth): set jwtAuthHandler middleware for /push, /repo and /user routes --- src/service/routes/index.js | 7 ++++--- src/service/routes/repo.js | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/service/routes/index.js b/src/service/routes/index.js index a7529a4ac..45b276c17 100644 --- a/src/service/routes/index.js +++ b/src/service/routes/index.js @@ -6,14 +6,15 @@ const repo = require('./repo'); const users = require('./users'); const healthcheck = require('./healthcheck'); const config = require('./config'); +const jwtAuthHandler = require('../passport/jwtAuthHandler'); const router = new express.Router(); router.use('/api', home); router.use('/api/auth', auth); router.use('/api/v1/healthcheck', healthcheck); -router.use('/api/v1/push', push); -router.use('/api/v1/repo', repo); -router.use('/api/v1/user', users); +router.use('/api/v1/push', jwtAuthHandler(), push); +router.use('/api/v1/repo', jwtAuthHandler(), repo); +router.use('/api/v1/user', jwtAuthHandler(), users); router.use('/api/v1/config', config); module.exports = router; diff --git a/src/service/routes/repo.js b/src/service/routes/repo.js index 36a7b88f2..1f6698e3b 100644 --- a/src/service/routes/repo.js +++ b/src/service/routes/repo.js @@ -2,9 +2,8 @@ const express = require('express'); const router = new express.Router(); const db = require('../../db'); const { getProxyURL } = require('../urls'); -const dynamicAuthHandler = require('../passport/dynamicAuthHandler'); -router.get('/', dynamicAuthHandler(), async (req, res) => { +router.get('/', async (req, res) => { const proxyURL = getProxyURL(req); const query = { type: 'push', From 3410c2940f1681cf0b7de2ac1b229b0a41a2482b Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 15:12:10 +0900 Subject: [PATCH 27/93] chore(auth): fix linter errors --- src/config/index.js | 2 +- src/service/passport/jwtAuthHandler.js | 4 ++-- src/service/passport/oidc.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index ddc4545f5..f9754859a 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -72,7 +72,7 @@ const getDatabase = () => { /** * Get the list of enabled authentication methods - * @returns {Array} List of enabled authentication methods + * @return {Array} List of enabled authentication methods */ const getAuthMethods = () => { if (_userSettings !== null && _userSettings.authentication) { diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index af97e8640..336c16e90 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -5,7 +5,7 @@ const jwkToPem = require("jwk-to-pem"); /** * Obtain the JSON Web Key Set (JWKS) from the OIDC authority. * @param {string} authorityUrl the OIDC authority URL. e.g. https://login.microsoftonline.com/{tenantId} - * @returns {Promise} the JWKS keys + * @return {Promise} the JWKS keys */ async function getJwks(authorityUrl) { try { @@ -26,7 +26,7 @@ async function getJwks(authorityUrl) { * @param {*} authorityUrl the OIDC authority URL * @param {*} clientID the OIDC client ID * @param {*} expectedAudience the expected audience for the token - * @returns {Promise} the verified payload or an error + * @return {Promise} the verified payload or an error */ async function validateJwt(token, authorityUrl, clientID, expectedAudience) { try { diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index 6681a6a8b..1adbfb215 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -63,7 +63,7 @@ const configure = async (passport) => { * Handles user authentication with OIDC. * @param userInfo the OIDC user info object * @param done the callback function - * @returns a promise with the authenticated user or an error + * @return a promise with the authenticated user or an error */ const handleUserAuthentication = async (userInfo, done) => { try { From 2d680adcee6e4754692f17e4397f20d9e6053ca1 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Sat, 8 Mar 2025 15:28:25 +0900 Subject: [PATCH 28/93] chore(auth): fix linting errors --- package-lock.json | 134 ------------------------- package.json | 1 - src/service/passport/jwtAuthHandler.js | 4 +- src/service/passport/oidc.js | 10 +- src/ui/views/Login/Login.jsx | 3 +- 5 files changed, 8 insertions(+), 144 deletions(-) diff --git a/package-lock.json b/package-lock.json index 005f0d811..a5563293b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,6 @@ "moment": "^2.29.4", "mongodb": "^5.0.0", "nodemailer": "^6.6.1", - "open": "^10.1.0", "openid-client": "^6.3.1", "parse-diff": "^0.11.1", "passport": "^0.7.0", @@ -4408,21 +4407,6 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5388,34 +5372,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -5456,18 +5412,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -7994,21 +7938,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8073,24 +8002,6 @@ "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -8372,21 +8283,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -10394,24 +10290,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/openid-client": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.3.1.tgz", @@ -11454,18 +11332,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index a6b25aa16..939a67b9f 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "moment": "^2.29.4", "mongodb": "^5.0.0", "nodemailer": "^6.6.1", - "open": "^10.1.0", "openid-client": "^6.3.1", "parse-diff": "^0.11.1", "passport": "^0.7.0", diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index 336c16e90..58db67964 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -81,8 +81,8 @@ const jwtAuthHandler = () => { return res.status(401).send("No token provided"); } - const { clientID, authorityURL, expectedAudience } = jwtAuthMethod?.jwtConfig; - let audience = expectedAudience || clientID; + const { clientID, authorityURL, expectedAudience } = jwtAuthMethod.jwtConfig; + const audience = expectedAudience || clientID; if (!authorityURL) { return res.status(500).send("OIDC authority URL is not configured"); diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index 1adbfb215..057f352eb 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -27,7 +27,7 @@ const configure = async (passport) => { }); // currentUrl must be overridden to match the callback URL - strategy.currentUrl = (request) => { + strategy.currentUrl = function(request) { const callbackUrl = new URL(callbackURL); const currentUrl = Strategy.prototype.currentUrl.call(this, request); currentUrl.host = callbackUrl.host; @@ -61,13 +61,13 @@ const configure = async (passport) => { /** * Handles user authentication with OIDC. - * @param userInfo the OIDC user info object - * @param done the callback function - * @return a promise with the authenticated user or an error + * @param {*} userInfo the OIDC user info object + * @param {*} done the callback function + * @return {*} a promise with the authenticated user or an error */ const handleUserAuthentication = async (userInfo, done) => { try { - let user = await db.findUserByOIDC(userInfo.sub); + const user = await db.findUserByOIDC(userInfo.sub); if (!user) { const email = safelyExtractEmail(userInfo); diff --git a/src/ui/views/Login/Login.jsx b/src/ui/views/Login/Login.jsx index 0be3781a6..ec8b3debd 100644 --- a/src/ui/views/Login/Login.jsx +++ b/src/ui/views/Login/Login.jsx @@ -24,8 +24,7 @@ export default function UserProfile() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [message, setMessage] = useState(''); - const [success, setSuccess] = useState(false); - const [gitAccountError, setGitAccountError] = useState(false); + const [, setGitAccountError] = useState(false); const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); From 050ba176ab7117fdfa9577327fef1dd4ffb7c606 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Mar 2025 13:35:58 +0900 Subject: [PATCH 29/93] chore(auth): fix linter issues --- src/ui/views/Login/Login.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/views/Login/Login.jsx b/src/ui/views/Login/Login.jsx index 0be3781a6..ec8b3debd 100644 --- a/src/ui/views/Login/Login.jsx +++ b/src/ui/views/Login/Login.jsx @@ -24,8 +24,7 @@ export default function UserProfile() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [message, setMessage] = useState(''); - const [success, setSuccess] = useState(false); - const [gitAccountError, setGitAccountError] = useState(false); + const [, setGitAccountError] = useState(false); const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); From b58d3a8373a8ea9b62aa5bfec0f644ca33ad5b74 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Mar 2025 15:03:01 +0900 Subject: [PATCH 30/93] fix(auth): fix OIDC login e2e test --- cypress/e2e/login.cy.js | 4 +++- src/service/passport/index.js | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js index 16da32f83..25d80e438 100644 --- a/cypress/e2e/login.cy.js +++ b/cypress/e2e/login.cy.js @@ -38,8 +38,10 @@ describe('Login page', () => { // Validates that OIDC is configured correctly it('should redirect to /oidc', () => { + // Set intercept first, since redirect on click can be quick + cy.intercept('GET', '/api/auth/oidc').as('oidcRedirect'); cy.get('[data-test="oidc-login"]').click(); - cy.url().should('include', '/oidc'); + cy.wait('@oidcRedirect'); }); }); }); diff --git a/src/service/passport/index.js b/src/service/passport/index.js index ce62719d2..72918282f 100644 --- a/src/service/passport/index.js +++ b/src/service/passport/index.js @@ -22,7 +22,6 @@ const configure = async () => { if (strategy && typeof strategy.configure === "function") { await strategy.configure(passport); } - console.log(`strategy type for ${auth.type}: ${strategy.type}`); } if (authMethods.some(auth => auth.type.toLowerCase() === "local")) { From 291c179a4689959fd2dff75ded1889ff13b9aade Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Mar 2025 15:14:00 +0900 Subject: [PATCH 31/93] fix(auth): fix linter issues --- src/config/index.js | 2 +- src/service/passport/oidc.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index ddc4545f5..f9754859a 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -72,7 +72,7 @@ const getDatabase = () => { /** * Get the list of enabled authentication methods - * @returns {Array} List of enabled authentication methods + * @return {Array} List of enabled authentication methods */ const getAuthMethods = () => { if (_userSettings !== null && _userSettings.authentication) { diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index baf65a5a7..ddbb7d39d 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -28,7 +28,7 @@ const configure = async () => { }); // currentUrl must be overridden to match the callback URL - strategy.currentUrl = (request) => { + strategy.currentUrl = function (request) { const callbackUrl = new URL(callbackURL); const currentUrl = Strategy.prototype.currentUrl.call(this, request); currentUrl.host = callbackUrl.host; @@ -62,13 +62,13 @@ const configure = async () => { /** * Handles user authentication with OIDC. - * @param userInfo the OIDC user info object - * @param done the callback function - * @returns a promise with the authenticated user or an error + * @param {*} userInfo the OIDC user info object + * @param {*} done the callback function + * @return {Promise} a promise with the authenticated user or an error */ const handleUserAuthentication = async (userInfo, done) => { try { - let user = await db.findUserByOIDC(userInfo.sub); + const user = await db.findUserByOIDC(userInfo.sub); if (!user) { const email = safelyExtractEmail(userInfo); From 2f19f8241d543ee4d4b9e29b0dbd6dc5410010f3 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Mon, 10 Mar 2025 15:45:32 +0900 Subject: [PATCH 32/93] fix(auth): try to fix ESM issue on openid-client import --- src/service/passport/oidc.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index ddbb7d39d..4954a4d7b 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -1,11 +1,12 @@ -const openIdClient = require('openid-client'); -const { Strategy } = require('openid-client/passport'); const passport = require('passport'); const db = require('../../db'); let type; const configure = async () => { + // Temp fix for ERR_REQUIRE_ESM, will be changed when we refactor to ESM + const { discovery, fetchUserInfo } = await import('openid-client'); + const { Strategy } = await import('openid-client/passport'); const authMethods = require('../../config').getAuthMethods(); const oidcConfig = authMethods.find((method) => method.type.toLowerCase() === "openidconnect")?.oidcConfig; const { issuer, clientID, clientSecret, callbackURL, scope } = oidcConfig; @@ -17,13 +18,13 @@ const configure = async () => { const server = new URL(issuer); try { - const config = await openIdClient.discovery(server, clientID, clientSecret); + const config = await discovery(server, clientID, clientSecret); const strategy = new Strategy({ callbackURL, config, scope }, async (tokenSet, done) => { // Validate token sub for added security const idTokenClaims = tokenSet.claims(); const expectedSub = idTokenClaims.sub; - const userInfo = await openIdClient.fetchUserInfo(config, tokenSet.access_token, expectedSub); + const userInfo = await fetchUserInfo(config, tokenSet.access_token, expectedSub); handleUserAuthentication(userInfo, done); }); From 4c4f513ed3d52f852a98a969efffa70de3fad09a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 2 Apr 2025 12:26:39 +0900 Subject: [PATCH 33/93] fix(auth): fix issue during merge --- src/ui/views/OpenPushRequests/components/PushesTable.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/views/OpenPushRequests/components/PushesTable.jsx b/src/ui/views/OpenPushRequests/components/PushesTable.jsx index 2b7213c60..2a3a7f33a 100644 --- a/src/ui/views/OpenPushRequests/components/PushesTable.jsx +++ b/src/ui/views/OpenPushRequests/components/PushesTable.jsx @@ -28,7 +28,7 @@ export default function PushesTable(props) { const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 5; const [searchTerm, setSearchTerm] = useState(''); - const openPush = (push) => navigate(`/admin/push/${push}`, { replace: true }); + const openPush = (push) => navigate(`/dashboard/push/${push}`, { replace: true }); useEffect(() => { const query = {}; From e259081507772e842c262989efcc3d0566c11056 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 2 Apr 2025 23:10:24 +0900 Subject: [PATCH 34/93] fix: add CI debug line and fix config typo --- proxy.config.json | 2 +- test/testLogin.test.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/proxy.config.json b/proxy.config.json index 07d3b1af6..7fa4ef059 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -70,7 +70,7 @@ "jwtConfig": { "clientID": "", "authorityURL": "", - "expecetedAudience": "" + "expectedAudience": "" } } ], diff --git a/test/testLogin.test.js b/test/testLogin.test.js index 833184e0b..79e4fb1a7 100644 --- a/test/testLogin.test.js +++ b/test/testLogin.test.js @@ -31,6 +31,13 @@ describe('auth', async () => { password: 'admin', }); + // Debug CI + console.log("DEBUG testLogin.test.js: "); + console.log(`res.body: ${JSON.stringify(res.body)}`); + console.log(`res.headers: ${JSON.stringify(res.headers)}`); + console.log(`res.status: ${res.status}`); + console.log(`res.text: ${res.text}`); + expect(res).to.have.cookie('connect.sid'); res.should.have.status(200); From 04c3878aba77120b60f3f7d6cf7c0f0916914b6d Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 4 Apr 2025 11:45:43 +0900 Subject: [PATCH 35/93] fix: fix createDefaultAdmin bug and add debug lines for CI --- src/service/passport/local.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/service/passport/local.js b/src/service/passport/local.js index 746434c6c..c292de57e 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.js @@ -13,6 +13,8 @@ const configure = async (passport) => { return done(null, false, { message: "Incorrect username." }); } + console.log(`Before bcrypt compare: user.password: ${user.password}`); + console.log(`Before bcrypt compare: password: ${password}`); const passwordCorrect = await bcrypt.compare(password, user.password); if (!passwordCorrect) { return done(null, false, { message: "Incorrect password." }); @@ -47,7 +49,8 @@ const configure = async (passport) => { const createDefaultAdmin = async () => { const admin = await db.findUser("admin"); if (!admin) { - await db.createUser("admin", "admin", "admin@place.com", "none", true, true, true, true); + console.log("No admin user found. Creating default admin user..."); + await db.createUser("admin", "admin", "admin@place.com", "none", true); } }; From 3605d73c89cb0ceedd36b4db8dcfc95819a6cd24 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 4 Apr 2025 11:58:39 +0900 Subject: [PATCH 36/93] fix: remove debug lines --- src/service/passport/jwtAuthHandler.js | 1 - src/service/passport/local.js | 3 --- src/service/passport/oidc.js | 2 +- src/ui/auth/AuthProvider.tsx | 3 --- src/ui/components/PrivateRoute/PrivateRoute.tsx | 6 +----- test/testLogin.test.js | 7 ------- 6 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index 58db67964..faa039f50 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -72,7 +72,6 @@ const jwtAuthHandler = () => { } if (req.isAuthenticated()) { - console.log('request is already authenticated'); return next(); } diff --git a/src/service/passport/local.js b/src/service/passport/local.js index c292de57e..579d47234 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.js @@ -13,8 +13,6 @@ const configure = async (passport) => { return done(null, false, { message: "Incorrect username." }); } - console.log(`Before bcrypt compare: user.password: ${user.password}`); - console.log(`Before bcrypt compare: password: ${password}`); const passwordCorrect = await bcrypt.compare(password, user.password); if (!passwordCorrect) { return done(null, false, { message: "Incorrect password." }); @@ -49,7 +47,6 @@ const configure = async (passport) => { const createDefaultAdmin = async () => { const admin = await db.findUser("admin"); if (!admin) { - console.log("No admin user found. Creating default admin user..."); await db.createUser("admin", "admin", "admin@place.com", "none", true); } }; diff --git a/src/service/passport/oidc.js b/src/service/passport/oidc.js index 284827e5a..6c9cd00a7 100644 --- a/src/service/passport/oidc.js +++ b/src/service/passport/oidc.js @@ -50,7 +50,7 @@ const configure = async (passport) => { done(err); } }) - console.log(`setting type to ${server.host}`) + type = server.host; return passport; diff --git a/src/ui/auth/AuthProvider.tsx b/src/ui/auth/AuthProvider.tsx index 1da89df51..f7ab4b2fc 100644 --- a/src/ui/auth/AuthProvider.tsx +++ b/src/ui/auth/AuthProvider.tsx @@ -16,13 +16,10 @@ export const AuthProvider = ({ children }) => { const [isLoading, setIsLoading] = useState(true); const refreshUser = async () => { - console.log('Refreshing user'); try { const data = await getUserInfo(); setUser(data); - console.log('User refreshed:', data); } catch (error) { - console.error('Error refreshing user:', error); setUser(null); } finally { setIsLoading(false); diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/PrivateRoute/PrivateRoute.tsx index 4e7a2f4bf..d010a7d86 100644 --- a/src/ui/components/PrivateRoute/PrivateRoute.tsx +++ b/src/ui/components/PrivateRoute/PrivateRoute.tsx @@ -4,20 +4,16 @@ import { useAuth } from '../../auth/AuthProvider'; const PrivateRoute = ({ component: Component, adminOnly = false }) => { const { user, isLoading } = useAuth(); - console.debug('PrivateRoute', { user, isLoading, adminOnly }); - + if (isLoading) { - console.debug('Auth is loading, waiting'); return
Loading...
; // TODO: Add loading spinner } if (!user) { - console.debug('User not logged in, redirecting to login page'); return ; } if (adminOnly && !user.admin) { - console.debug('User is not an admin, redirecting to not authorized page'); return ; } diff --git a/test/testLogin.test.js b/test/testLogin.test.js index 79e4fb1a7..833184e0b 100644 --- a/test/testLogin.test.js +++ b/test/testLogin.test.js @@ -31,13 +31,6 @@ describe('auth', async () => { password: 'admin', }); - // Debug CI - console.log("DEBUG testLogin.test.js: "); - console.log(`res.body: ${JSON.stringify(res.body)}`); - console.log(`res.headers: ${JSON.stringify(res.headers)}`); - console.log(`res.status: ${res.status}`); - console.log(`res.text: ${res.text}`); - expect(res).to.have.cookie('connect.sid'); res.should.have.status(200); From 1e5d3610ff29c9d71083328c17264c1fec9967ab Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 4 Apr 2025 12:10:23 +0900 Subject: [PATCH 37/93] chore: allow OFL-1.1 license Free license according to FSF (https://www.gnu.org/licenses/license-list.html#SILOFL) --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index fdbc7ecb1..dd4c21bc5 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -21,6 +21,6 @@ jobs: with: comment-summary-in-pr: always fail-on-severity: high - allow-licenses: MIT, MIT-0, Apache-2.0, BSD-3-Clause, BSD-3-Clause-Clear, ISC, BSD-2-Clause, Unlicense, CC0-1.0, 0BSD, X11, MPL-2.0, MPL-1.0, MPL-1.1, MPL-2.0, Zlib + allow-licenses: MIT, MIT-0, Apache-2.0, BSD-3-Clause, BSD-3-Clause-Clear, ISC, BSD-2-Clause, Unlicense, CC0-1.0, 0BSD, X11, MPL-2.0, MPL-1.0, MPL-1.1, MPL-2.0, OFL-1.1, Zlib fail-on-scopes: development, runtime allow-dependencies-licenses: 'pkg:npm/caniuse-lite' From 634640ff92c1dbea6763bfef491767a9cb3ca80a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Tue, 8 Apr 2025 00:18:54 +0900 Subject: [PATCH 38/93] fix: add line breaks to jwt auth error messages --- src/service/passport/jwtAuthHandler.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index faa039f50..2989cf65d 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -57,7 +57,7 @@ async function validateJwt(token, authorityUrl, clientID, expectedAudience) { return { verifiedPayload }; } catch (error) { - const errorMessage = `JWT validation failed: ${error.message}`; + const errorMessage = `JWT validation failed: ${error.message}\n`; console.error(errorMessage); return { error: errorMessage }; } @@ -77,18 +77,18 @@ const jwtAuthHandler = () => { const token = req.header("Authorization"); if (!token) { - return res.status(401).send("No token provided"); + return res.status(401).send("No token provided\n"); } const { clientID, authorityURL, expectedAudience } = jwtAuthMethod.jwtConfig; const audience = expectedAudience || clientID; if (!authorityURL) { - return res.status(500).send("OIDC authority URL is not configured"); + return res.status(500).send("OIDC authority URL is not configured\n"); } if (!clientID) { - return res.status(500).send("OIDC client ID is not configured"); + return res.status(500).send("OIDC client ID is not configured\n"); } const tokenParts = token.split(" "); From edf4f7daef2bf0236b019339eff72c9a5e5da8c2 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Thu, 10 Apr 2025 16:47:56 +0900 Subject: [PATCH 39/93] refactor(auth): convert jwt auth to API-only (remove jwt passport strategy) --- config.schema.json | 7 +++++ package-lock.json | 12 +------- package.json | 2 +- proxy.config.json | 41 +++++++++++++------------- src/config/index.js | 20 +++++++++++++ src/service/passport/index.js | 2 -- src/service/passport/jwt.js | 26 ---------------- src/service/passport/jwtAuthHandler.js | 4 +-- 8 files changed, 52 insertions(+), 62 deletions(-) delete mode 100644 src/service/passport/jwt.js diff --git a/config.schema.json b/config.schema.json index 771e83d0c..135018be9 100644 --- a/config.schema.json +++ b/config.schema.json @@ -78,6 +78,13 @@ "type": "object" } } + }, + "apiAuthentication": { + "description": "List of authentication sources for API endpoints. May be empty, in which case all endpoints are public.", + "type": "array", + "items": { + "$ref": "#/definitions/authentication" + } } }, "definitions": { diff --git a/package-lock.json b/package-lock.json index f580e9ff5..1a84fabc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "history": "5.3.0", "isomorphic-git": "^1.27.1", "jsonschema": "^1.4.1", + "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.7", "load-plugin": "^6.0.0", "lodash": "^4.17.21", @@ -43,7 +44,6 @@ "parse-diff": "^0.11.1", "passport": "^0.7.0", "passport-activedirectory": "^1.0.4", - "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "perfect-scrollbar": "^1.5.5", "prop-types": "15.8.1", @@ -10504,16 +10504,6 @@ "url": "https://github.com/sponsors/jaredhanson" } }, - "node_modules/passport-jwt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", - "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", - "license": "MIT", - "dependencies": { - "jsonwebtoken": "^9.0.0", - "passport-strategy": "^1.0.0" - } - }, "node_modules/passport-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", diff --git a/package.json b/package.json index b0896fcd4..d820d2c13 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "history": "5.3.0", "isomorphic-git": "^1.27.1", "jsonschema": "^1.4.1", + "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.7", "load-plugin": "^6.0.0", "lodash": "^4.17.21", @@ -64,7 +65,6 @@ "parse-diff": "^0.11.1", "passport": "^0.7.0", "passport-activedirectory": "^1.0.4", - "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "perfect-scrollbar": "^1.5.5", "prop-types": "15.8.1", diff --git a/proxy.config.json b/proxy.config.json index 7fa4ef059..d494898e8 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -49,28 +49,19 @@ "baseDN": "", "searchBase": "" } - }, + }, { "type": "openidconnect", - "enabled": false, + "enabled": true, "oidcConfig": { - "issuer": "", - "clientID": "", - "clientSecret": "", - "authorizationURL": "", - "tokenURL": "", - "userInfoURL": "", - "callbackURL": "", - "scope": "" - } - }, - { - "type": "jwt", - "enabled": false, - "jwtConfig": { - "clientID": "", - "authorityURL": "", - "expectedAudience": "" + "issuer": "https://accounts.google.com", + "clientID": "1009968223893-u92qq6itk7ej5008o4174gjubs5lhorg.apps.googleusercontent.com", + "clientSecret": "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", + "authorizationURL": "https://accounts.google.com/o/oauth2/auth", + "tokenURL": "https://oauth2.googleapis.com/token", + "userInfoURL": "https://openidconnect.googleapis.com/v1/userinfo", + "callbackURL": "http://localhost:8080/api/auth/oidc/callback", + "scope": "openid email profile" } } ], @@ -120,5 +111,15 @@ "urlShortener": "", "contactEmail": "", "csrfProtection": true, - "plugins": [] + "plugins": [], + "apiAuthentication": [ + { + "type": "jwt", + "enabled": true, + "jwtConfig": { + "clientID": "1009968223893-u92qq6itk7ej5008o4174gjubs5lhorg.apps.googleusercontent.com", + "authorityURL": "https://accounts.google.com" + } + } + ] } diff --git a/src/config/index.js b/src/config/index.js index f9754859a..c00ca1de0 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -10,6 +10,7 @@ if (fs.existsSync(userSettingsPath)) { let _authorisedList = defaultSettings.authorisedList; let _database = defaultSettings.sink; let _authentication = defaultSettings.authentication; +let _apiAuthentication = defaultSettings.apiAuthentication; let _tempPassword = defaultSettings.tempPassword; let _proxyUrl = defaultSettings.proxyUrl; let _api = defaultSettings.api; @@ -88,6 +89,24 @@ const getAuthMethods = () => { return enabledAuthMethods; }; +/** + * Get the list of enabled authentication methods for API endpoints + * @return {Array} List of enabled authentication methods + */ +const getAPIAuthMethods = () => { + if (_userSettings !== null && _userSettings.apiAuthentication) { + _apiAuthentication = _userSettings.apiAuthentication; + } + + const enabledAuthMethods = _apiAuthentication.filter(auth => auth.enabled); + + if (enabledAuthMethods.length === 0) { + throw new Error("No authentication method enabled."); + } + + return enabledAuthMethods; +}; + // Log configuration to console const logConfiguration = () => { console.log(`authorisedList = ${JSON.stringify(getAuthorisedList())}`); @@ -205,6 +224,7 @@ exports.getAuthorisedList = getAuthorisedList; exports.getDatabase = getDatabase; exports.logConfiguration = logConfiguration; exports.getAuthMethods = getAuthMethods; +exports.getAPIAuthMethods = getAPIAuthMethods; exports.getTempPasswordConfig = getTempPasswordConfig; exports.getCookieSecret = getCookieSecret; exports.getSessionMaxAgeHours = getSessionMaxAgeHours; diff --git a/src/service/passport/index.js b/src/service/passport/index.js index 7126ae10f..533577d13 100644 --- a/src/service/passport/index.js +++ b/src/service/passport/index.js @@ -2,7 +2,6 @@ const passport = require("passport"); const local = require('./local'); const activeDirectory = require('./activeDirectory'); const oidc = require('./oidc'); -const jwt = require('./jwt'); const config = require('../../config'); // Allows obtaining strategy config function and type @@ -11,7 +10,6 @@ const authStrategies = { local: local, activedirectory: activeDirectory, openidconnect: oidc, - jwt: jwt, }; const configure = async () => { diff --git a/src/service/passport/jwt.js b/src/service/passport/jwt.js deleted file mode 100644 index f2b2dadb1..000000000 --- a/src/service/passport/jwt.js +++ /dev/null @@ -1,26 +0,0 @@ -const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt'); - -const type = "jwt"; - -const configure = (passport) => { - const JWT_SECRET = process.env.JWT_SECRET; - - if (!JWT_SECRET) { - console.log('JWT secret not provided. Skipping JWT strategy registration.'); - return; // Skip JWT registration if not configured - } - - const opts = { - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: JWT_SECRET, - }; - - passport.use('jwt', new JwtStrategy(opts, (jwtPayload, done) => { - if (!jwtPayload) return done(null, false); - return done(null, jwtPayload); - })); - - console.log('JWT strategy registered successfully.'); -}; - -module.exports = { configure, type }; diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index 2989cf65d..7a4c62635 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -65,8 +65,8 @@ async function validateJwt(token, authorityUrl, clientID, expectedAudience) { const jwtAuthHandler = () => { return async (req, res, next) => { - const authMethods = require('../../config').getAuthMethods(); - const jwtAuthMethod = authMethods.find((method) => method.type.toLowerCase() === "jwt"); + const apiAuthMethods = require('../../config').getAPIAuthMethods(); + const jwtAuthMethod = apiAuthMethods.find((method) => method.type.toLowerCase() === "jwt"); if (!jwtAuthMethod) { return next(); } From 1dcc04b486443f13364f5be801dd44786591be8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 07:47:00 +0000 Subject: [PATCH 40/93] fix(deps): update npm - li-cli - experimental/li-cli/package.json --- experimental/li-cli/package-lock.json | 239 +++++++++++++++----------- experimental/li-cli/package.json | 14 +- 2 files changed, 147 insertions(+), 106 deletions(-) diff --git a/experimental/li-cli/package-lock.json b/experimental/li-cli/package-lock.json index 9b3276498..dee7d0811 100644 --- a/experimental/li-cli/package-lock.json +++ b/experimental/li-cli/package-lock.json @@ -9,22 +9,22 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "@inquirer/prompts": "^7.3.3", - "yaml": "^2.7.0", + "@inquirer/prompts": "^7.5.0", + "yaml": "^2.7.1", "yargs": "^17.7.2", - "zod": "^3.24.2" + "zod": "^3.24.3" }, "devDependencies": { "@jest/globals": "^29.7.0", - "@types/node": "^22.13.10", + "@types/node": "^22.15.3", "@types/yargs": "^17.0.33", "jest": "^29.7.0", "rimraf": "^6.0.1", - "ts-jest": "^29.2.6", + "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "tsc-alias": "^1.8.11", + "tsc-alias": "^1.8.15", "tslib": "^2.8.1", - "typescript": "^5.8.2" + "typescript": "^5.8.3" } }, "node_modules/@ampproject/remapping": { @@ -563,14 +563,14 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.3.tgz", - "integrity": "sha512-KU1MGwf24iABJjGESxhyj+/rlQYSRoCfcuHDEHXfZ1DENmbuSRfyrUb+LLjHoee5TNOFKwaFxDXc5/zRwJUPMQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz", + "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", + "@inquirer/core": "^10.1.10", "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -587,13 +587,13 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.7.tgz", - "integrity": "sha512-Xrfbrw9eSiHb+GsesO8TQIeHSMTP0xyvTCeeYevgZ4sKW+iz9w/47bgfG9b0niQm+xaLY2EWPBINUPldLwvYiw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", + "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5" + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" }, "engines": { "node": ">=18" @@ -608,13 +608,13 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.8.tgz", - "integrity": "sha512-HpAqR8y715zPpM9e/9Q+N88bnGwqqL8ePgZ0SMv/s3673JLMv3bIkoivGmjPqXlEgisUksSXibweQccUwEx4qQ==", + "version": "10.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", + "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", "license": "MIT", "dependencies": { "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -649,13 +649,13 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.8.tgz", - "integrity": "sha512-UkGKbMFlQw5k4ZLjDwEi5z8NIVlP/3DAlLHta0o0pSsdpPThNmPtUL8mvGCHUaQtR+QrxR9yRYNWgKMsHkfIUA==", + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz", + "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5", + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", "external-editor": "^3.1.0" }, "engines": { @@ -671,13 +671,13 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.10.tgz", - "integrity": "sha512-leyBouGJ77ggv51Jb/OJmLGGnU2HYc13MZ2iiPNLwe2VgFgZPVqsrRWSa1RAHKyazjOyvSNKLD1B2K7A/iWi1g==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz", + "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5", + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -702,13 +702,13 @@ } }, "node_modules/@inquirer/input": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.7.tgz", - "integrity": "sha512-rCQAipJNA14UTH84df/z4jDJ9LZ54H6zzuCAi7WZ0qVqx3CSqLjfXAMd5cpISIxbiHVJCPRB81gZksq6CZsqDg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz", + "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5" + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" }, "engines": { "node": ">=18" @@ -723,13 +723,13 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.10.tgz", - "integrity": "sha512-GLsdnxzNefjCJUmWyjaAuNklHgDpCTL4RMllAVhVvAzBwRW9g38eZ5tWgzo1lirtSDTpsh593hqXVhxvdrjfwA==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz", + "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5" + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" }, "engines": { "node": ">=18" @@ -744,13 +744,13 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.10.tgz", - "integrity": "sha512-JC538ujqeYKkFqLoWZ0ILBteIUO2yajBMVEUZSxjl9x6fiEQtM+I5Rca7M2D8edMDbyHLnXifGH1hJZdh8V5rA==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz", + "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5", + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2" }, "engines": { @@ -766,21 +766,21 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.3.tgz", - "integrity": "sha512-QS1AQgJ113iE/nmym03yKZKHvGjVWwkGZT3B1yKrrMG0bJKQg1jUkntFP8aPd2FUQzu/nga7QU2eDpzIP5it0Q==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.0.tgz", + "integrity": "sha512-tk8Bx7l5AX/CR0sVfGj3Xg6v7cYlFBkEahH+EgBB+cZib6Fc83dwerTbzj7f2+qKckjIUGsviWRI1d7lx6nqQA==", "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.1.3", - "@inquirer/confirm": "^5.1.7", - "@inquirer/editor": "^4.2.8", - "@inquirer/expand": "^4.0.10", - "@inquirer/input": "^4.1.7", - "@inquirer/number": "^3.0.10", - "@inquirer/password": "^4.0.10", - "@inquirer/rawlist": "^4.0.10", - "@inquirer/search": "^3.0.10", - "@inquirer/select": "^4.0.10" + "@inquirer/checkbox": "^4.1.5", + "@inquirer/confirm": "^5.1.9", + "@inquirer/editor": "^4.2.10", + "@inquirer/expand": "^4.0.12", + "@inquirer/input": "^4.1.9", + "@inquirer/number": "^3.0.12", + "@inquirer/password": "^4.0.12", + "@inquirer/rawlist": "^4.1.0", + "@inquirer/search": "^3.0.12", + "@inquirer/select": "^4.2.0" }, "engines": { "node": ">=18" @@ -795,13 +795,13 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.10.tgz", - "integrity": "sha512-vOQbQkmhaCsF2bUmjoyRSZJBz77UnIF/F3ZS2LMgwbgyaG2WgwKHh0WKNj0APDB72WDbZijhW5nObQbk+TnbcA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.0.tgz", + "integrity": "sha512-6ob45Oh9pXmfprKqUiEeMz/tjtVTFQTgDDz1xAMKMrIvyrYjAmRbQZjMJfsictlL4phgjLhdLu27IkHNnNjB7g==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5", + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -817,14 +817,14 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.10.tgz", - "integrity": "sha512-EAVKAz6P1LajZOdoL+R+XC3HJYSU261fbJzO4fCkJJ7UPFcm+nP+gzC+DDZWsb2WK9PQvKsnaKiNKsY8B6dBWQ==", + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz", + "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", + "@inquirer/core": "^10.1.10", "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/type": "^3.0.6", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -840,14 +840,14 @@ } }, "node_modules/@inquirer/select": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.10.tgz", - "integrity": "sha512-Tg8S9nESnCfISu5tCZSuXpXq0wHuDVimj7xyHstABgR34zcJnLdq/VbjB2mdZvNAMAehYBnNzSjxB06UE8LLAA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.0.tgz", + "integrity": "sha512-KkXQ4aSySWimpV4V/TUJWdB3tdfENZUU765GjOIZ0uPwdbGIG6jrxD4dDf1w68uP+DVtfNhr1A92B+0mbTZ8FA==", "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.8", + "@inquirer/core": "^10.1.10", "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -864,9 +864,9 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", - "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", "license": "MIT", "engines": { "node": ">=18" @@ -1575,13 +1575,13 @@ } }, "node_modules/@types/node": { - "version": "22.13.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", - "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/stack-utils": { @@ -2637,6 +2637,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4348,6 +4361,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -4749,9 +4772,9 @@ } }, "node_modules/ts-jest": { - "version": "29.2.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", - "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "version": "29.3.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.2.tgz", + "integrity": "sha512-bJJkrWc6PjFVz5g2DGCNUo8z7oFEYaz1xP1NpeDU7KNLMWPpEyV8Chbpkn8xjzgRDpQhnGMyvyldoL7h8JXyug==", "dev": true, "license": "MIT", "dependencies": { @@ -4763,6 +4786,7 @@ "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", "semver": "^7.7.1", + "type-fest": "^4.39.1", "yargs-parser": "^21.1.1" }, "bin": { @@ -4810,6 +4834,19 @@ "node": ">=10" } }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.40.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.1.tgz", + "integrity": "sha512-9YvLNnORDpI+vghLU/Nf+zSv0kL47KbVJ1o3sKgoTefl6i+zebxbiDQWoe/oWWqPhIgQdRZRT1KA9sCPL810SA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -4855,14 +4892,15 @@ } }, "node_modules/tsc-alias": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.11.tgz", - "integrity": "sha512-2DuEQ58A9Rj2NE2c1+/qaGKlshni9MCK95MJzRGhQG0CYLw0bE/ACgbhhTSf/p1svLelwqafOd8stQate2bYbg==", + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.15.tgz", + "integrity": "sha512-yKLVx8ddUurRwhVcS6JFF2ZjksOX2ZWDRIdgt+PQhJBDegIdAdilptiHsuAbx9UFxa16GFrxeKQ2kTcGvR6fkQ==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", "globby": "^11.0.4", "mylas": "^2.1.9", "normalize-path": "^3.0.0", @@ -4870,6 +4908,9 @@ }, "bin": { "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" } }, "node_modules/tslib": { @@ -4902,9 +4943,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4916,9 +4957,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "devOptional": true, "license": "MIT" }, @@ -5093,9 +5134,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -5167,9 +5208,9 @@ } }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/experimental/li-cli/package.json b/experimental/li-cli/package.json index 6bb723fb0..4b24950b4 100644 --- a/experimental/li-cli/package.json +++ b/experimental/li-cli/package.json @@ -13,21 +13,21 @@ "test": "jest --forceExit --detectOpenHandles" }, "dependencies": { - "@inquirer/prompts": "^7.3.3", - "yaml": "^2.7.0", + "@inquirer/prompts": "^7.5.0", + "yaml": "^2.7.1", "yargs": "^17.7.2", - "zod": "^3.24.2" + "zod": "^3.24.3" }, "devDependencies": { "@jest/globals": "^29.7.0", - "@types/node": "^22.13.10", + "@types/node": "^22.15.3", "@types/yargs": "^17.0.33", "jest": "^29.7.0", "rimraf": "^6.0.1", - "ts-jest": "^29.2.6", + "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "tsc-alias": "^1.8.11", + "tsc-alias": "^1.8.15", "tslib": "^2.8.1", - "typescript": "^5.8.2" + "typescript": "^5.8.3" } } From ee149facca4c2227007a201df8632255180affe9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:55:42 +0000 Subject: [PATCH 41/93] chore(deps): update github-actions - workflows - .github/workflows/dependency-review.yml --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/codeql.yml | 8 ++++---- .github/workflows/dependency-review.yml | 4 ++-- .github/workflows/experimental-inventory-ci.yml | 4 ++-- .github/workflows/experimental-inventory-cli-publish.yml | 4 ++-- .github/workflows/experimental-inventory-publish.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/npm.yml | 4 ++-- .github/workflows/pr-lint.yml | 2 +- .github/workflows/sample-publish.yml | 4 ++-- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/unused-dependencies.yml | 4 ++-- 12 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5a9ccadd..794c7556e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ matrix.node-version }} @@ -52,7 +52,7 @@ jobs: npm run test-coverage-ci --workspaces --if-present - name: Upload test coverage report - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: files: ./coverage/lcov.info token: ${{ secrets.CODECOV_TOKEN }} @@ -71,7 +71,7 @@ jobs: path: build - name: Download the build folders - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: build path: build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8f6cd1eb7..ec9d82b44 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -51,7 +51,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2 with: egress-policy: audit @@ -60,7 +60,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,7 +74,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -87,6 +87,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index fdbc7ecb1..effb06f1f 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -10,14 +10,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Dependency Review - uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4 + uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4 with: comment-summary-in-pr: always fail-on-severity: high diff --git a/.github/workflows/experimental-inventory-ci.yml b/.github/workflows/experimental-inventory-ci.yml index bca99dc2f..f8c1b728c 100644 --- a/.github/workflows/experimental-inventory-ci.yml +++ b/.github/workflows/experimental-inventory-ci.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit @@ -33,7 +33,7 @@ jobs: fetch-depth: 0 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/experimental-inventory-cli-publish.yml b/.github/workflows/experimental-inventory-cli-publish.yml index 3387e285c..8fbf7c3e2 100644 --- a/.github/workflows/experimental-inventory-cli-publish.yml +++ b/.github/workflows/experimental-inventory-cli-publish.yml @@ -14,14 +14,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/experimental-inventory-publish.yml b/.github/workflows/experimental-inventory-publish.yml index 7f16f9bca..fdeac8cf3 100644 --- a/.github/workflows/experimental-inventory-publish.yml +++ b/.github/workflows/experimental-inventory-publish.yml @@ -14,14 +14,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '22.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8739eff7c..0da265d7e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-latest steps: # list of steps - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2 with: egress-policy: audit - name: Install NodeJS - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: ${{ env.NODE_VERSION }} diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 2815640c2..8445a2123 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '18.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index c67da0be0..6d18d2d98 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit diff --git a/.github/workflows/sample-publish.yml b/.github/workflows/sample-publish.yml index 005af5ce1..c61370d82 100644 --- a/.github/workflows/sample-publish.yml +++ b/.github/workflows/sample-publish.yml @@ -13,13 +13,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '18.x' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ecfc13e42..7f5579f94 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 with: egress-policy: audit @@ -72,6 +72,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3.28.13 + uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 with: sarif_file: results.sarif diff --git a/.github/workflows/unused-dependencies.yml b/.github/workflows/unused-dependencies.yml index 39071e270..61f4222c0 100644 --- a/.github/workflows/unused-dependencies.yml +++ b/.github/workflows/unused-dependencies.yml @@ -9,14 +9,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2 + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2 with: egress-policy: audit - name: 'Checkout Repository' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: 'Setup Node.js' - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '18.x' - name: 'Run depcheck' From bdf0fc74ce0ca5632e6f44b9f3038b0b47f3b3a3 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Tue, 29 Apr 2025 10:01:58 +0100 Subject: [PATCH 42/93] chore: run npm audit fix --- package-lock.json | 51 ++++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3052eaddc..3b461ffd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,7 +92,7 @@ "ts-node": "^10.9.2", "tsx": "^4.19.3", "typescript": "^5.7.3", - "vite": "4.5.5", + "vite": "^4.5.13", "vite-tsconfig-paths": "^5.1.4" }, "optionalDependencies": { @@ -1034,27 +1034,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1202,9 +1202,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", - "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1213,15 +1214,15 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -1247,9 +1248,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "license": "MIT", "dependencies": { @@ -13800,9 +13801,9 @@ } }, "node_modules/vite": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", - "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "version": "4.5.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.13.tgz", + "integrity": "sha512-Hgp8IF/yZDzKsN1hQWOuQZbrKiaFsbQud+07jJ8h9m9PaHWkpvZ5u55Xw5yYjWRXwRQ4jwFlJvY7T7FUJG9MCA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 757dfbd92..741a54ec1 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "ts-node": "^10.9.2", "tsx": "^4.19.3", "typescript": "^5.7.3", - "vite": "4.5.5", + "vite": "^4.5.13", "vite-tsconfig-paths": "^5.1.4" }, "optionalDependencies": { From 734a908f95b9bf146d62610534cbf4dfb24a5494 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Tue, 29 Apr 2025 10:24:09 +0100 Subject: [PATCH 43/93] chore: bump by minor to v1.11.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b461ffd7..fdf67700c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/git-proxy", - "version": "1.10.0", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/git-proxy", - "version": "1.10.0", + "version": "1.11.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" diff --git a/package.json b/package.json index 741a54ec1..2db0042be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@finos/git-proxy", - "version": "1.10.0", + "version": "1.11.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { "cli": "node ./packages/git-proxy-cli/index.js", From 1e7b1813dc20f95f03becdefabbd00039bf5e774 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Tue, 29 Apr 2025 11:17:28 +0100 Subject: [PATCH 44/93] chore: switch @typescript-eslint/no-explicit-any to off instead of warn --- .eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.json b/.eslintrc.json index 09ee628c2..fb129879f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -42,7 +42,7 @@ "react/prop-types": "off", "require-jsdoc": "off", "no-async-promise-executor": "off", - "@typescript-eslint/no-explicit-any": "warn", // temporary until TS refactor is complete + "@typescript-eslint/no-explicit-any": "off", // temporary until TS refactor is complete "@typescript-eslint/no-unused-vars": "off", // temporary until TS refactor is complete "@typescript-eslint/no-require-imports": "off", // prevents error on old "require" imports "@typescript-eslint/no-unused-expressions": "off" // prevents error on test "expect" expressions From 044ae8da041e4c569259145e85f0554fc6566f90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:24:43 +0000 Subject: [PATCH 45/93] chore(deps): update dependency @finos/git-proxy to ^1.11.0 - git-proxy-plugin-samples - plugins/git-proxy-plugin-samples/package.json --- plugins/git-proxy-plugin-samples/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/git-proxy-plugin-samples/package.json b/plugins/git-proxy-plugin-samples/package.json index 622b3c3e9..2a9455fcd 100644 --- a/plugins/git-proxy-plugin-samples/package.json +++ b/plugins/git-proxy-plugin-samples/package.json @@ -16,6 +16,6 @@ "express": "^4.21.2" }, "peerDependencies": { - "@finos/git-proxy": "^1.9.3" + "@finos/git-proxy": "^1.11.0" } } From 909d3835bb6f22f73d8c1495a9345d25f62ec0ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 10:32:03 +0000 Subject: [PATCH 46/93] fix(deps): update dependency axios to ^1.9.0 - git-proxy-cli - packages/git-proxy-cli/package.json --- package-lock.json | 8 ++++---- packages/git-proxy-cli/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fdf67700c..d2accb66c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4650,9 +4650,9 @@ "dev": true }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -14310,7 +14310,7 @@ "license": "Apache-2.0", "dependencies": { "@finos/git-proxy": "file:../..", - "axios": "^1.8.4", + "axios": "^1.9.0", "yargs": "^17.7.2" }, "bin": { diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json index baade725c..d8babc8d6 100644 --- a/packages/git-proxy-cli/package.json +++ b/packages/git-proxy-cli/package.json @@ -4,7 +4,7 @@ "description": "Command line interface tool for FINOS GitProxy.", "bin": "./index.js", "dependencies": { - "axios": "^1.8.4", + "axios": "^1.9.0", "yargs": "^17.7.2", "@finos/git-proxy": "file:../.." }, From ced6237c5a95727cdd02f5fc4292b0a6d8d98924 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 15:56:05 +0000 Subject: [PATCH 47/93] fix(deps): update npm - website - website/package.json --- website/package.json | 10 +-- website/yarn.lock | 151 ++++++++++++++++++++++++++----------------- 2 files changed, 97 insertions(+), 64 deletions(-) diff --git a/website/package.json b/website/package.json index 446392c87..6be24ea2b 100644 --- a/website/package.json +++ b/website/package.json @@ -13,13 +13,13 @@ "@docusaurus/core": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0", "@docusaurus/plugin-google-gtag": "^3.7.0", - "axios": "^1.8.4", + "axios": "^1.9.0", "classnames": "^2.5.1", "clsx": "^2.1.1", - "eslint": "^9.23.0", - "eslint-plugin-react": "^7.37.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "eslint": "^9.25.1", + "eslint-plugin-react": "^7.37.5", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-player": "^2.16.0", "react-slick": "^0.30.3", "react-social-media-embed": "^2.5.18", diff --git a/website/yarn.lock b/website/yarn.lock index 7cae312cd..9cdc65fbc 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2832,24 +2832,24 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== -"@eslint/config-array@^0.19.2": - version "0.19.2" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.19.2.tgz#3060b809e111abfc97adb0bb1172778b90cb46aa" - integrity sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w== +"@eslint/config-array@^0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.20.0.tgz#7a1232e82376712d3340012a2f561a2764d1988f" + integrity sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ== dependencies: "@eslint/object-schema" "^2.1.6" debug "^4.3.1" minimatch "^3.1.2" -"@eslint/config-helpers@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.0.tgz#12dc8d65c31c4b6c3ebf0758db6601eb7692ce59" - integrity sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ== +"@eslint/config-helpers@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz#26042c028d1beee5ce2235a7929b91c52651646d" + integrity sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw== -"@eslint/core@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.12.0.tgz#5f960c3d57728be9f6c65bd84aa6aa613078798e" - integrity sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg== +"@eslint/core@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.13.0.tgz#bf02f209846d3bf996f9e8009db62df2739b458c" + integrity sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw== dependencies: "@types/json-schema" "^7.0.15" @@ -2868,22 +2868,22 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.23.0": - version "9.23.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.23.0.tgz#c09ded4f3dc63b40b933bcaeb853fceddb64da30" - integrity sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw== +"@eslint/js@9.25.1": + version "9.25.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117" + integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg== "@eslint/object-schema@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@eslint/plugin-kit@^0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz#9901d52c136fb8f375906a73dcc382646c3b6a27" - integrity sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g== +"@eslint/plugin-kit@^0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8" + integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA== dependencies: - "@eslint/core" "^0.12.0" + "@eslint/core" "^0.13.0" levn "^0.4.1" "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": @@ -4132,10 +4132,10 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@^1.8.4: - version "1.8.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" - integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== +axios@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901" + integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -4340,6 +4340,14 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: es-errors "^1.3.0" function-bind "^1.1.2" +call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.5, call-bind@^1.0.7, call-bind@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" @@ -4358,6 +4366,14 @@ call-bound@^1.0.2, call-bound@^1.0.3: call-bind-apply-helpers "^1.0.1" get-intrinsic "^1.2.6" +call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -5469,7 +5485,7 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== -es-object-atoms@^1.0.0: +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== @@ -5537,10 +5553,10 @@ escape-string-regexp@^5.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== -eslint-plugin-react@^7.37.4: - version "7.37.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz#1b6c80b6175b6ae4b26055ae4d55d04c414c7181" - integrity sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ== +eslint-plugin-react@^7.37.5: + version "7.37.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" + integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== dependencies: array-includes "^3.1.8" array.prototype.findlast "^1.2.5" @@ -5552,7 +5568,7 @@ eslint-plugin-react@^7.37.4: hasown "^2.0.2" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.8" + object.entries "^1.1.9" object.fromentries "^2.0.8" object.values "^1.2.1" prop-types "^15.8.1" @@ -5587,19 +5603,19 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@^9.23.0: - version "9.23.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.23.0.tgz#b88f3ab6dc83bcb927fdb54407c69ffe5f2441a6" - integrity sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw== +eslint@^9.25.1: + version "9.25.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.25.1.tgz#8a7cf8dd0e6acb858f86029720adb1785ee57580" + integrity sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" - "@eslint/config-array" "^0.19.2" - "@eslint/config-helpers" "^0.2.0" - "@eslint/core" "^0.12.0" + "@eslint/config-array" "^0.20.0" + "@eslint/config-helpers" "^0.2.1" + "@eslint/core" "^0.13.0" "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.23.0" - "@eslint/plugin-kit" "^0.2.7" + "@eslint/js" "9.25.1" + "@eslint/plugin-kit" "^0.2.8" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" @@ -6118,6 +6134,22 @@ get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@ hasown "^2.0.2" math-intrinsics "^1.1.0" +get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" @@ -8375,14 +8407,15 @@ object.assign@^4.1.4, object.assign@^4.1.7: has-symbols "^1.1.0" object-keys "^1.1.1" -object.entries@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" - integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== +object.entries@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== dependencies: - call-bind "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" define-properties "^1.2.1" - es-object-atoms "^1.0.0" + es-object-atoms "^1.1.1" object.fromentries@^2.0.8: version "2.0.8" @@ -9454,12 +9487,12 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57" - integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== +react-dom@^19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623" + integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g== dependencies: - scheduler "^0.25.0" + scheduler "^0.26.0" react-error-overlay@^6.0.11: version "6.0.11" @@ -9601,10 +9634,10 @@ react-youtube@^10.1.0: prop-types "15.8.1" youtube-player "5.5.2" -react@^19.0.0: - version "19.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd" - integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== +react@^19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" + integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== readable-stream@^2.0.1: version "2.3.8" @@ -10021,10 +10054,10 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== -scheduler@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015" - integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA== +scheduler@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" + integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== schema-utils@2.7.0: version "2.7.0" From fb6271d60bba1177294861a26605a1dc24c1241d Mon Sep 17 00:00:00 2001 From: kriswest Date: Wed, 30 Apr 2025 17:27:25 +0100 Subject: [PATCH 48/93] feat: api rate limiting configuration --- config.schema.json | 24 ++++++++++++++++++++++++ proxy.config.json | 4 ++++ src/config/index.ts | 12 ++++++++++-- src/config/types.ts | 5 +++++ src/service/index.js | 5 +---- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/config.schema.json b/config.schema.json index 771e83d0c..f6b7280d1 100644 --- a/config.schema.json +++ b/config.schema.json @@ -24,6 +24,30 @@ "description": "Provide domains to use alternative to the defaults", "type": "object" }, + "rateLimit": { + "description": "API Rate limiting configuration.", + "type": "object", + "properties": { + "windowMs": { + "type": "number", + "description": "How long to remember requests for, in milliseconds (default 10 mins)." + }, + "limit": { + "type": "number", + "description": "How many requests to allow (default 150)." + }, + "statusCode": { + "type": "number", + "description": "HTTP status code after limit is reached (default is 429)." + }, + "message": { + "type": "string", + "description": "Response to return after limit is reached." + } + }, + "required": ["windowMs", "limit"], + "additionalProperties": false + }, "privateOrganizations": { "description": "Pattern searches for listed private organizations are disabled", "type": "array" diff --git a/proxy.config.json b/proxy.config.json index 14d016e4d..a3e853cb2 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -2,6 +2,10 @@ "proxyUrl": "https://github.com", "cookieSecret": "cookie secret", "sessionMaxAgeHours": 12, + "rateLimit": { + "windowMs": 600000, + "limit": 150 + }, "tempPassword": { "sendEmail": false, "emailConfig": {} diff --git a/src/config/index.ts b/src/config/index.ts index a1779ed9d..abf6e8bc1 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,8 +2,7 @@ import { existsSync, readFileSync } from 'fs'; import defaultSettings from '../../proxy.config.json'; import { configFile } from './file'; -import { Authentication, AuthorisedRepo, Database, TempPasswordConfig, UserSettings } from './types'; - +import { Authentication, AuthorisedRepo, Database, RateLimitConfig, TempPasswordConfig, UserSettings } from './types'; let _userSettings: UserSettings | null = null; if (existsSync(configFile)) { @@ -25,6 +24,8 @@ let _urlShortener: string = defaultSettings.urlShortener; let _contactEmail: string = defaultSettings.contactEmail; let _csrfProtection: boolean = defaultSettings.csrfProtection; let _domains: Record = defaultSettings.domains; +let _rateLimit: RateLimitConfig = defaultSettings.rateLimit; + // These are not always present in the default config file, so casting is required let _sslKeyPath: string = (defaultSettings as any).sslKeyPemPath; let _sslCertPath: string = (defaultSettings as any).sslCertPemPath; @@ -198,3 +199,10 @@ export const getDomains = () => { } return _domains; }; + +export const getRateLimit = () => { + if (_userSettings && _userSettings.rateLimit) { + _rateLimit = _userSettings.rateLimit; + } + return _rateLimit; +}; diff --git a/src/config/types.ts b/src/config/types.ts index 7364f6201..333e70a8f 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,3 +1,5 @@ +import { Options as RateLimitOptions } from "express-rate-limit"; + export interface UserSettings { authorisedList: AuthorisedRepo[]; sink: Database[]; @@ -17,6 +19,7 @@ export interface UserSettings { contactEmail: string; csrfProtection: boolean; domains: Record; + rateLimit: RateLimitOptions; } export interface AuthorisedRepo { @@ -43,3 +46,5 @@ export interface TempPasswordConfig { sendEmail: boolean; emailConfig: Record; } + +export type RateLimitConfig = Partial>; diff --git a/src/service/index.js b/src/service/index.js index d384fcd6e..5cd1cb6a8 100644 --- a/src/service/index.js +++ b/src/service/index.js @@ -9,10 +9,7 @@ const db = require('../db'); const rateLimit = require('express-rate-limit'); const lusca = require('lusca'); -const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // limit each IP to 100 requests per windowMs -}); +const limiter = rateLimit(config.getRateLimit); const { GIT_PROXY_UI_PORT: uiPort } = require('../config/env').serverConfig; From 2d69e94ac390c57dd7d29656fc5e845b0a0f0f9d Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 1 May 2025 08:41:15 +0100 Subject: [PATCH 49/93] test: rateLimit config testing --- proxy.config.json | 4 ++-- src/config/index.ts | 12 ++++++++++-- src/config/types.ts | 8 +++++--- src/service/index.js | 2 +- test/testConfig.test.js | 37 +++++++++++++++++++++++-------------- 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/proxy.config.json b/proxy.config.json index a3e853cb2..b16c3dcea 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -3,8 +3,8 @@ "cookieSecret": "cookie secret", "sessionMaxAgeHours": 12, "rateLimit": { - "windowMs": 600000, - "limit": 150 + "windowMs": 60000, + "max": 150 }, "tempPassword": { "sendEmail": false, diff --git a/src/config/index.ts b/src/config/index.ts index abf6e8bc1..3b4088085 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,7 +2,14 @@ import { existsSync, readFileSync } from 'fs'; import defaultSettings from '../../proxy.config.json'; import { configFile } from './file'; -import { Authentication, AuthorisedRepo, Database, RateLimitConfig, TempPasswordConfig, UserSettings } from './types'; +import { + Authentication, + AuthorisedRepo, + Database, + RateLimitConfig, + TempPasswordConfig, + UserSettings, +} from './types'; let _userSettings: UserSettings | null = null; if (existsSync(configFile)) { @@ -94,6 +101,7 @@ export const logConfiguration = () => { console.log(`authorisedList = ${JSON.stringify(getAuthorisedList())}`); console.log(`data sink = ${JSON.stringify(getDatabase())}`); console.log(`authentication = ${JSON.stringify(getAuthentication())}`); + console.log(`rateLimit = ${JSON.stringify(getRateLimit())}`); }; export const getAPIs = () => { @@ -171,7 +179,7 @@ export const getPlugins = () => { _plugins = _userSettings.plugins; } return _plugins; -} +}; export const getSSLKeyPath = () => { if (_userSettings && _userSettings.sslKeyPemPath) { diff --git a/src/config/types.ts b/src/config/types.ts index 333e70a8f..667e0ae54 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -1,4 +1,4 @@ -import { Options as RateLimitOptions } from "express-rate-limit"; +import { Options as RateLimitOptions } from 'express-rate-limit'; export interface UserSettings { authorisedList: AuthorisedRepo[]; @@ -19,7 +19,7 @@ export interface UserSettings { contactEmail: string; csrfProtection: boolean; domains: Record; - rateLimit: RateLimitOptions; + rateLimit: RateLimitConfig; } export interface AuthorisedRepo { @@ -47,4 +47,6 @@ export interface TempPasswordConfig { emailConfig: Record; } -export type RateLimitConfig = Partial>; +export type RateLimitConfig = Partial< + Pick +>; diff --git a/src/service/index.js b/src/service/index.js index 5cd1cb6a8..180800d34 100644 --- a/src/service/index.js +++ b/src/service/index.js @@ -9,7 +9,7 @@ const db = require('../db'); const rateLimit = require('express-rate-limit'); const lusca = require('lusca'); -const limiter = rateLimit(config.getRateLimit); +const limiter = rateLimit(config.getRateLimit()); const { GIT_PROXY_UI_PORT: uiPort } = require('../config/env').serverConfig; diff --git a/test/testConfig.test.js b/test/testConfig.test.js index 125ee7b44..094c346d3 100644 --- a/test/testConfig.test.js +++ b/test/testConfig.test.js @@ -16,8 +16,9 @@ describe('default configuration', function () { expect(config.getDatabase()).to.be.eql(defaultSettings.sink[0]); expect(config.getTempPasswordConfig()).to.be.eql(defaultSettings.tempPassword); expect(config.getAuthorisedList()).to.be.eql(defaultSettings.authorisedList); - expect(config.getSSLKeyPath()).to.be.eql("../../certs/key.pem"); - expect(config.getSSLCertPath()).to.be.eql("../../certs/cert.pem"); + expect(config.getSSLKeyPath()).to.be.eql('../../certs/key.pem'); + expect(config.getSSLCertPath()).to.be.eql('../../certs/cert.pem'); + expect(config.getRateLimit()).to.be.eql(defaultSettings.rateLimit); }); after(function () { delete require.cache[require.resolve('../src/config')]; @@ -94,8 +95,8 @@ describe('user configuration', function () { it('should override default settings for SSL certificate', function () { const user = { - sslKeyPemPath: "my-key.pem", - sslCertPemPath: "my-cert.pem" + sslKeyPemPath: 'my-key.pem', + sslCertPemPath: 'my-cert.pem', }; fs.writeFileSync(tempUserFile, JSON.stringify(user)); @@ -105,6 +106,21 @@ describe('user configuration', function () { expect(config.getSSLCertPath()).to.be.eql(user.sslCertPemPath); }); + it('should override default settings for rate limiting', function () { + const limitConfig = { + rateLimit: { + windowMs: 60000, + limit: 1500, + }, + }; + fs.writeFileSync(tempUserFile, JSON.stringify(limitConfig)); + + const config = require('../src/config'); + + expect(config.getRateLimit().windowMs).to.be.eql(limitConfig.rateLimit.windowMs); + expect(config.getRateLimit().limit).to.be.eql(limitConfig.rateLimit.limit); + }); + afterEach(function () { fs.rmSync(tempUserFile); fs.rmdirSync(tempDir); @@ -116,21 +132,14 @@ describe('validate config files', function () { const config = require('../src/config/file'); it('all valid config files should pass validation', function () { - const validConfigFiles = [ - 'proxy.config.valid-1.json', - 'proxy.config.valid-2.json', - ]; + const validConfigFiles = ['proxy.config.valid-1.json', 'proxy.config.valid-2.json']; for (const testConfigFile of validConfigFiles) { - expect(config.validate(path.join(__dirname, fixtures, testConfigFile))).to - .be.true; + expect(config.validate(path.join(__dirname, fixtures, testConfigFile))).to.be.true; } }); it('all invalid config files should fail validation', function () { - const invalidConfigFiles = [ - 'proxy.config.invalid-1.json', - 'proxy.config.invalid-2.json', - ]; + const invalidConfigFiles = ['proxy.config.invalid-1.json', 'proxy.config.invalid-2.json']; for (const testConfigFile of invalidConfigFiles) { const test = function () { config.validate(path.join(__dirname, fixtures, testConfigFile)); From 86784f0656baba8559359621320585bb9e3b6bc4 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Thu, 1 May 2025 19:27:16 +0900 Subject: [PATCH 50/93] test: fix failing test (login required to view pushes) --- cypress/e2e/autoApproved.cy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/autoApproved.cy.js b/cypress/e2e/autoApproved.cy.js index ae67f3ecd..65d9d65a1 100644 --- a/cypress/e2e/autoApproved.cy.js +++ b/cypress/e2e/autoApproved.cy.js @@ -2,6 +2,8 @@ import moment from 'moment'; describe('Auto-Approved Push Test', () => { beforeEach(() => { + cy.login('admin', 'admin'); + cy.intercept('GET', '/api/v1/push/123', { statusCode: 200, body: { @@ -45,7 +47,7 @@ describe('Auto-Approved Push Test', () => { }); it('should display auto-approved message and verify tooltip contains the expected timestamp', () => { - cy.visit('/admin/push/123'); + cy.visit('/dashboard/push/123'); cy.wait('@getPush'); From d0c20c4f7427c0405fb64764a2d8074e21a50a8b Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 1 May 2025 17:51:50 +0100 Subject: [PATCH 51/93] fix: use limit rather than max in rateLimit config --- proxy.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy.config.json b/proxy.config.json index b16c3dcea..ae08abdfd 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -4,7 +4,7 @@ "sessionMaxAgeHours": 12, "rateLimit": { "windowMs": 60000, - "max": 150 + "limit": 150 }, "tempPassword": { "sendEmail": false, From aef5313e64f31cf9692f6259f621910ee7cf1c85 Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 1 May 2025 18:23:16 +0100 Subject: [PATCH 52/93] docs: regenerate schema reference for rateLimit --- website/docs/configuration/reference.mdx | 330 ++++++++++++++++------- 1 file changed, 235 insertions(+), 95 deletions(-) diff --git a/website/docs/configuration/reference.mdx b/website/docs/configuration/reference.mdx index 6a7eceedf..14d617b79 100644 --- a/website/docs/configuration/reference.mdx +++ b/website/docs/configuration/reference.mdx @@ -7,11 +7,11 @@ description: JSON schema reference documentation for GitProxy **Title:** GitProxy configuration file -| | | -| ------------------------- | ------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Not allowed]](# "Additional Properties not allowed.") | +| | | +| ------------------------- | ----------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Not allowed | **Description:** Configuration for customizing git-proxy @@ -63,11 +63,11 @@ description: JSON schema reference documentation for GitProxy
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | **Description:** Third party APIs @@ -80,11 +80,11 @@ description: JSON schema reference documentation for GitProxy
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | **Description:** Enforce rules and patterns on commits including e-mail and message @@ -97,11 +97,11 @@ description: JSON schema reference documentation for GitProxy
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | **Description:** Customisable questions to add to attestation form @@ -114,11 +114,11 @@ description: JSON schema reference documentation for GitProxy
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | **Description:** Provide domains to use alternative to the defaults @@ -127,7 +127,88 @@ description: JSON schema reference documentation for GitProxy
- 8. [Optional] Property GitProxy configuration file > privateOrganizations + 8. [Optional] Property GitProxy configuration file > rateLimit + +
+ +| | | +| ------------------------- | ----------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Not allowed | + +**Description:** API Rate limiting configuration. + +
+ + 8.1. [Required] Property GitProxy configuration file > rateLimit > windowMs + +
+ +| | | +| ------------ | -------- | +| **Type** | `number` | +| **Required** | Yes | + +**Description:** How long to remember requests for, in milliseconds (default 10 mins). + +
+
+ +
+ + 8.2. [Required] Property GitProxy configuration file > rateLimit > limit + +
+ +| | | +| ------------ | -------- | +| **Type** | `number` | +| **Required** | Yes | + +**Description:** How many requests to allow (default 150). + +
+
+ +
+ + 8.3. [Optional] Property GitProxy configuration file > rateLimit > statusCode + +
+ +| | | +| ------------ | -------- | +| **Type** | `number` | +| **Required** | No | + +**Description:** HTTP status code after limit is reached (default is 429). + +
+
+ +
+ + 8.4. [Optional] Property GitProxy configuration file > rateLimit > message + +
+ +| | | +| ------------ | -------- | +| **Type** | `string` | +| **Required** | No | + +**Description:** Response to return after limit is reached. + +
+
+ +
+
+ +
+ + 9. [Optional] Property GitProxy configuration file > privateOrganizations
@@ -143,7 +224,7 @@ description: JSON schema reference documentation for GitProxy
- 9. [Optional] Property GitProxy configuration file > urlShortener + 10. [Optional] Property GitProxy configuration file > urlShortener
@@ -159,7 +240,7 @@ description: JSON schema reference documentation for GitProxy
- 10. [Optional] Property GitProxy configuration file > contactEmail + 11. [Optional] Property GitProxy configuration file > contactEmail
@@ -175,7 +256,7 @@ description: JSON schema reference documentation for GitProxy
- 11. [Optional] Property GitProxy configuration file > csrfProtection + 12. [Optional] Property GitProxy configuration file > csrfProtection
@@ -191,7 +272,7 @@ description: JSON schema reference documentation for GitProxy
- 12. [Optional] Property GitProxy configuration file > plugins + 13. [Optional] Property GitProxy configuration file > plugins
@@ -206,7 +287,7 @@ description: JSON schema reference documentation for GitProxy | ------------------------------- | ----------- | | [plugins items](#plugins_items) | - | -### 12.1. GitProxy configuration file > plugins > plugins items +### 13.1. GitProxy configuration file > plugins > plugins items | | | | ------------ | -------- | @@ -218,7 +299,7 @@ description: JSON schema reference documentation for GitProxy
- 13. [Optional] Property GitProxy configuration file > authorisedList + 14. [Optional] Property GitProxy configuration file > authorisedList
@@ -233,18 +314,18 @@ description: JSON schema reference documentation for GitProxy | --------------------------------------- | ----------- | | [authorisedRepo](#authorisedList_items) | - | -### 13.1. GitProxy configuration file > authorisedList > authorisedRepo +### 14.1. GitProxy configuration file > authorisedList > authorisedRepo -| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | -| **Defined in** | #/definitions/authorisedRepo | +| | | +| ------------------------- | ---------------------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | +| **Defined in** | #/definitions/authorisedRepo |
- 13.1.1. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > project + 14.1.1. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > project
@@ -258,7 +339,7 @@ description: JSON schema reference documentation for GitProxy
- 13.1.2. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > name + 14.1.2. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > name
@@ -272,7 +353,7 @@ description: JSON schema reference documentation for GitProxy
- 13.1.3. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > url + 14.1.3. [Required] Property GitProxy configuration file > authorisedList > authorisedList items > url
@@ -289,7 +370,7 @@ description: JSON schema reference documentation for GitProxy
- 14. [Optional] Property GitProxy configuration file > sink + 15. [Optional] Property GitProxy configuration file > sink
@@ -304,18 +385,18 @@ description: JSON schema reference documentation for GitProxy | ------------------------------- | ----------- | | [database](#sink_items) | - | -### 14.1. GitProxy configuration file > sink > database +### 15.1. GitProxy configuration file > sink > database -| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | -| **Defined in** | #/definitions/database | +| | | +| ------------------------- | ---------------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | +| **Defined in** | #/definitions/database |
- 14.1.1. [Required] Property GitProxy configuration file > sink > sink items > type + 15.1.1. [Required] Property GitProxy configuration file > sink > sink items > type
@@ -329,7 +410,7 @@ description: JSON schema reference documentation for GitProxy
- 14.1.2. [Required] Property GitProxy configuration file > sink > sink items > enabled + 15.1.2. [Required] Property GitProxy configuration file > sink > sink items > enabled
@@ -343,7 +424,7 @@ description: JSON schema reference documentation for GitProxy
- 14.1.3. [Optional] Property GitProxy configuration file > sink > sink items > connectionString + 15.1.3. [Optional] Property GitProxy configuration file > sink > sink items > connectionString
@@ -357,30 +438,30 @@ description: JSON schema reference documentation for GitProxy
- 14.1.4. [Optional] Property GitProxy configuration file > sink > sink items > options + 15.1.4. [Optional] Property GitProxy configuration file > sink > sink items > options
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed |
- 14.1.5. [Optional] Property GitProxy configuration file > sink > sink items > params + 15.1.5. [Optional] Property GitProxy configuration file > sink > sink items > params
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed |
@@ -390,7 +471,7 @@ description: JSON schema reference documentation for GitProxy
- 15. [Optional] Property GitProxy configuration file > authentication + 16. [Optional] Property GitProxy configuration file > authentication
@@ -405,18 +486,18 @@ description: JSON schema reference documentation for GitProxy | --------------------------------------- | ----------- | | [authentication](#authentication_items) | - | -### 15.1. GitProxy configuration file > authentication > authentication +### 16.1. GitProxy configuration file > authentication > authentication -| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | -| **Defined in** | #/definitions/authentication | +| | | +| ------------------------- | ---------------------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | +| **Defined in** | #/definitions/authentication |
- 15.1.1. [Required] Property GitProxy configuration file > authentication > authentication items > type + 16.1.1. [Required] Property GitProxy configuration file > authentication > authentication items > type
@@ -430,7 +511,7 @@ description: JSON schema reference documentation for GitProxy
- 15.1.2. [Required] Property GitProxy configuration file > authentication > authentication items > enabled + 16.1.2. [Required] Property GitProxy configuration file > authentication > authentication items > enabled
@@ -444,15 +525,15 @@ description: JSON schema reference documentation for GitProxy
- 15.1.3. [Optional] Property GitProxy configuration file > authentication > authentication items > options + 16.1.3. [Optional] Property GitProxy configuration file > authentication > authentication items > options
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed |
@@ -462,21 +543,21 @@ description: JSON schema reference documentation for GitProxy
- 16. [Optional] Property GitProxy configuration file > tempPassword + 17. [Optional] Property GitProxy configuration file > tempPassword
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | **Description:** Toggle the generation of temporary password for git-proxy admin user
- 16.1. [Optional] Property GitProxy configuration file > tempPassword > sendEmail + 17.1. [Optional] Property GitProxy configuration file > tempPassword > sendEmail
@@ -490,15 +571,15 @@ description: JSON schema reference documentation for GitProxy
- 16.2. [Optional] Property GitProxy configuration file > tempPassword > emailConfig + 17.2. [Optional] Property GitProxy configuration file > tempPassword > emailConfig
-| | | -| ------------------------- | ------------------------------------------------------------------------- | -| **Type** | `object` | -| **Required** | No | -| **Additional properties** | [[Any type: allowed]](# "Additional Properties of any type are allowed.") | +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | **Description:** Generic object to configure nodemailer. For full type information, please see https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nodemailer @@ -508,5 +589,64 @@ description: JSON schema reference documentation for GitProxy
+
+ + 18. [Optional] Property GitProxy configuration file > tls + +
+ +| | | +| ------------------------- | ---------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | Any type allowed | + +**Description:** TLS configuration for secure connections + +
+ + 18.1. [Required] Property GitProxy configuration file > tls > enabled + +
+ +| | | +| ------------ | --------- | +| **Type** | `boolean` | +| **Required** | Yes | + +
+
+ +
+ + 18.2. [Required] Property GitProxy configuration file > tls > key + +
+ +| | | +| ------------ | -------- | +| **Type** | `string` | +| **Required** | Yes | + +
+
+ +
+ + 18.3. [Required] Property GitProxy configuration file > tls > cert + +
+ +| | | +| ------------ | -------- | +| **Type** | `string` | +| **Required** | Yes | + +
+
+ +
+
+ ---------------------------------------------------------------------------------------------------------------------------- -Generated using [json-schema-for-humans](https://github.com/coveooss/json-schema-for-humans) on 2024-10-22 at 16:45:32 +0100 +Generated using [json-schema-for-humans](https://github.com/coveooss/json-schema-for-humans) on 2025-05-01 at 18:17:32 +0100 From d1c6c269c394db89a15635778db95f0667ebe318 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 2 May 2025 18:15:07 +0900 Subject: [PATCH 53/93] feat: add uiRouteAuth setting to config --- config.schema.json | 21 +++++++++++++++++++++ proxy.config.json | 15 +++++++++++++++ src/config/index.ts | 8 ++++++++ 3 files changed, 44 insertions(+) diff --git a/config.schema.json b/config.schema.json index 4e9622ca0..529255abe 100644 --- a/config.schema.json +++ b/config.schema.json @@ -88,6 +88,19 @@ "cert": { "type": "string" } }, "required": ["enabled", "key", "cert"] + }, + "uiRouteAuth": { + "description": "UI routes that require authentication (logged in or admin)", + "type": "object", + "properties": { + "enabled": { "type": "boolean" }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/routeAuthRule" + } + } + } } }, "definitions": { @@ -119,6 +132,14 @@ "options": { "type": "object" } }, "required": ["type", "enabled"] + }, + "routeAuthRule": { + "type": "object", + "properties": { + "pattern": { "type": "string" }, + "adminOnly": { "type": "boolean" }, + "loginRequired": { "type": "boolean" } + } } }, "additionalProperties": false diff --git a/proxy.config.json b/proxy.config.json index fdb32a0d0..5a3ddd00c 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -102,5 +102,20 @@ "enabled": true, "key": "certs/key.pem", "cert": "certs/cert.pem" + }, + "uiRouteAuth": { + "enabled": false, + "rules": [ + { + "pattern": "/dashboard/*", + "adminOnly": false, + "loginRequired": true + }, + { + "pattern": "/admin/*", + "adminOnly": true, + "loginRequired": true + } + ] } } diff --git a/src/config/index.ts b/src/config/index.ts index 782c75564..75d2d60c4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -34,6 +34,7 @@ let _domains: Record = defaultSettings.domains; let _tlsEnabled = defaultSettings.tls.enabled; let _tlsKeyPemPath = defaultSettings.tls.key; let _tlsCertPemPath = defaultSettings.tls.cert; +let _uiRouteAuth: Record = defaultSettings.uiRouteAuth; // Get configured proxy URL export const getProxyUrl = () => { @@ -217,3 +218,10 @@ export const getDomains = () => { } return _domains; }; + +export const getUIRouteAuth = () => { + if (_userSettings && _userSettings.uiRouteAuth) { + _uiRouteAuth = _userSettings.uiRouteAuth; + } + return _uiRouteAuth; +}; From 645a27bdb8f3ca7f481052a1d70a96cb671e8e01 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 2 May 2025 18:24:06 +0900 Subject: [PATCH 54/93] feat: add dynamic auth processing for PrivateRoute --- src/routes.jsx | 42 ++++++++++++--- src/service/routes/config.js | 4 ++ .../components/PrivateRoute/PrivateRoute.tsx | 53 +++++++++++++++---- src/ui/services/config.js | 14 ++++- 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/routes.jsx b/src/routes.jsx index a1204b735..6b9273877 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -35,7 +35,11 @@ const dashboardRoutes = [ path: '/repo', name: 'Repositories', icon: RepoIcon, - component: (props) => , + component: (props) => + , layout: '/dashboard', visible: true, }, @@ -43,7 +47,11 @@ const dashboardRoutes = [ path: '/repo/:id', name: 'Repo Details', icon: Person, - component: (props) => , + component: (props) => + , layout: '/dashboard', visible: false, }, @@ -51,7 +59,11 @@ const dashboardRoutes = [ path: '/push', name: 'Dashboard', icon: Dashboard, - component: (props) => , + component: (props) => + , layout: '/dashboard', visible: true, }, @@ -59,7 +71,11 @@ const dashboardRoutes = [ path: '/push/:id', name: 'Open Push Requests', icon: Person, - component: (props) => , + component: (props) => + , layout: '/dashboard', visible: false, }, @@ -67,7 +83,11 @@ const dashboardRoutes = [ path: '/profile', name: 'My Account', icon: AccountCircle, - component: (props) => , + component: (props) => + , layout: '/dashboard', visible: true, }, @@ -75,7 +95,11 @@ const dashboardRoutes = [ path: '/admin/user', name: 'Users', icon: Group, - component: (props) => , + component: (props) => + , layout: '/dashboard', visible: true, }, @@ -83,7 +107,11 @@ const dashboardRoutes = [ path: '/admin/user/:id', name: 'User', icon: Person, - component: (props) => , + component: (props) => + , layout: '/dashboard', visible: false, }, diff --git a/src/service/routes/config.js b/src/service/routes/config.js index 82712ca48..e80d70b5b 100644 --- a/src/service/routes/config.js +++ b/src/service/routes/config.js @@ -15,4 +15,8 @@ router.get('/contactEmail', function ({ res }) { res.send(config.getContactEmail()); }); +router.get('/uiRouteAuth', function ({ res }) { + res.send(config.getUIRouteAuth()); +}); + module.exports = router; diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/PrivateRoute/PrivateRoute.tsx index 4e7a2f4bf..1a320032b 100644 --- a/src/ui/components/PrivateRoute/PrivateRoute.tsx +++ b/src/ui/components/PrivateRoute/PrivateRoute.tsx @@ -1,23 +1,56 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Navigate } from 'react-router-dom'; import { useAuth } from '../../auth/AuthProvider'; +import { getUIRouteAuth } from '../../services/config'; -const PrivateRoute = ({ component: Component, adminOnly = false }) => { +interface PrivateRouteProps { + component: React.ComponentType; + fullRoutePath: string; +} + +interface UIRouteAuth { + enabled: boolean; + rules: { + pattern: string; + adminOnly: boolean; + loginRequired: boolean; + }[]; +} + +const PrivateRoute = ({ component: Component, fullRoutePath }: PrivateRouteProps) => { const { user, isLoading } = useAuth(); - console.debug('PrivateRoute', { user, isLoading, adminOnly }); - if (isLoading) { - console.debug('Auth is loading, waiting'); - return
Loading...
; // TODO: Add loading spinner + const [loginRequired, setLoginRequired] = useState(false); + const [adminOnly, setAdminOnly] = useState(false); + const [authChecked, setAuthChecked] = useState(false); + + useEffect(() => { + getUIRouteAuth((uiRouteAuth: UIRouteAuth) => { + if (uiRouteAuth?.enabled) { + for (const rule of uiRouteAuth.rules) { + if (new RegExp(rule.pattern).test(fullRoutePath)) { + // Allow multiple rules to be applied according to route precedence + // Ex: /dashboard/admin/* will override /dashboard/* + setLoginRequired(loginRequired || rule.loginRequired); + setAdminOnly(adminOnly || rule.adminOnly); + } + } + } else { + console.log('UI route auth is not enabled.'); + } + setAuthChecked(true); + }); + }, [fullRoutePath]); + + if (!authChecked || isLoading) { + return
Loading...
; // TODO: Add a skeleton loader or spinner } - if (!user) { - console.debug('User not logged in, redirecting to login page'); + if (loginRequired && !user) { return ; } - if (adminOnly && !user.admin) { - console.debug('User is not an admin, redirecting to not authorized page'); + if (adminOnly && !user?.admin) { return ; } diff --git a/src/ui/services/config.js b/src/ui/services/config.js index edebc6e12..5536e4a35 100644 --- a/src/ui/services/config.js +++ b/src/ui/services/config.js @@ -25,4 +25,16 @@ const getEmailContact = async (setData) => { }); }; -export { getAttestationConfig, getURLShortener, getEmailContact }; +const getUIRouteAuth = async (setData) => { + const url = new URL(`${baseUrl}/config/uiRouteAuth`); + await axios(url.toString()).then((response) => { + setData(response.data); + }); +}; + +export { + getAttestationConfig, + getURLShortener, + getEmailContact, + getUIRouteAuth, +}; From 351d67d9b8bb681c0bca528fff1da0616fdb30f6 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Fri, 2 May 2025 18:35:50 +0900 Subject: [PATCH 55/93] chore: rename PrivateRoute to RouteGuard --- src/routes.jsx | 16 ++++++++-------- .../RouteGuard.tsx} | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) rename src/ui/components/{PrivateRoute/PrivateRoute.tsx => RouteGuard/RouteGuard.tsx} (91%) diff --git a/src/routes.jsx b/src/routes.jsx index 6b9273877..c409f599e 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -17,7 +17,7 @@ */ import React from 'react'; -import PrivateRoute from './ui/components/PrivateRoute/PrivateRoute'; +import RouteGuard from './ui/components/RouteGuard/RouteGuard'; import Person from '@material-ui/icons/Person'; import OpenPushRequests from './ui/views/OpenPushRequests/OpenPushRequests'; import PushDetails from './ui/views/PushDetails/PushDetails'; @@ -36,7 +36,7 @@ const dashboardRoutes = [ name: 'Repositories', icon: RepoIcon, component: (props) => - , @@ -48,7 +48,7 @@ const dashboardRoutes = [ name: 'Repo Details', icon: Person, component: (props) => - , @@ -60,7 +60,7 @@ const dashboardRoutes = [ name: 'Dashboard', icon: Dashboard, component: (props) => - , @@ -72,7 +72,7 @@ const dashboardRoutes = [ name: 'Open Push Requests', icon: Person, component: (props) => - , @@ -84,7 +84,7 @@ const dashboardRoutes = [ name: 'My Account', icon: AccountCircle, component: (props) => - , @@ -96,7 +96,7 @@ const dashboardRoutes = [ name: 'Users', icon: Group, component: (props) => - , @@ -108,7 +108,7 @@ const dashboardRoutes = [ name: 'User', icon: Person, component: (props) => - , diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/RouteGuard/RouteGuard.tsx similarity index 91% rename from src/ui/components/PrivateRoute/PrivateRoute.tsx rename to src/ui/components/RouteGuard/RouteGuard.tsx index 1a320032b..d3146b63d 100644 --- a/src/ui/components/PrivateRoute/PrivateRoute.tsx +++ b/src/ui/components/RouteGuard/RouteGuard.tsx @@ -3,7 +3,7 @@ import { Navigate } from 'react-router-dom'; import { useAuth } from '../../auth/AuthProvider'; import { getUIRouteAuth } from '../../services/config'; -interface PrivateRouteProps { +interface RouteGuardProps { component: React.ComponentType; fullRoutePath: string; } @@ -17,7 +17,7 @@ interface UIRouteAuth { }[]; } -const PrivateRoute = ({ component: Component, fullRoutePath }: PrivateRouteProps) => { +const RouteGuard = ({ component: Component, fullRoutePath }: RouteGuardProps) => { const { user, isLoading } = useAuth(); const [loginRequired, setLoginRequired] = useState(false); @@ -57,4 +57,4 @@ const PrivateRoute = ({ component: Component, fullRoutePath }: PrivateRouteProps return ; }; -export default PrivateRoute; +export default RouteGuard; From 8962c7e6a9daf66d745b7527b5f7fb72ef102922 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Tue, 6 May 2025 11:41:50 +0100 Subject: [PATCH 56/93] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c6591558e..24e178b1b 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,13 @@ [![NPM](https://img.shields.io/npm/v/@finos/git-proxy?colorA=00C586&colorB=000000)](https://www.npmjs.com/package/@finos/git-proxy) [![Build](https://img.shields.io/github/actions/workflow/status/finos/git-proxy/ci.yml?branch=main&label=CI&logo=github&colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/finos/git-proxy/branch/main/graph/badge.svg)](https://codecov.io/gh/finos/git-proxy) -[![git-proxy](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy/badge)](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy) [![Documentation](https://img.shields.io/badge/_-documentation-000000?colorA=00C586&logo=docusaurus&logoColor=FFFFFF&)](https://git-proxy.finos.org)
[![License](https://img.shields.io/github/license/finos/git-proxy?colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/blob/main/LICENSE) [![Contributors](https://img.shields.io/github/contributors/finos/git-proxy?colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/graphs/contributors) [![Slack](https://img.shields.io/badge/_-Chat_on_Slack-000000.svg?logo=slack&colorA=00C586)](https://app.slack.com/client/T01E7QRQH97/C06LXNW0W76) -[![Stars](https://img.shields.io/github/stars/finos/git-proxy?colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/stargazers) -[![Forks](https://img.shields.io/github/forks/finos/git-proxy?colorA=00C586&colorB=000000)](https://github.com/finos/git-proxy/forks) +[![git-proxy](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy/badge)](https://api.securityscorecards.dev/projects/github.com/finos/git-proxy) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10520/badge)](https://www.bestpractices.dev/projects/10520)
From 6e0a779aedefe073f4c9f83ce0dc93e6ad768715 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 10:49:32 +0000 Subject: [PATCH 57/93] chore(deps): update dependency node to v20 - workflows - .github/workflows/unused-dependencies.yml --- .github/workflows/npm.yml | 2 +- .github/workflows/sample-publish.yml | 2 +- .github/workflows/unused-dependencies.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 8445a2123..7e592f77b 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -18,7 +18,7 @@ jobs: # Setup .npmrc file to publish to npm - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: '18.x' + node-version: '20.x' registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build diff --git a/.github/workflows/sample-publish.yml b/.github/workflows/sample-publish.yml index c61370d82..b6db78b02 100644 --- a/.github/workflows/sample-publish.yml +++ b/.github/workflows/sample-publish.yml @@ -21,7 +21,7 @@ jobs: # Setup .npmrc file to publish to npm - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: '18.x' + node-version: '20.x' registry-url: 'https://registry.npmjs.org' - name: publish sample package run: npm install --include peer && npm publish --access=public diff --git a/.github/workflows/unused-dependencies.yml b/.github/workflows/unused-dependencies.yml index 61f4222c0..a401f7b50 100644 --- a/.github/workflows/unused-dependencies.yml +++ b/.github/workflows/unused-dependencies.yml @@ -18,7 +18,7 @@ jobs: - name: 'Setup Node.js' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: - node-version: '18.x' + node-version: '20.x' - name: 'Run depcheck' run: | npx depcheck --skip-missing --ignores="tsx,@babel/*,@commitlint/*,eslint,eslint-*,husky,mocha,ts-mocha,ts-node,concurrently,nyc,prettier,typescript,tsconfig-paths,vite-tsconfig-paths" From 55d11fd043236cfe88d3d24a38b988d755ac2a1e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 11:55:51 +0000 Subject: [PATCH 58/93] fix(deps): update npm - li-cli - experimental/li-cli/package.json --- experimental/li-cli/package-lock.json | 24 ++++++++++++------------ experimental/li-cli/package.json | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/experimental/li-cli/package-lock.json b/experimental/li-cli/package-lock.json index dee7d0811..5636e7646 100644 --- a/experimental/li-cli/package-lock.json +++ b/experimental/li-cli/package-lock.json @@ -12,17 +12,17 @@ "@inquirer/prompts": "^7.5.0", "yaml": "^2.7.1", "yargs": "^17.7.2", - "zod": "^3.24.3" + "zod": "^3.24.4" }, "devDependencies": { "@jest/globals": "^29.7.0", - "@types/node": "^22.15.3", + "@types/node": "^22.15.12", "@types/yargs": "^17.0.33", "jest": "^29.7.0", "rimraf": "^6.0.1", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "tsc-alias": "^1.8.15", + "tsc-alias": "^1.8.16", "tslib": "^2.8.1", "typescript": "^5.8.3" } @@ -1575,9 +1575,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", - "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", + "version": "22.15.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.12.tgz", + "integrity": "sha512-K0fpC/ZVeb8G9rm7bH7vI0KAec4XHEhBam616nVJCV51bKzJ6oA3luG4WdKoaztxe70QaNjS/xBmcDLmr4PiGw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4892,9 +4892,9 @@ } }, "node_modules/tsc-alias": { - "version": "1.8.15", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.15.tgz", - "integrity": "sha512-yKLVx8ddUurRwhVcS6JFF2ZjksOX2ZWDRIdgt+PQhJBDegIdAdilptiHsuAbx9UFxa16GFrxeKQ2kTcGvR6fkQ==", + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", "dev": true, "license": "MIT", "dependencies": { @@ -5208,9 +5208,9 @@ } }, "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/experimental/li-cli/package.json b/experimental/li-cli/package.json index 4b24950b4..794e86446 100644 --- a/experimental/li-cli/package.json +++ b/experimental/li-cli/package.json @@ -16,17 +16,17 @@ "@inquirer/prompts": "^7.5.0", "yaml": "^2.7.1", "yargs": "^17.7.2", - "zod": "^3.24.3" + "zod": "^3.24.4" }, "devDependencies": { "@jest/globals": "^29.7.0", - "@types/node": "^22.15.3", + "@types/node": "^22.15.12", "@types/yargs": "^17.0.33", "jest": "^29.7.0", "rimraf": "^6.0.1", "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "tsc-alias": "^1.8.15", + "tsc-alias": "^1.8.16", "tslib": "^2.8.1", "typescript": "^5.8.3" } From f1f445ef64c1e518ad6fb581febec7ac909a67ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 14:17:04 +0000 Subject: [PATCH 59/93] chore(deps): update github-actions - workflows - .github/workflows/ci.yml --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 794c7556e..33c5fd418 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: path: build - name: Run cypress test - uses: cypress-io/github-action@108b8684ae52e735ff7891524cbffbcd4be5b19f # v6.7.16 + uses: cypress-io/github-action@0ee1130f05f69098ab5c560bd198fecf5a14d75b # v6.9.0 with: start: npm start & wait-on: "http://localhost:3000" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ec9d82b44..051a3e424 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -60,7 +60,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3 + uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,7 +74,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3 + uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -87,6 +87,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3 + uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 7f5579f94..e6320599b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -72,6 +72,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: sarif_file: results.sarif From 26bf0b0d18e74c71b503f391fc8ca24bf775afb9 Mon Sep 17 00:00:00 2001 From: kriswest Date: Wed, 7 May 2025 14:34:09 +0100 Subject: [PATCH 60/93] feat: report illegal commit messages in error message --- src/proxy/processors/push-action/checkCommitMessages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy/processors/push-action/checkCommitMessages.ts b/src/proxy/processors/push-action/checkCommitMessages.ts index 577a572af..506f872cf 100644 --- a/src/proxy/processors/push-action/checkCommitMessages.ts +++ b/src/proxy/processors/push-action/checkCommitMessages.ts @@ -70,7 +70,7 @@ const exec = async (req: any, action: Action): Promise => { step.error = true; step.log(`The following commit messages are illegal: ${illegalMessages}`); step.setError( - '\n\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\n\n', + `\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\nThe following commit messages are illegal: ${illegalMessages}\n\n`, ); action.addStep(step); From 2039d08595fe7619f85b278f77c5f75833c461e1 Mon Sep 17 00:00:00 2001 From: kriswest Date: Wed, 7 May 2025 14:41:12 +0100 Subject: [PATCH 61/93] feat: stringify the illegal commit messages when reporting them --- src/proxy/processors/push-action/checkCommitMessages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/proxy/processors/push-action/checkCommitMessages.ts b/src/proxy/processors/push-action/checkCommitMessages.ts index 506f872cf..7a95f6c12 100644 --- a/src/proxy/processors/push-action/checkCommitMessages.ts +++ b/src/proxy/processors/push-action/checkCommitMessages.ts @@ -70,7 +70,7 @@ const exec = async (req: any, action: Action): Promise => { step.error = true; step.log(`The following commit messages are illegal: ${illegalMessages}`); step.setError( - `\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\nThe following commit messages are illegal: ${illegalMessages}\n\n`, + `\n\n\nYour push has been blocked.\nPlease ensure your commit message(s) does not contain sensitive information or URLs.\n\nThe following commit messages are illegal: ${JSON.stringify(illegalMessages)}\n\n`, ); action.addStep(step); From 405aadde4485f31ba744d76044dbecf392dfeebf Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 14 May 2025 12:28:59 +0100 Subject: [PATCH 62/93] chore: dont publish experimental or cypress --- .npmignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.npmignore b/.npmignore index 27087e67d..286c7a75f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,5 @@ # This file required to override .gitignore when publishing to npm website/ plugins/ +experimental/ +cypress/ From c730f972c69641f531602dc3d83b379c9e77262e Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 14 May 2025 12:31:11 +0100 Subject: [PATCH 63/93] ci: generate js and definitions from ts and place in original location --- package.json | 7 +++++-- scripts/build-for-publish.sh | 26 ++++++++++++++++++++++++++ scripts/undo-build.sh | 11 +++++++++++ src/proxy/index.ts | 6 +++--- tsconfig.json | 4 +++- tsconfig.publish.json | 15 +++++++++++++++ 6 files changed, 63 insertions(+), 6 deletions(-) create mode 100755 scripts/build-for-publish.sh create mode 100755 scripts/undo-build.sh create mode 100644 tsconfig.publish.json diff --git a/package.json b/package.json index 2db0042be..d44634451 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,11 @@ "clientinstall": "npm install --prefix client", "server": "tsx index.ts", "start": "concurrently \"npm run server\" \"npm run client\"", - "build": "vite build", - "build-ts": "tsc", + "build": "npm run build-ui && npm run build-lib", + "build-ui": "vite build", + "build-lib": "./scripts/build-for-publish.sh", + "restore-lib": "./scripts/undo-build.sh", + "check-types": "tsc", "test": "NODE_ENV=test ts-mocha './test/*.js' --exit", "test-coverage": "nyc npm run test", "test-coverage-ci": "nyc --reporter=lcovonly --reporter=text npm run test", diff --git a/scripts/build-for-publish.sh b/scripts/build-for-publish.sh new file mode 100755 index 000000000..d326e69b8 --- /dev/null +++ b/scripts/build-for-publish.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euxo pipefail + +# This script allows for emitting js and definitions from the typescript into +# the same import locations as the original files. +# When we adjust how we import the library we can move to a "dist" folder and +# explicit "exports". + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +rm -rf dist || true +tsc --project tsconfig.publish.json +# replace tsx with node for the new index.js +sed -ie '1s/tsx/node/' dist/index.js +# ensure it's executable +chmod +x dist/index.js +# move the ts source +mv src src-old +# move the built source +mv dist/src dist/index.js dist/index.d.ts . +# copy back unchanged ui code +# could probably drop this as the ui code shouldn't really be imported from +# the main package but keep for compat until split out. +mv src-old/ui src/ui +rm -rf src-old index.ts dist diff --git a/scripts/undo-build.sh b/scripts/undo-build.sh new file mode 100755 index 000000000..998123e09 --- /dev/null +++ b/scripts/undo-build.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euxo pipefail + +# Undo what was done by build-for-publish.sh in the event this was ran locally + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +rm -rf dist index.js index.d.ts || true +git checkout src index.ts +git clean -f src diff --git a/src/proxy/index.ts b/src/proxy/index.ts index 04fce52d7..0a49d0a6f 100644 --- a/src/proxy/index.ts +++ b/src/proxy/index.ts @@ -27,7 +27,7 @@ const options = { cert: getTLSEnabled() ? fs.readFileSync(getTLSCertPemPath()) : undefined, }; -const proxyPreparations = async () => { +export const proxyPreparations = async () => { const plugins = getPlugins(); const pluginLoader = new PluginLoader(plugins); await pluginLoader.load(); @@ -47,7 +47,7 @@ const proxyPreparations = async () => { }; // just keep this async incase it needs async stuff in the future -const createApp = async () => { +export const createApp = async () => { const app = express(); // Setup the proxy middleware app.use(bodyParser.raw(options)); @@ -55,7 +55,7 @@ const createApp = async () => { return app; }; -const start = async () => { +export const start = async () => { const app = await createApp(); await proxyPreparations(); http.createServer(options as any, app).listen(proxyHttpPort, () => { diff --git a/tsconfig.json b/tsconfig.json index 805153d01..a389ca8c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "moduleResolution": "Node", "strict": true, "noEmit": true, + "declaration": true, "skipLibCheck": true, "isolatedModules": true, "module": "CommonJS", @@ -15,5 +16,6 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true }, - "include": ["src"] + "include": ["."], + "exclude": ["experimental/**", "plugins/**"] } diff --git a/tsconfig.publish.json b/tsconfig.publish.json new file mode 100644 index 000000000..ef9be14f7 --- /dev/null +++ b/tsconfig.publish.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "./dist" + }, + "exclude": [ + "experimental/**", + "plugins/**", + "./dist/**", + "./src/ui", + "./src/**/*.jsx", + "./src/context.js" + ] +} From cd1b431ee1b65b8ecc1de8d24b6c33bb74e02260 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 14 May 2025 15:44:25 +0100 Subject: [PATCH 64/93] ci: add guardrails and add to publish --- .github/workflows/npm.yml | 2 ++ scripts/build-for-publish.sh | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 7e592f77b..274019886 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -22,6 +22,8 @@ jobs: registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build + env: + IS_PUBLISHING: 'YES' - run: npm publish --access=public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/scripts/build-for-publish.sh b/scripts/build-for-publish.sh index d326e69b8..1c9ac4130 100755 --- a/scripts/build-for-publish.sh +++ b/scripts/build-for-publish.sh @@ -1,11 +1,20 @@ #!/usr/bin/env bash -set -euxo pipefail +set -euo pipefail # This script allows for emitting js and definitions from the typescript into # the same import locations as the original files. # When we adjust how we import the library we can move to a "dist" folder and # explicit "exports". +if [ "${IS_PUBLISHING:-}" != "YES" ]; then + echo "This script is intended to prepare the directory for publishing" + echo "and replaces files. If you only want to build the UI run \`npm run build-ui\`." + echo "Otherwise set IS_PUBLISHING to \"YES\"" + exit 1 +fi + +set -x + REPO_ROOT="$(git rev-parse --show-toplevel)" cd "$REPO_ROOT" From 8810a636704683f83c7b65b3ef694b5f4b3a9097 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 14 May 2025 16:01:57 +0100 Subject: [PATCH 65/93] ci: correctly build frontend in ci --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33c5fd418..972fd7e1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,8 +60,8 @@ jobs: # if: ${{ steps.test.outputs.exit_code }} != 0 # run: exit ${{ steps.test.outputs.exit_code }} - - name: Build application - run: npm run build + - name: Build frontend + run: npm run build-ui - name: Save build folder uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 From 2a25b9c5d71b3c82262dad82f12402b333c4aa96 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Thu, 15 May 2025 15:36:07 +0100 Subject: [PATCH 66/93] chore: bump by minor to v1.12.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d2accb66c..619cb0fce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/git-proxy", - "version": "1.11.0", + "version": "1.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/git-proxy", - "version": "1.11.0", + "version": "1.12.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" diff --git a/package.json b/package.json index d44634451..fcad06c46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@finos/git-proxy", - "version": "1.11.0", + "version": "1.12.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { "cli": "node ./packages/git-proxy-cli/index.js", From b6157f990ede59c602a8a92e3b727ab637f1eed0 Mon Sep 17 00:00:00 2001 From: stingrayza Date: Mon, 19 May 2025 13:47:09 +0100 Subject: [PATCH 67/93] docs(readme): fix markdown in README Signed-off-by: stingrayza --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 24e178b1b..98740e769 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ $ git push proxy $(git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remo Using the default configuration, GitProxy intercepts the push and _blocks_ it. To enable code pushing to your fork via GitProxy, add your repository URL into the GitProxy config file (`proxy.config.json`). For more information, refer to [our documentation](https://git-proxy.finos.org). ## Documentation + For detailed step-by-step instructions for how to install, deploy & configure GitProxy and customize for your environment, see the [project's documentation](https://git-proxy.finos.org/docs/): @@ -101,11 +102,11 @@ If you identify a security vulnerability in the codebase, please follow the step ## Code of Conduct -We are committed to making open source an enjoyable and respectful experience for our community. See CODE_OF_CONDUCT for more information. +We are committed to making open source an enjoyable and respectful experience for our community. See [`CODE_OF_CONDUCT`](CODE_OF_CONDUCT.md) for more information. ## License -This project is distributed under the Apache-2.0 license. See LICENSE for more information. +This project is distributed under the Apache-2.0 license. See [`LICENSE`](LICENSE) for more information. ## Contact @@ -115,4 +116,4 @@ If you can't access Slack, you can also [subscribe to our mailing list](mailto:g Join our [fortnightly Zoom meeting](https://zoom.us/j/97235277537?pwd=aDJsaE8zcDJpYW1vZHJmSTJ0RXNZUT09) on Monday, 11AM EST (odd week numbers). Send an e-mail to [help@finos.org](mailto:help@finos.org) to get a calendar invitation. -Otherwise, if you have a deeper query or require more support, please [raise an issue](https://github.com/finos/git-proxy/issues). +Otherwise, if you have a deeper query or require more support, please [raise an issue](https://github.com/finos/git-proxy/issues). From 4f350f947bac36305b1b025109dcbf77df2091f1 Mon Sep 17 00:00:00 2001 From: Jack Kelly Date: Wed, 14 May 2025 09:59:21 +0100 Subject: [PATCH 68/93] feat: integrate gitleaks --- src/proxy/chain.ts | 10 +- src/proxy/processors/push-action/gitleaks.ts | 184 +++++++++++++++++++ src/proxy/processors/push-action/index.ts | 2 + test/chain.test.js | 8 + 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 src/proxy/processors/push-action/gitleaks.ts diff --git a/src/proxy/chain.ts b/src/proxy/chain.ts index 41a7cc495..55e271a3e 100644 --- a/src/proxy/chain.ts +++ b/src/proxy/chain.ts @@ -14,12 +14,16 @@ const pushActionChain: ((req: any, action: Action) => Promise)[] = [ proc.push.writePack, proc.push.preReceive, proc.push.getDiff, + // run before clear remote + proc.push.gitleaks, proc.push.clearBareClone, proc.push.scanDiff, proc.push.blockForAuth, ]; -const pullActionChain: ((req: any, action: Action) => Promise)[] = [proc.push.checkRepoInAuthorisedList]; +const pullActionChain: ((req: any, action: Action) => Promise)[] = [ + proc.push.checkRepoInAuthorisedList, +]; let pluginsInserted = false; @@ -57,7 +61,9 @@ export const executeChain = async (req: any, res: any): Promise => { */ let chainPluginLoader: PluginLoader; -const getChain = async (action: Action): Promise<((req: any, action: Action) => Promise)[]> => { +const getChain = async ( + action: Action, +): Promise<((req: any, action: Action) => Promise)[]> => { if (chainPluginLoader === undefined) { console.error( 'Plugin loader was not initialized! This is an application error. Please report it to the GitProxy maintainers. Skipping plugins...', diff --git a/src/proxy/processors/push-action/gitleaks.ts b/src/proxy/processors/push-action/gitleaks.ts new file mode 100644 index 000000000..73aabe550 --- /dev/null +++ b/src/proxy/processors/push-action/gitleaks.ts @@ -0,0 +1,184 @@ +import { Action, Step } from '../../actions'; +import { getAPIs } from '../../../config'; +import { spawn } from 'node:child_process'; +import fs from 'node:fs/promises'; +import { PathLike } from 'node:fs'; + +const EXIT_CODE = 99; + +function runCommand( + cwd: string, + command: string, + args: readonly string[] = [], +): Promise<{ + exitCode: number | null; + stdout: string; + stderr: string; +}> { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { cwd, shell: true }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data?.toString() ?? ''; + }); + + child.stderr.on('data', (data) => { + stderr += data?.toString() ?? ''; + }); + + child.on('close', (exitCode) => { + resolve({ exitCode, stdout, stderr }); + }); + + child.on('error', (err) => { + reject(err); + }); + }); +} + +type ConfigOptions = { + enabled: boolean; + ignoreGitleaksAllow: boolean; + noColor: boolean; + configPath: string | undefined; +}; + +const DEFAULT_CONFIG: ConfigOptions = { + // adding gitleaks into main git-proxy for now as default off + // in the future will likely be moved to a plugin where it'll be default on + enabled: false, + ignoreGitleaksAllow: true, + noColor: false, + configPath: undefined, +}; + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +async function fileIsReadable(path: PathLike): Promise { + try { + if (!(await fs.stat(path)).isFile()) { + return false; + } + await fs.access(path, fs.constants.R_OK); + return true; + } catch (e) { + return false; + } +} + +const getPluginConfig = async (): Promise => { + const userConfig = getAPIs(); + if (typeof userConfig !== 'object') { + return DEFAULT_CONFIG; + } + if (!Object.hasOwn(userConfig, 'gitleaks')) { + return DEFAULT_CONFIG; + } + const gitleaksConfig = userConfig.gitleaks; + if (!isRecord(gitleaksConfig)) { + return DEFAULT_CONFIG; + } + + let configPath: string | undefined = undefined; + if (typeof gitleaksConfig.configPath === 'string') { + const userConfigPath = gitleaksConfig.configPath.trim(); + if (userConfigPath.length > 0 && (await fileIsReadable(userConfigPath))) { + configPath = userConfigPath; + } else { + console.error('could not read file at the config path provided, will not be fed to gitleaks'); + throw new Error("could not check user's config path"); + } + } + + // TODO: integrate zod + return { + enabled: + typeof gitleaksConfig.enabled === 'boolean' ? gitleaksConfig.enabled : DEFAULT_CONFIG.enabled, + ignoreGitleaksAllow: + typeof gitleaksConfig.ignoreGitleaksAllow === 'boolean' + ? gitleaksConfig.ignoreGitleaksAllow + : DEFAULT_CONFIG.ignoreGitleaksAllow, + noColor: + typeof gitleaksConfig.noColor === 'boolean' ? gitleaksConfig.noColor : DEFAULT_CONFIG.noColor, + configPath, + }; +}; + +const exec = async (req: any, action: Action): Promise => { + const step = new Step('gitleaks'); + + let config: ConfigOptions | undefined = undefined; + try { + config = await getPluginConfig(); + } catch (e) { + console.error('failed to get gitleaks config, please fix the error:', e); + action.error = true; + step.setError('failed setup gitleaks, please contact an administrator\n'); + action.addStep(step); + return action; + } + + const { commitFrom, commitTo } = action; + const workingDir = `${action.proxyGitPath}/${action.repoName}`; + console.log(`Scanning range with gitleaks: ${commitFrom}:${commitTo}`, workingDir); + + try { + const gitRootCommit = await runCommand(workingDir, 'git', [ + 'rev-list', + '--max-parents=0', + 'HEAD', + ]); + if (gitRootCommit.exitCode !== 0) { + throw new Error('failed to run git'); + } + const rootCommit = gitRootCommit.stdout.trim(); + + const gitleaksArgs = [ + `--exit-code=${EXIT_CODE}`, + '--platform=none', + config.configPath ? `--config=${config.configPath}` : undefined, // allow for custom config + config.ignoreGitleaksAllow ? '--ignore-gitleaks-allow' : undefined, // force scanning for security + '--no-banner', // reduce git-proxy error output + config.noColor ? '--no-color' : undefined, // colour output should appear properly in the console + '--redact', // avoid printing the contents + '--verbose', + 'git', + // not using --no-merges to be sure we're scanning the diff + // only add ^ if the commitFrom isn't the repo's rootCommit + `--log-opts='--first-parent ${rootCommit === commitFrom ? rootCommit : `${commitFrom}^`}..${commitTo}'`, + ].filter((v) => typeof v === 'string'); + const gitleaks = await runCommand(workingDir, 'gitleaks', gitleaksArgs); + + if (gitleaks.exitCode !== 0) { + // any failure + step.error = true; + if (gitleaks.exitCode !== EXIT_CODE) { + step.setError('failed to run gitleaks, please contact an administrator\n'); + } else { + // exit code matched our gitleaks findings exit code + // newline prefix to avoid tab indent at the start + step.setError('\n' + gitleaks.stdout + gitleaks.stderr); + } + } else { + console.log('succeded'); + console.log(gitleaks.stderr); + } + } catch (e) { + action.error = true; + step.setError('failed to spawn gitleaks, please contact an administrator\n'); + action.addStep(step); + return action; + } + + action.addStep(step); + return action; +}; + +exec.displayName = 'gitleaks.exec'; + +export { exec }; diff --git a/src/proxy/processors/push-action/index.ts b/src/proxy/processors/push-action/index.ts index 9fc2065f7..704e6febf 100644 --- a/src/proxy/processors/push-action/index.ts +++ b/src/proxy/processors/push-action/index.ts @@ -5,6 +5,7 @@ import { exec as audit } from './audit'; import { exec as pullRemote } from './pullRemote'; import { exec as writePack } from './writePack'; import { exec as getDiff } from './getDiff'; +import { exec as gitleaks } from './gitleaks'; import { exec as scanDiff } from './scanDiff'; import { exec as blockForAuth } from './blockForAuth'; import { exec as checkIfWaitingAuth } from './checkIfWaitingAuth'; @@ -21,6 +22,7 @@ export { pullRemote, writePack, getDiff, + gitleaks, scanDiff, blockForAuth, checkIfWaitingAuth, diff --git a/test/chain.test.js b/test/chain.test.js index d646b9dc7..d2c14620b 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -27,6 +27,7 @@ const mockPushProcessors = { writePack: sinon.stub(), preReceive: sinon.stub(), getDiff: sinon.stub(), + gitleaks: sinon.stub(), clearBareClone: sinon.stub(), scanDiff: sinon.stub(), blockForAuth: sinon.stub(), @@ -42,6 +43,7 @@ mockPushProcessors.pullRemote.displayName = 'pullRemote'; mockPushProcessors.writePack.displayName = 'writePack'; mockPushProcessors.preReceive.displayName = 'preReceive'; mockPushProcessors.getDiff.displayName = 'getDiff'; +mockPushProcessors.gitleaks.displayName = 'gitleaks'; mockPushProcessors.clearBareClone.displayName = 'clearBareClone'; mockPushProcessors.scanDiff.displayName = 'scanDiff'; mockPushProcessors.blockForAuth.displayName = 'blockForAuth'; @@ -179,6 +181,7 @@ describe('proxy chain', function () { mockPushProcessors.writePack.resolves(continuingAction); mockPushProcessors.preReceive.resolves(continuingAction); mockPushProcessors.getDiff.resolves(continuingAction); + mockPushProcessors.gitleaks.resolves(continuingAction); mockPushProcessors.clearBareClone.resolves(continuingAction); mockPushProcessors.scanDiff.resolves(continuingAction); mockPushProcessors.blockForAuth.resolves(continuingAction); @@ -196,6 +199,7 @@ describe('proxy chain', function () { expect(mockPushProcessors.writePack.called).to.be.true; expect(mockPushProcessors.preReceive.called).to.be.true; expect(mockPushProcessors.getDiff.called).to.be.true; + expect(mockPushProcessors.gitleaks.called).to.be.true; expect(mockPushProcessors.clearBareClone.called).to.be.true; expect(mockPushProcessors.scanDiff.called).to.be.true; expect(mockPushProcessors.blockForAuth.called).to.be.true; @@ -276,6 +280,7 @@ describe('proxy chain', function () { }); mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.gitleaks.resolves(action); mockPushProcessors.clearBareClone.resolves(action); mockPushProcessors.scanDiff.resolves(action); mockPushProcessors.blockForAuth.resolves(action); @@ -322,6 +327,7 @@ describe('proxy chain', function () { }); mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.gitleaks.resolves(action); mockPushProcessors.clearBareClone.resolves(action); mockPushProcessors.scanDiff.resolves(action); mockPushProcessors.blockForAuth.resolves(action); @@ -368,6 +374,7 @@ describe('proxy chain', function () { }); mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.gitleaks.resolves(action); mockPushProcessors.clearBareClone.resolves(action); mockPushProcessors.scanDiff.resolves(action); mockPushProcessors.blockForAuth.resolves(action); @@ -413,6 +420,7 @@ describe('proxy chain', function () { }); mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.gitleaks.resolves(action); mockPushProcessors.clearBareClone.resolves(action); mockPushProcessors.scanDiff.resolves(action); mockPushProcessors.blockForAuth.resolves(action); From b10821a2586775ce214e9b1233bbd50258605e2c Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Mon, 19 May 2025 14:36:06 +0100 Subject: [PATCH 69/93] chore: bump by minor to v1.13.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 619cb0fce..9a566172e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/git-proxy", - "version": "1.12.0", + "version": "1.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/git-proxy", - "version": "1.12.0", + "version": "1.13.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" diff --git a/package.json b/package.json index fcad06c46..3ce14e6aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@finos/git-proxy", - "version": "1.12.0", + "version": "1.13.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { "cli": "node ./packages/git-proxy-cli/index.js", From eab0e3ec48a7dd970dc1178330c42d7ec2468020 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 13:42:27 +0000 Subject: [PATCH 70/93] fix(deps): update dependency eslint to ^9.27.0 - website - website/package.json --- website/package.json | 2 +- website/yarn.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/website/package.json b/website/package.json index 6be24ea2b..9456a83c4 100644 --- a/website/package.json +++ b/website/package.json @@ -16,7 +16,7 @@ "axios": "^1.9.0", "classnames": "^2.5.1", "clsx": "^2.1.1", - "eslint": "^9.25.1", + "eslint": "^9.27.0", "eslint-plugin-react": "^7.37.5", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/website/yarn.lock b/website/yarn.lock index 9cdc65fbc..ce259a41f 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2846,10 +2846,10 @@ resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.2.1.tgz#26042c028d1beee5ce2235a7929b91c52651646d" integrity sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw== -"@eslint/core@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.13.0.tgz#bf02f209846d3bf996f9e8009db62df2739b458c" - integrity sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw== +"@eslint/core@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.14.0.tgz#326289380968eaf7e96f364e1e4cf8f3adf2d003" + integrity sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg== dependencies: "@types/json-schema" "^7.0.15" @@ -2868,22 +2868,22 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.25.1": - version "9.25.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117" - integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg== +"@eslint/js@9.27.0": + version "9.27.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.27.0.tgz#181a23460877c484f6dd03890f4e3fa2fdeb8ff0" + integrity sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA== "@eslint/object-schema@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f" integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== -"@eslint/plugin-kit@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz#47488d8f8171b5d4613e833313f3ce708e3525f8" - integrity sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA== +"@eslint/plugin-kit@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz#b71b037b2d4d68396df04a8c35a49481e5593067" + integrity sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w== dependencies: - "@eslint/core" "^0.13.0" + "@eslint/core" "^0.14.0" levn "^0.4.1" "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": @@ -5603,19 +5603,19 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@^9.25.1: - version "9.25.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.25.1.tgz#8a7cf8dd0e6acb858f86029720adb1785ee57580" - integrity sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ== +eslint@^9.27.0: + version "9.27.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.27.0.tgz#a587d3cd5b844b68df7898944323a702afe38979" + integrity sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" "@eslint/config-array" "^0.20.0" "@eslint/config-helpers" "^0.2.1" - "@eslint/core" "^0.13.0" + "@eslint/core" "^0.14.0" "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.25.1" - "@eslint/plugin-kit" "^0.2.8" + "@eslint/js" "9.27.0" + "@eslint/plugin-kit" "^0.3.1" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/retry" "^0.4.2" From 2ebaa1e145e72bf72e92789c7b8a6f08a310146d Mon Sep 17 00:00:00 2001 From: Juan Estrella Date: Mon, 19 May 2025 16:49:21 +0200 Subject: [PATCH 71/93] Create meeting_minutes.md add meeting minutes GitHub Issue template --- .github/ISSUE_TEMPLATE/meeting_minutes.md | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/meeting_minutes.md diff --git a/.github/ISSUE_TEMPLATE/meeting_minutes.md b/.github/ISSUE_TEMPLATE/meeting_minutes.md new file mode 100644 index 000000000..37692aaf0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/meeting_minutes.md @@ -0,0 +1,49 @@ +--- +name: "\U0001F91D Git Proxy Meeting Minutes" +about: To track Git Proxy meeting agenda and attendance +title: DD MMM YYYY - Git Proxy Meeting Minutes +labels: meeting +assignees: + +--- + + ## Date +YYYYMMDD - time + +## Meeting info +- [Meeting link](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595) + +- [Register for future meetings](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595&invite=true) + +## Untracked attendees +- Fullname, Affiliation, (optional) GitHub username +- ... + +## Meeting notices +- FINOS **Project leads** are responsible for observing the FINOS guidelines for [running project meetings](https://community.finos.org/docs/governance/meeting-procedures/). Project maintainers can find additional resources in the [FINOS Maintainers Cheatsheet](https://community.finos.org/docs/finos-maintainers-cheatsheet). + +- **All participants** in FINOS project meetings are subject to the [LF Antitrust Policy](https://www.linuxfoundation.org/antitrust-policy/), the [FINOS Community Code of Conduct](https://community.finos.org/docs/governance/code-of-conduct) and all other [FINOS policies](https://community.finos.org/docs/governance/#policies). + +- FINOS meetings involve participation by industry competitors, and it is the intention of FINOS and the Linux Foundation to conduct all of its activities in accordance with applicable antitrust and competition laws. It is therefore extremely important that attendees adhere to meeting agendas, and be aware of, and not participate in, any activities that are prohibited under applicable US state, federal or foreign antitrust and competition laws. Please contact legal@finos.org with any questions. + +- FINOS project meetings may be recorded for use solely by the FINOS team for administration purposes. In very limited instances, and with explicit approval, recordings may be made more widely available. + +## Agenda +- [ ] Convene & roll call (5mins) +- [ ] Display [FINOS Antitrust Policy summary slide](https://community.finos.org/Compliance-Slides/Antitrust-Compliance-Slide.pdf) +- [ ] Review Meeting Notices (see above) +- [ ] Approve past meeting minutes +- [ ] Agenda item 1 +- [ ] Agenda item 2 +- [ ] ... +- [ ] AOB, Q&A & Adjourn (5mins) + +## Decisions Made +- [ ] Decision 1 +- [ ] Decision 2 +- [ ] ... + +## Action Items +- [ ] Action 1 +- [ ] Action 2 +- [ ] ... From cc75f83019a15c1db24f3a0d79fd95e872d8d1fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 16:03:21 +0000 Subject: [PATCH 72/93] chore(deps): update github-actions - workflows - .github/workflows/dependency-review.yml --- .github/workflows/ci.yml | 4 ++-- .github/workflows/codeql.yml | 6 +++--- .github/workflows/dependency-review.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 972fd7e1d..19da6f459 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: npm run test-coverage-ci --workspaces --if-present - name: Upload test coverage report - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 with: files: ./coverage/lcov.info token: ${{ secrets.CODECOV_TOKEN }} @@ -77,7 +77,7 @@ jobs: path: build - name: Run cypress test - uses: cypress-io/github-action@0ee1130f05f69098ab5c560bd198fecf5a14d75b # v6.9.0 + uses: cypress-io/github-action@be1bab96b388bbd9ce3887e397d373c8557e15af # v6.9.2 with: start: npm start & wait-on: "http://localhost:3000" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 051a3e424..51577cfcf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -60,7 +60,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,7 +74,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 + uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -87,6 +87,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index effb06f1f..6508003ab 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Dependency Review - uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4 + uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4 with: comment-summary-in-pr: always fail-on-severity: high diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index e6320599b..20d892b7c 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -72,6 +72,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 + uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: sarif_file: results.sarif From 6b98af8ecbb87af150920bd723faf9c780ed96db Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Mon, 19 May 2025 17:16:22 +0100 Subject: [PATCH 73/93] Update .github/ISSUE_TEMPLATE/meeting_minutes.md --- .github/ISSUE_TEMPLATE/meeting_minutes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/meeting_minutes.md b/.github/ISSUE_TEMPLATE/meeting_minutes.md index 37692aaf0..39eeb8ed6 100644 --- a/.github/ISSUE_TEMPLATE/meeting_minutes.md +++ b/.github/ISSUE_TEMPLATE/meeting_minutes.md @@ -16,7 +16,7 @@ YYYYMMDD - time - [Register for future meetings](https://zoom-lfx.platform.linuxfoundation.org/meeting/95849833904?password=99413314-d03a-4b1c-b682-1ede2c399595&invite=true) ## Untracked attendees -- Fullname, Affiliation, (optional) GitHub username +- Full Name, Affiliation, (optional) GitHub username - ... ## Meeting notices From d8ae510821317558b2ba20da1fa659f94b5a0ac5 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Mon, 19 May 2025 17:16:28 +0100 Subject: [PATCH 74/93] Update .github/ISSUE_TEMPLATE/meeting_minutes.md --- .github/ISSUE_TEMPLATE/meeting_minutes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/meeting_minutes.md b/.github/ISSUE_TEMPLATE/meeting_minutes.md index 39eeb8ed6..ad291fee2 100644 --- a/.github/ISSUE_TEMPLATE/meeting_minutes.md +++ b/.github/ISSUE_TEMPLATE/meeting_minutes.md @@ -1,7 +1,7 @@ --- -name: "\U0001F91D Git Proxy Meeting Minutes" -about: To track Git Proxy meeting agenda and attendance -title: DD MMM YYYY - Git Proxy Meeting Minutes +name: "\U0001F91D GitProxy Meeting Minutes" +about: To track GitProxy meeting agenda and attendance +title: DD MMM YYYY - GitProxy Meeting Minutes labels: meeting assignees: From 203d4383dc9c6953676eb70aa7195e4a2e8ada94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Fri, 16 May 2025 13:01:37 +0200 Subject: [PATCH 75/93] feat: implements config loader to enable remote or external configs feat: converted to typescript fix: config loader clone command issue fix: adds input validation, uses array arguments, prevented shell spawn fix: adds failsafe checking for directory location and structure fix: env-paths change to v2.2.1 which support require and minor code fix fix: improves test coverage Adds additional tests for better cove fix: fixed creating cache directory --- .gitignore | 8 +- config.schema.json | 14 +- package.json | 1 + packages/git-proxy-cli/index.js | 31 +- proxy.config.json | 33 ++ src/config/ConfigLoader.ts | 419 ++++++++++++++++++++++ src/config/index.ts | 61 +++- src/proxy/chain.ts | 2 +- src/proxy/index.ts | 50 ++- src/service/index.js | 38 ++ test/ConfigLoader.test.js | 426 +++++++++++++++++++++++ test/chain.test.js | 86 +++-- website/docs/configuration/overview.mdx | 69 +++- website/docs/configuration/reference.mdx | 102 +++++- 14 files changed, 1288 insertions(+), 52 deletions(-) create mode 100644 src/config/ConfigLoader.ts create mode 100644 test/ConfigLoader.test.js diff --git a/.gitignore b/.gitignore index 1849589c4..747f84c76 100644 --- a/.gitignore +++ b/.gitignore @@ -263,4 +263,10 @@ yarn-error.log* # Docusaurus website website/build -website/.docusaurus \ No newline at end of file +website/.docusaurus + +# git-config-cache +.git-config-cache + +# Jetbrains IDE +.idea diff --git a/config.schema.json b/config.schema.json index c0ac89663..78cc005c8 100644 --- a/config.schema.json +++ b/config.schema.json @@ -28,7 +28,7 @@ "description": "API Rate limiting configuration.", "type": "object", "properties": { - "windowMs": { + "windowMs": { "type": "number", "description": "How long to remember requests for, in milliseconds (default 10 mins)." }, @@ -112,6 +112,18 @@ "cert": { "type": "string" } }, "required": ["enabled", "key", "cert"] + }, + "configurationSources": { + "enabled": { "type": "boolean" }, + "reloadIntervalSeconds": { "type": "number" }, + "merge": { "type": "boolean" }, + "sources": { + "type": "array", + "items": { + "type": "object", + "description": "Configuration source" + } + } } }, "definitions": { diff --git a/package.json b/package.json index 3ce14e6aa..4c62e9fa3 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "connect-mongo": "^5.1.0", "cors": "^2.8.5", "diff2html": "^3.4.33", + "env-paths": "^2.2.1", "express": "^4.18.2", "express-http-proxy": "^2.0.0", "express-rate-limit": "^7.1.5", diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.js index b0090a4bf..142a58a33 100755 --- a/packages/git-proxy-cli/index.js +++ b/packages/git-proxy-cli/index.js @@ -7,7 +7,8 @@ const util = require('util'); const GIT_PROXY_COOKIE_FILE = 'git-proxy-cookie'; // GitProxy UI HOST and PORT (configurable via environment variable) -const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 8080 } = process.env; +const { GIT_PROXY_UI_HOST: uiHost = 'http://localhost', GIT_PROXY_UI_PORT: uiPort = 8080 } = + process.env; const baseUrl = `${uiHost}:${uiPort}`; @@ -306,6 +307,29 @@ async function logout() { console.log('Logout: OK'); } +/** + * Reloads the GitProxy configuration without restarting the process + */ +async function reloadConfig() { + if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + console.error('Error: Reload config: Authentication required'); + process.exitCode = 1; + return; + } + + try { + const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8')); + + await axios.post(`${baseUrl}/api/v1/admin/reload-config`, {}, { headers: { Cookie: cookies } }); + + console.log('Configuration reloaded successfully'); + } catch (error) { + const errorMessage = `Error: Reload config: '${error.message}'`; + process.exitCode = 2; + console.error(errorMessage); + } +} + // Parsing command line arguments yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused-expressions .command({ @@ -436,6 +460,11 @@ yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused rejectGitPush(argv.id); }, }) + .command({ + command: 'reload-config', + description: 'Reload GitProxy configuration without restarting', + action: reloadConfig, + }) .demandCommand(1, 'You need at least one command before moving on') .strict() .help().argv; diff --git a/proxy.config.json b/proxy.config.json index 580982cd4..ed3238354 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -96,6 +96,39 @@ } ] }, + "configurationSources": { + "enabled": false, + "reloadIntervalSeconds": 60, + "merge": false, + "sources": [ + { + "type": "file", + "enabled": false, + "path": "./external-config.json" + }, + { + "type": "http", + "enabled": false, + "url": "http://config-service/git-proxy-config", + "headers": {}, + "auth": { + "type": "bearer", + "token": "" + } + }, + { + "type": "git", + "enabled": false, + "repository": "https://git-server.com/project/git-proxy-config", + "branch": "main", + "path": "git-proxy/config.json", + "auth": { + "type": "ssh", + "privateKeyPath": "/path/to/.ssh/id_rsa" + } + } + ] + }, "domains": {}, "privateOrganizations": [], "urlShortener": "", diff --git a/src/config/ConfigLoader.ts b/src/config/ConfigLoader.ts new file mode 100644 index 000000000..80429e382 --- /dev/null +++ b/src/config/ConfigLoader.ts @@ -0,0 +1,419 @@ +import fs from 'fs'; +import path from 'path'; +import axios from 'axios'; +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import EventEmitter from 'events'; +import envPaths from 'env-paths'; + +const execFileAsync = promisify(execFile); + +interface GitAuth { + type: 'ssh'; + privateKeyPath: string; +} + +interface HttpAuth { + type: 'bearer'; + token: string; +} + +interface BaseSource { + type: 'file' | 'http' | 'git'; + enabled: boolean; +} + +interface FileSource extends BaseSource { + type: 'file'; + path: string; +} + +interface HttpSource extends BaseSource { + type: 'http'; + url: string; + headers?: Record; + auth?: HttpAuth; +} + +interface GitSource extends BaseSource { + type: 'git'; + repository: string; + branch?: string; + path: string; + auth?: GitAuth; +} + +type ConfigurationSource = FileSource | HttpSource | GitSource; + +export interface ConfigurationSources { + enabled: boolean; + sources: ConfigurationSource[]; + reloadIntervalSeconds: number; + merge?: boolean; +} + +export interface Configuration { + configurationSources: ConfigurationSources; + [key: string]: any; +} + +// Add path validation helper +function isValidPath(filePath: string): boolean { + if (!filePath || typeof filePath !== 'string') return false; + + // Check for null bytes and other control characters + if (/[\0]/.test(filePath)) return false; + + try { + path.resolve(filePath); + return true; + } catch (error) { + return false; + } +} + +// Add URL validation helper +function isValidGitUrl(url: string): boolean { + // Allow git://, https://, or ssh:// URLs + // Also allow scp-style URLs (user@host:path) + const validUrlPattern = + /^(git:\/\/|https:\/\/|ssh:\/\/|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}:)/; + return typeof url === 'string' && validUrlPattern.test(url); +} + +// Add branch name validation helper +function isValidBranchName(branch: string): boolean { + if (typeof branch !== 'string') return false; + + // Check for consecutive dots + if (branch.includes('..')) return false; + + // Check other branch name rules + // Branch names can contain alphanumeric, -, _, /, and . + // Cannot start with - or . + // Cannot contain consecutive dots + // Cannot contain control characters or spaces + const validBranchPattern = /^[a-zA-Z0-9][a-zA-Z0-9_/.-]*$/; + return validBranchPattern.test(branch); +} + +export class ConfigLoader extends EventEmitter { + private config: Configuration; + private reloadTimer: NodeJS.Timeout | null; + private isReloading: boolean; + private cacheDir: string | null; + + constructor(initialConfig: Configuration) { + super(); + this.config = initialConfig; + this.reloadTimer = null; + this.isReloading = false; + this.cacheDir = null; + } + + async initialize(): Promise { + // Get cache directory path + const paths = envPaths('git-proxy'); + this.cacheDir = paths.cache; + + // Create cache directory if it doesn't exist + if (!fs.existsSync(this.cacheDir)) { + try { + fs.mkdirSync(this.cacheDir, { recursive: true }); + console.log(`Created cache directory at ${this.cacheDir}`); + return true; + } catch (err) { + console.error('Failed to create cache directory:', err); + return false; + } + } + console.log(`Using cache directory at ${this.cacheDir}`); + return true; + } + + async start(): Promise { + const { configurationSources } = this.config; + if (!configurationSources?.enabled) { + console.log('Configuration sources are disabled'); + return; + } + + console.log('Configuration sources are enabled'); + console.log( + `Sources: ${JSON.stringify(configurationSources.sources.filter((s: ConfigurationSource) => s.enabled).map((s: ConfigurationSource) => s.type))}`, + ); + + // Clear any existing interval before starting a new one + if (this.reloadTimer) { + clearInterval(this.reloadTimer); + this.reloadTimer = null; + } + + // Start periodic reload if interval is set + if (configurationSources.reloadIntervalSeconds > 0) { + console.log( + `Setting reload interval to ${configurationSources.reloadIntervalSeconds} seconds`, + ); + this.reloadTimer = setInterval( + () => this.reloadConfiguration(), + configurationSources.reloadIntervalSeconds * 1000, + ); + } + + // Do initial load + await this.reloadConfiguration(); + } + + stop(): void { + if (this.reloadTimer) { + clearInterval(this.reloadTimer); + this.reloadTimer = null; + } + } + + async reloadConfiguration(): Promise { + if (this.isReloading) { + console.log('Configuration reload already in progress, skipping'); + return; + } + this.isReloading = true; + console.log('Starting configuration reload'); + + try { + const { configurationSources } = this.config; + if (!configurationSources?.enabled) { + console.log('Configuration sources are disabled, skipping reload'); + return; + } + + const enabledSources = configurationSources.sources.filter( + (source: ConfigurationSource) => source.enabled, + ); + console.log(`Found ${enabledSources.length} enabled configuration sources`); + + const configs = await Promise.all( + enabledSources.map(async (source: ConfigurationSource) => { + try { + console.log(`Loading configuration from ${source.type} source`); + return await this.loadFromSource(source); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`Error loading from ${source.type} source:`, error.message); + } + return null; + } + }), + ); + + // Filter out null results from failed loads + const validConfigs = configs.filter((config): config is Configuration => config !== null); + + if (validConfigs.length === 0) { + console.log('No valid configurations loaded from any source'); + return; + } + + // Use merge strategy based on configuration + const shouldMerge = configurationSources.merge ?? true; // Default to true for backward compatibility + console.log(`Using ${shouldMerge ? 'merge' : 'override'} strategy for configuration`); + + const newConfig = shouldMerge + ? validConfigs.reduce( + (acc, curr) => { + return this.deepMerge(acc, curr) as Configuration; + }, + { ...this.config }, + ) + : { ...this.config, ...validConfigs[validConfigs.length - 1] }; // Use last config for override + + // Emit change event if config changed + if (JSON.stringify(newConfig) !== JSON.stringify(this.config)) { + console.log('Configuration has changed, updating and emitting change event'); + this.config = newConfig; + this.emit('configurationChanged', this.config); + } else { + console.log('Configuration has not changed, no update needed'); + } + } catch (error: unknown) { + console.error('Error reloading configuration:', error); + this.emit('configurationError', error); + } finally { + this.isReloading = false; + } + } + + async loadFromSource(source: ConfigurationSource): Promise { + let exhaustiveCheck: never; + switch (source.type) { + case 'file': + return this.loadFromFile(source as FileSource); + case 'http': + return this.loadFromHttp(source as HttpSource); + case 'git': + return this.loadFromGit(source as GitSource); + default: + exhaustiveCheck = source; + throw new Error(`Unsupported configuration source type: ${exhaustiveCheck}`); + } + } + + async loadFromFile(source: FileSource): Promise { + const configPath = path.resolve(process.cwd(), source.path); + if (!isValidPath(configPath)) { + throw new Error('Invalid configuration file path'); + } + console.log(`Loading configuration from file: ${configPath}`); + const content = await fs.promises.readFile(configPath, 'utf8'); + return JSON.parse(content); + } + + async loadFromHttp(source: HttpSource): Promise { + console.log(`Loading configuration from HTTP: ${source.url}`); + const headers = { + ...source.headers, + ...(source.auth?.type === 'bearer' ? { Authorization: `Bearer ${source.auth.token}` } : {}), + }; + + const response = await axios.get(source.url, { headers }); + return response.data; + } + + async loadFromGit(source: GitSource): Promise { + console.log(`Loading configuration from Git: ${source.repository}`); + + // Validate inputs + if (!source.repository || !isValidGitUrl(source.repository)) { + throw new Error('Invalid repository URL format'); + } + if (source.branch && !isValidBranchName(source.branch)) { + throw new Error('Invalid branch name format'); + } + + // Use OS-specific cache directory + const paths = envPaths('git-proxy', { suffix: '' }); + const tempDir = path.join(paths.cache, 'git-config-cache'); + + if (!isValidPath(tempDir)) { + throw new Error('Invalid temporary directory path'); + } + + console.log(`Creating git cache directory at ${tempDir}`); + await fs.promises.mkdir(tempDir, { recursive: true }); + + // Create a safe directory name from the repository URL + const repoDirName = Buffer.from(source.repository) + .toString('base64') + .replace(/[^a-zA-Z0-9]/g, '_'); + const repoDir = path.join(tempDir, repoDirName); + + if (!isValidPath(repoDir)) { + throw new Error('Invalid repository directory path'); + } + + console.log(`Using repository directory: ${repoDir}`); + + // Clone or pull repository + if (!fs.existsSync(repoDir)) { + console.log(`Cloning repository ${source.repository} to ${repoDir}`); + const execOptions = { + cwd: process.cwd(), + env: { + ...process.env, + ...(source.auth?.type === 'ssh' + ? { + GIT_SSH_COMMAND: `ssh -i ${source.auth.privateKeyPath}`, + } + : {}), + }, + }; + + try { + await execFileAsync('git', ['clone', source.repository, repoDir], execOptions); + console.log('Repository cloned successfully'); + } catch (error: unknown) { + if (error instanceof Error) { + console.error('Failed to clone repository:', error.message); + throw new Error(`Failed to clone repository: ${error.message}`); + } + throw error; + } + } else { + console.log(`Pulling latest changes from ${source.repository}`); + try { + await execFileAsync('git', ['pull'], { cwd: repoDir }); + console.log('Repository pulled successfully'); + } catch (error: unknown) { + if (error instanceof Error) { + console.error('Failed to pull repository:', error.message); + throw new Error(`Failed to pull repository: ${error.message}`); + } + throw error; + } + } + + // Checkout specific branch if specified + if (source.branch) { + console.log(`Checking out branch: ${source.branch}`); + try { + await execFileAsync('git', ['checkout', source.branch], { cwd: repoDir }); + console.log(`Branch ${source.branch} checked out successfully`); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`Failed to checkout branch ${source.branch}:`, error.message); + throw new Error(`Failed to checkout branch ${source.branch}: ${error.message}`); + } + throw error; + } + } + + // Read and parse config file + const configPath = path.join(repoDir, source.path); + if (!isValidPath(configPath)) { + throw new Error('Invalid configuration file path in repository'); + } + + console.log(`Reading configuration file: ${configPath}`); + if (!fs.existsSync(configPath)) { + throw new Error(`Configuration file not found at ${configPath}`); + } + + try { + const content = await fs.promises.readFile(configPath, 'utf8'); + const config = JSON.parse(content); + console.log('Configuration loaded successfully from Git'); + return config; + } catch (error: unknown) { + if (error instanceof Error) { + console.error('Failed to read or parse configuration file:', error.message); + throw new Error(`Failed to read or parse configuration file: ${error.message}`); + } + throw error; + } + } + + deepMerge(target: Record, source: Record): Record { + const output = { ...target }; + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach((key) => { + if (isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + output[key] = this.deepMerge(target[key], source[key]); + } + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + } + return output; + } +} + +// Helper function to check if a value is an object +function isObject(item: unknown): item is Record { + return item !== null && typeof item === 'object' && !Array.isArray(item); +} + +export default ConfigLoader; +export { isValidGitUrl, isValidPath, isValidBranchName }; diff --git a/src/config/index.ts b/src/config/index.ts index d041344a4..b92134d75 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,7 +1,8 @@ import { existsSync, readFileSync } from 'fs'; import defaultSettings from '../../proxy.config.json'; -import { configFile } from './file'; +import { configFile, validate } from './file'; +import { ConfigLoader, Configuration } from './ConfigLoader'; import { Authentication, AuthorisedRepo, @@ -38,6 +39,12 @@ let _tlsEnabled = defaultSettings.tls.enabled; let _tlsKeyPemPath = defaultSettings.tls.key; let _tlsCertPemPath = defaultSettings.tls.cert; +// Initialize configuration with defaults and user settings +let _config = { ...defaultSettings, ...(_userSettings || {}) } as Configuration; + +// Create config loader instance +const configLoader = new ConfigLoader(_config); + // Get configured proxy URL export const getProxyUrl = () => { if (_userSettings !== null && _userSettings.proxyUrl) { @@ -228,3 +235,55 @@ export const getRateLimit = () => { } return _rateLimit; }; + +// Function to handle configuration updates +const handleConfigUpdate = async (newConfig: typeof _config) => { + console.log('Configuration updated from external source'); + try { + // 1. Get proxy module dynamically to avoid circular dependency + const proxy = require('../proxy'); + + // 2. Stop existing services + await proxy.stop(); + + // 3. Update config + _config = newConfig; + + // 4. Validate new configuration + validate(); + + // 5. Restart services with new config + await proxy.start(); + + console.log('Services restarted with new configuration'); + } catch (error) { + console.error('Failed to apply new configuration:', error); + // Attempt to restart with previous config + try { + const proxy = require('../proxy'); + await proxy.start(); + } catch (startError) { + console.error('Failed to restart services:', startError); + } + } +}; + +// Handle configuration updates +configLoader.on('configurationChanged', handleConfigUpdate); + +configLoader.on('configurationError', (error: Error) => { + console.error('Error loading external configuration:', error); +}); + +// Start the config loader if external sources are enabled +configLoader.start().catch((error: Error) => { + console.error('Failed to start configuration loader:', error); +}); + +// Force reload of configuration +const reloadConfiguration = async () => { + await configLoader.reloadConfiguration(); +}; + +// Export reloadConfiguration +export { reloadConfiguration }; diff --git a/src/proxy/chain.ts b/src/proxy/chain.ts index 55e271a3e..8bc5e3120 100644 --- a/src/proxy/chain.ts +++ b/src/proxy/chain.ts @@ -61,7 +61,7 @@ export const executeChain = async (req: any, res: any): Promise => { */ let chainPluginLoader: PluginLoader; -const getChain = async ( +export const getChain = async ( action: Action, ): Promise<((req: any, action: Action) => Promise)[]> => { if (chainPluginLoader === undefined) { diff --git a/src/proxy/index.ts b/src/proxy/index.ts index 0a49d0a6f..4cfcda986 100644 --- a/src/proxy/index.ts +++ b/src/proxy/index.ts @@ -1,4 +1,4 @@ -import express from 'express'; +import express, { Application } from 'express'; import bodyParser from 'body-parser'; import http from 'http'; import https from 'https'; @@ -19,7 +19,15 @@ import { Repo } from '../db/types'; const { GIT_PROXY_SERVER_PORT: proxyHttpPort, GIT_PROXY_HTTPS_SERVER_PORT: proxyHttpsPort } = require('../config/env').serverConfig; -const options = { +interface ServerOptions { + inflate: boolean; + limit: string; + type: string; + key: Buffer | undefined; + cert: Buffer | undefined; +} + +const options: ServerOptions = { inflate: true, limit: '100000kb', type: '*/*', @@ -47,7 +55,7 @@ export const proxyPreparations = async () => { }; // just keep this async incase it needs async stuff in the future -export const createApp = async () => { +const createApp = async (): Promise => { const app = express(); // Setup the proxy middleware app.use(bodyParser.raw(options)); @@ -55,23 +63,53 @@ export const createApp = async () => { return app; }; -export const start = async () => { +let httpServer: http.Server | null = null; +let httpsServer: https.Server | null = null; + +const start = async (): Promise => { const app = await createApp(); await proxyPreparations(); - http.createServer(options as any, app).listen(proxyHttpPort, () => { + httpServer = http.createServer(options as any, app).listen(proxyHttpPort, () => { console.log(`HTTP Proxy Listening on ${proxyHttpPort}`); }); // Start HTTPS server only if TLS is enabled if (getTLSEnabled()) { - https.createServer(options, app).listen(proxyHttpsPort, () => { + httpsServer = https.createServer(options, app).listen(proxyHttpsPort, () => { console.log(`HTTPS Proxy Listening on ${proxyHttpsPort}`); }); } return app; }; +const stop = (): Promise => { + return new Promise((resolve, reject) => { + try { + // Close HTTP server if it exists + if (httpServer) { + httpServer.close(() => { + console.log('HTTP server closed'); + httpServer = null; + }); + } + + // Close HTTPS server if it exists + if (httpsServer) { + httpsServer.close(() => { + console.log('HTTPS server closed'); + httpsServer = null; + }); + } + + resolve(); + } catch (error) { + reject(error); + } + }); +}; + export default { proxyPreparations, createApp, start, + stop, }; diff --git a/src/service/index.js b/src/service/index.js index 180800d34..02e416aa0 100644 --- a/src/service/index.js +++ b/src/service/index.js @@ -8,6 +8,8 @@ const config = require('../config'); const db = require('../db'); const rateLimit = require('express-rate-limit'); const lusca = require('lusca'); +const configLoader = require('../config/ConfigLoader'); +const proxy = require('../proxy'); const limiter = rateLimit(config.getRateLimit()); @@ -29,6 +31,42 @@ const createApp = async () => { app.use(cors(corsOptions)); app.set('trust proxy', 1); app.use(limiter); + + // Add new admin-only endpoint to reload config + app.post('/api/v1/admin/reload-config', async (req, res) => { + if (!req.isAuthenticated() || !req.user.admin) { + return res.status(403).json({ error: 'Unauthorized' }); + } + + try { + // 1. Reload configuration + await configLoader.loadConfiguration(); + + // 2. Stop existing services + await proxy.stop(); + + // 3. Apply new configuration + config.validate(); + + // 4. Restart services with new config + await proxy.start(); + + console.log('Configuration reloaded and services restarted successfully'); + res.json({ status: 'success', message: 'Configuration reloaded and services restarted' }); + } catch (error) { + console.error('Failed to reload configuration and restart services:', error); + + // Attempt to restart with existing config if reload fails + try { + await proxy.start(); + } catch (startError) { + console.error('Failed to restart services:', startError); + } + + res.status(500).json({ error: 'Failed to reload configuration' }); + } + }); + app.use( session({ store: config.getDatabase().type === 'mongo' ? db.getSessionStore(session) : null, diff --git a/test/ConfigLoader.test.js b/test/ConfigLoader.test.js new file mode 100644 index 000000000..59d63458a --- /dev/null +++ b/test/ConfigLoader.test.js @@ -0,0 +1,426 @@ +import fs from 'fs'; +import path from 'path'; +import { expect } from 'chai'; +import { ConfigLoader } from '../src/config/ConfigLoader'; +import { isValidGitUrl, isValidPath, isValidBranchName } from '../src/config/ConfigLoader'; +import sinon from 'sinon'; +import axios from 'axios'; + +describe('ConfigLoader', () => { + let configLoader; + let tempDir; + let tempConfigFile; + + beforeEach(() => { + // Create temp directory for test files + tempDir = fs.mkdtempSync('gitproxy-configloader-test-'); + tempConfigFile = path.join(tempDir, 'test-config.json'); + }); + + afterEach(() => { + // Clean up temp files + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true }); + } + sinon.restore(); + }); + + describe('loadFromFile', () => { + it('should load configuration from file', async () => { + const testConfig = { + proxyUrl: 'https://test.com', + cookieSecret: 'test-secret', + }; + fs.writeFileSync(tempConfigFile, JSON.stringify(testConfig)); + + configLoader = new ConfigLoader({}); + const result = await configLoader.loadFromFile({ + type: 'file', + enabled: true, + path: tempConfigFile, + }); + + expect(result).to.deep.equal(testConfig); + }); + }); + + describe('loadFromHttp', () => { + it('should load configuration from HTTP endpoint', async () => { + const testConfig = { + proxyUrl: 'https://test.com', + cookieSecret: 'test-secret', + }; + + sinon.stub(axios, 'get').resolves({ data: testConfig }); + + configLoader = new ConfigLoader({}); + const result = await configLoader.loadFromHttp({ + type: 'http', + enabled: true, + url: 'http://config-service/config', + headers: {}, + }); + + expect(result).to.deep.equal(testConfig); + }); + + it('should include bearer token if provided', async () => { + const axiosStub = sinon.stub(axios, 'get').resolves({ data: {} }); + + configLoader = new ConfigLoader({}); + await configLoader.loadFromHttp({ + type: 'http', + enabled: true, + url: 'http://config-service/config', + auth: { + type: 'bearer', + token: 'test-token', + }, + }); + + expect( + axiosStub.calledWith('http://config-service/config', { + headers: { Authorization: 'Bearer test-token' }, + }), + ).to.be.true; + }); + }); + + describe('reloadConfiguration', () => { + it('should emit configurationChanged event when config changes', async () => { + const initialConfig = { + configurationSources: { + enabled: true, + sources: [ + { + type: 'file', + enabled: true, + path: tempConfigFile, + }, + ], + reloadIntervalSeconds: 0, + }, + }; + + const newConfig = { + proxyUrl: 'https://new-test.com', + }; + + fs.writeFileSync(tempConfigFile, JSON.stringify(newConfig)); + + configLoader = new ConfigLoader(initialConfig); + const spy = sinon.spy(); + configLoader.on('configurationChanged', spy); + + await configLoader.reloadConfiguration(); + + expect(spy.calledOnce).to.be.true; + expect(spy.firstCall.args[0]).to.deep.include(newConfig); + }); + + it('should not emit event if config has not changed', async () => { + const testConfig = { + proxyUrl: 'https://test.com', + }; + + const config = { + configurationSources: { + enabled: true, + sources: [ + { + type: 'file', + enabled: true, + path: tempConfigFile, + }, + ], + reloadIntervalSeconds: 0, + }, + }; + + fs.writeFileSync(tempConfigFile, JSON.stringify(testConfig)); + + configLoader = new ConfigLoader(config); + const spy = sinon.spy(); + configLoader.on('configurationChanged', spy); + + await configLoader.reloadConfiguration(); // First reload should emit + await configLoader.reloadConfiguration(); // Second reload should not emit since config hasn't changed + + expect(spy.calledOnce).to.be.true; // Should only emit once + }); + }); + + describe('initialize', () => { + it('should initialize cache directory using env-paths', async () => { + const configLoader = new ConfigLoader({}); + await configLoader.initialize(); + + // Check that cacheDir is set and is a string + expect(configLoader.cacheDir).to.be.a('string'); + + // Check that it contains 'git-proxy' in the path + expect(configLoader.cacheDir).to.include('git-proxy'); + + // On macOS, it should be in the Library/Caches directory + // On Linux, it should be in the ~/.cache directory + // On Windows, it should be in the AppData/Local directory + if (process.platform === 'darwin') { + expect(configLoader.cacheDir).to.include('Library/Caches'); + } else if (process.platform === 'linux') { + expect(configLoader.cacheDir).to.include('.cache'); + } else if (process.platform === 'win32') { + expect(configLoader.cacheDir).to.include('AppData/Local'); + } + }); + + it('should create cache directory if it does not exist', async () => { + const configLoader = new ConfigLoader({}); + await configLoader.initialize(); + + // Check if directory exists + expect(fs.existsSync(configLoader.cacheDir)).to.be.true; + }); + }); + + describe('loadRemoteConfig', () => { + let configLoader; + beforeEach(async () => { + const configFilePath = path.join(__dirname, '..', 'proxy.config.json'); + const config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8')); + + config.configurationSources.enabled = true; + configLoader = new ConfigLoader(config); + await configLoader.initialize(); + }); + + it('should load configuration from git repository', async function () { + // eslint-disable-next-line no-invalid-this + this.timeout(10000); + + const source = { + type: 'git', + repository: 'https://github.com/finos/git-proxy.git', + path: 'proxy.config.json', + branch: 'main', + enabled: true, + }; + + const config = await configLoader.loadFromGit(source); + + // Verify the loaded config has expected structure + expect(config).to.be.an('object'); + expect(config).to.have.property('proxyUrl'); + expect(config).to.have.property('cookieSecret'); + }); + + it('should throw error for invalid configuration file path', async function () { + const source = { + type: 'git', + repository: 'https://github.com/finos/git-proxy.git', + path: '\0', // Invalid path + branch: 'main', + enabled: true, + }; + + try { + await configLoader.loadFromGit(source); + throw new Error('Expected error was not thrown'); + } catch (error) { + expect(error.message).to.equal('Invalid configuration file path in repository'); + } + }); + + it('should load configuration from http', async function () { + // eslint-disable-next-line no-invalid-this + this.timeout(10000); + + const source = { + type: 'http', + url: 'https://raw.githubusercontent.com/finos/git-proxy/refs/heads/main/proxy.config.json', + enabled: true, + }; + + const config = await configLoader.loadFromHttp(source); + + // Verify the loaded config has expected structure + expect(config).to.be.an('object'); + expect(config).to.have.property('proxyUrl'); + expect(config).to.have.property('cookieSecret'); + }); + }); + + describe('deepMerge', () => { + let configLoader; + + beforeEach(() => { + configLoader = new ConfigLoader({}); + }); + + it('should merge simple objects', () => { + const target = { a: 1, b: 2 }; + const source = { b: 3, c: 4 }; + + const result = configLoader.deepMerge(target, source); + + expect(result).to.deep.equal({ a: 1, b: 3, c: 4 }); + }); + + it('should merge nested objects', () => { + const target = { + a: 1, + b: { x: 1, y: 2 }, + c: { z: 3 }, + }; + const source = { + b: { y: 4, w: 5 }, + c: { z: 6 }, + }; + + const result = configLoader.deepMerge(target, source); + + expect(result).to.deep.equal({ + a: 1, + b: { x: 1, y: 4, w: 5 }, + c: { z: 6 }, + }); + }); + + it('should handle arrays by replacing them', () => { + const target = { + a: [1, 2, 3], + b: { items: [4, 5] }, + }; + const source = { + a: [7, 8], + b: { items: [9] }, + }; + + const result = configLoader.deepMerge(target, source); + + expect(result).to.deep.equal({ + a: [7, 8], + b: { items: [9] }, + }); + }); + + it('should handle null and undefined values', () => { + const target = { + a: 1, + b: null, + c: undefined, + }; + const source = { + a: null, + b: 2, + c: 3, + }; + + const result = configLoader.deepMerge(target, source); + + expect(result).to.deep.equal({ + a: null, + b: 2, + c: 3, + }); + }); + + it('should handle empty objects', () => { + const target = {}; + const source = { a: 1, b: { c: 2 } }; + + const result = configLoader.deepMerge(target, source); + + expect(result).to.deep.equal({ a: 1, b: { c: 2 } }); + }); + + it('should not modify the original objects', () => { + const target = { a: 1, b: { c: 2 } }; + const source = { b: { c: 3 } }; + const originalTarget = { ...target }; + const originalSource = { ...source }; + + configLoader.deepMerge(target, source); + + expect(target).to.deep.equal(originalTarget); + expect(source).to.deep.equal(originalSource); + }); + }); +}); + +describe('Validation Helpers', () => { + describe('isValidGitUrl', () => { + it('should validate git URLs correctly', () => { + // Valid URLs + expect(isValidGitUrl('git://github.com/user/repo.git')).to.be.true; + expect(isValidGitUrl('https://github.com/user/repo.git')).to.be.true; + expect(isValidGitUrl('ssh://git@github.com/user/repo.git')).to.be.true; + expect(isValidGitUrl('user@github.com:user/repo.git')).to.be.true; + + // Invalid URLs + expect(isValidGitUrl('not-a-git-url')).to.be.false; + expect(isValidGitUrl('http://github.com/user/repo')).to.be.false; + expect(isValidGitUrl('')).to.be.false; + expect(isValidGitUrl(null)).to.be.false; + expect(isValidGitUrl(undefined)).to.be.false; + expect(isValidGitUrl(123)).to.be.false; + }); + }); + + describe('isValidPath', () => { + it('should validate file paths correctly', () => { + const cwd = process.cwd(); + + // Valid paths + expect(isValidPath(path.join(cwd, 'config.json'))).to.be.true; + expect(isValidPath(path.join(cwd, 'subfolder/config.json'))).to.be.true; + expect(isValidPath('/etc/passwd')).to.be.true; + expect(isValidPath('../config.json')).to.be.true; + + // Invalid paths + expect(isValidPath('')).to.be.false; + expect(isValidPath(null)).to.be.false; + expect(isValidPath(undefined)).to.be.false; + + // Additional edge cases + expect(isValidPath({})).to.be.false; + expect(isValidPath([])).to.be.false; + expect(isValidPath(123)).to.be.false; + expect(isValidPath(true)).to.be.false; + expect(isValidPath('\0invalid')).to.be.false; + expect(isValidPath('\u0000')).to.be.false; + }); + + it('should handle path resolution errors', () => { + // Mock path.resolve to throw an error + const originalResolve = path.resolve; + path.resolve = () => { + throw new Error('Mock path resolution error'); + }; + + expect(isValidPath('some/path')).to.be.false; + + // Restore original path.resolve + path.resolve = originalResolve; + }); + }); + + describe('isValidBranchName', () => { + it('should validate git branch names correctly', () => { + // Valid branch names + expect(isValidBranchName('main')).to.be.true; + expect(isValidBranchName('feature/new-feature')).to.be.true; + expect(isValidBranchName('release-1.0')).to.be.true; + expect(isValidBranchName('fix_123')).to.be.true; + expect(isValidBranchName('user/feature/branch')).to.be.true; + + // Invalid branch names + expect(isValidBranchName('.invalid')).to.be.false; + expect(isValidBranchName('-invalid')).to.be.false; + expect(isValidBranchName('branch with spaces')).to.be.false; + expect(isValidBranchName('')).to.be.false; + expect(isValidBranchName(null)).to.be.false; + expect(isValidBranchName(undefined)).to.be.false; + expect(isValidBranchName('branch..name')).to.be.false; + }); + }); +}); diff --git a/test/chain.test.js b/test/chain.test.js index d2c14620b..d5c070eb2 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -15,67 +15,79 @@ const mockLoader = { ], }; -const mockPushProcessors = { - parsePush: sinon.stub(), - audit: sinon.stub(), - checkRepoInAuthorisedList: sinon.stub(), - checkCommitMessages: sinon.stub(), - checkAuthorEmails: sinon.stub(), - checkUserPushPermission: sinon.stub(), - checkIfWaitingAuth: sinon.stub(), - pullRemote: sinon.stub(), - writePack: sinon.stub(), - preReceive: sinon.stub(), - getDiff: sinon.stub(), - gitleaks: sinon.stub(), - clearBareClone: sinon.stub(), - scanDiff: sinon.stub(), - blockForAuth: sinon.stub(), +const initMockPushProcessors = () => { + const mockPushProcessors = { + parsePush: sinon.stub(), + audit: sinon.stub(), + checkRepoInAuthorisedList: sinon.stub(), + checkCommitMessages: sinon.stub(), + checkAuthorEmails: sinon.stub(), + checkUserPushPermission: sinon.stub(), + checkIfWaitingAuth: sinon.stub(), + pullRemote: sinon.stub(), + writePack: sinon.stub(), + preReceive: sinon.stub(), + getDiff: sinon.stub(), + clearBareClone: sinon.stub(), + scanDiff: sinon.stub(), + blockForAuth: sinon.stub(), + }; + mockPushProcessors.parsePush.displayName = 'parsePush'; + mockPushProcessors.audit.displayName = 'audit'; + mockPushProcessors.checkRepoInAuthorisedList.displayName = 'checkRepoInAuthorisedList'; + mockPushProcessors.checkCommitMessages.displayName = 'checkCommitMessages'; + mockPushProcessors.checkAuthorEmails.displayName = 'checkAuthorEmails'; + mockPushProcessors.checkUserPushPermission.displayName = 'checkUserPushPermission'; + mockPushProcessors.checkIfWaitingAuth.displayName = 'checkIfWaitingAuth'; + mockPushProcessors.pullRemote.displayName = 'pullRemote'; + mockPushProcessors.writePack.displayName = 'writePack'; + mockPushProcessors.preReceive.displayName = 'preReceive'; + mockPushProcessors.getDiff.displayName = 'getDiff'; + mockPushProcessors.clearBareClone.displayName = 'clearBareClone'; + mockPushProcessors.scanDiff.displayName = 'scanDiff'; + mockPushProcessors.blockForAuth.displayName = 'blockForAuth'; + return mockPushProcessors; }; -mockPushProcessors.parsePush.displayName = 'parsePush'; -mockPushProcessors.audit.displayName = 'audit'; -mockPushProcessors.checkRepoInAuthorisedList.displayName = 'checkRepoInAuthorisedList'; -mockPushProcessors.checkCommitMessages.displayName = 'checkCommitMessages'; -mockPushProcessors.checkAuthorEmails.displayName = 'checkAuthorEmails'; -mockPushProcessors.checkUserPushPermission.displayName = 'checkUserPushPermission'; -mockPushProcessors.checkIfWaitingAuth.displayName = 'checkIfWaitingAuth'; -mockPushProcessors.pullRemote.displayName = 'pullRemote'; -mockPushProcessors.writePack.displayName = 'writePack'; -mockPushProcessors.preReceive.displayName = 'preReceive'; -mockPushProcessors.getDiff.displayName = 'getDiff'; -mockPushProcessors.gitleaks.displayName = 'gitleaks'; -mockPushProcessors.clearBareClone.displayName = 'clearBareClone'; -mockPushProcessors.scanDiff.displayName = 'scanDiff'; -mockPushProcessors.blockForAuth.displayName = 'blockForAuth'; const mockPreProcessors = { parseAction: sinon.stub(), }; +const clearCache = (sandbox) => { + delete require.cache[require.resolve('../src/proxy/processors')]; + delete require.cache[require.resolve('../src/proxy/chain')]; + sandbox.reset(); +}; + describe('proxy chain', function () { let processors; let chain; + let mockPushProcessors; + let sandboxSinon; beforeEach(async () => { + // Create a new sandbox for each test + sandboxSinon = sinon.createSandbox(); + // Initialize the mock push processors + mockPushProcessors = initMockPushProcessors(); + // Re-import the processors module after clearing the cache processors = await import('../src/proxy/processors'); // Mock the processors module - sinon.stub(processors, 'pre').value(mockPreProcessors); + sandboxSinon.stub(processors, 'pre').value(mockPreProcessors); - sinon.stub(processors, 'push').value(mockPushProcessors); + sandboxSinon.stub(processors, 'push').value(mockPushProcessors); // Re-import the chain module after stubbing processors - chain = (await import('../src/proxy/chain')).default; + chain = require('../src/proxy/chain').default; chain.chainPluginLoader = new PluginLoader([]); }); afterEach(() => { // Clear the module from the cache after each test - delete require.cache[require.resolve('../src/proxy/processors')]; - delete require.cache[require.resolve('../src/proxy/chain')]; - sinon.reset(); + clearCache(sandboxSinon); }); it('getChain should set pluginLoaded if loader is undefined', async function () { diff --git a/website/docs/configuration/overview.mdx b/website/docs/configuration/overview.mdx index 5493d54f6..274de5443 100644 --- a/website/docs/configuration/overview.mdx +++ b/website/docs/configuration/overview.mdx @@ -7,6 +7,7 @@ description: How to customise push protections and policies On installation, GitProxy ships with an [out-of-the-box configuration](https://github.com/finos/git-proxy/blob/main/proxy.config.json). This is fine for demonstration purposes but is likely not what you want to deploy into your environment. + ### Customise configuration To customise your GitProxy configuration, create a `proxy.config.json` in your current @@ -44,8 +45,9 @@ npx -- @finos/git-proxy --config ./config.json ``` ### Set ports with ENV variables + By default, GitProxy uses port 8000 to expose the Git Server and 8080 for the frontend application. -The ports can be changed by setting the `GIT_PROXY_SERVER_PORT`, `GIT_PROXY_HTTPS_SERVER_PORT` (optional) and `GIT_PROXY_UI_PORT` +The ports can be changed by setting the `GIT_PROXY_SERVER_PORT`, `GIT_PROXY_HTTPS_SERVER_PORT` (optional) and `GIT_PROXY_UI_PORT` environment variables: ``` @@ -54,10 +56,10 @@ export GIT_PROXY_SERVER_PORT="9090" export GIT_PROXY_HTTPS_SERVER_PORT="9443" ``` -Note that `GIT_PROXY_UI_PORT` is needed for both server and UI Node processes, +Note that `GIT_PROXY_UI_PORT` is needed for both server and UI Node processes, whereas `GIT_PROXY_SERVER_PORT` (and `GIT_PROXY_HTTPS_SERVER_PORT`) is only needed by the server process. -By default, GitProxy CLI connects to GitProxy running on localhost and default port. This can be +By default, GitProxy CLI connects to GitProxy running on localhost and default port. This can be changed by setting the `GIT_PROXY_UI_HOST` and `GIT_PROXY_UI_PORT` environment variables: ``` @@ -79,5 +81,66 @@ To validate your configuration at a custom file location, run: git-proxy --validate --config ./config.json ``` +### Configuration Sources + +GitProxy supports dynamic configuration loading from multiple sources. This feature allows you to manage your configuration from external sources and update it without restarting the service. Configuration sources can be files, HTTP endpoints, or Git repositories. + +To enable configuration sources, add the `configurationSources` section to your configuration: + +```json +{ + "configurationSources": { + "enabled": true, + "reloadIntervalSeconds": 60, + "merge": false, + "sources": [ + { + "type": "file", + "enabled": true, + "path": "./external-config.json" + }, + { + "type": "http", + "enabled": true, + "url": "http://config-service/git-proxy-config", + "headers": {}, + "auth": { + "type": "bearer", + "token": "your-token" + } + }, + { + "type": "git", + "enabled": true, + "repository": "https://git-server.com/project/git-proxy-config", + "branch": "main", + "path": "git-proxy/config.json", + "auth": { + "type": "ssh", + "privateKeyPath": "/path/to/.ssh/id_rsa" + } + } + ] + } +} +``` + +The configuration options for `configurationSources` are: + +- `enabled`: Enable/disable dynamic configuration loading +- `reloadIntervalSeconds`: How often to check for configuration updates (in seconds) +- `merge`: When true, merges configurations from all enabled sources. When false, uses the last successful configuration load. This can be used to upload only partial configuration to external source +- `sources`: Array of configuration sources to load from + +Each source can be one of three types: + +1. `file`: Load from a local JSON file +2. `http`: Load from an HTTP endpoint +3. `git`: Load from a Git repository +When configuration changes are detected, GitProxy will: +1. Validate the new configuration +2. Stop existing services +3. Apply the new configuration +4. Restart services with the updated configuration diff --git a/website/docs/configuration/reference.mdx b/website/docs/configuration/reference.mdx index 14d617b79..3b8402305 100644 --- a/website/docs/configuration/reference.mdx +++ b/website/docs/configuration/reference.mdx @@ -648,5 +648,105 @@ description: JSON schema reference documentation for GitProxy
----------------------------------------------------------------------------------------------------------------------------- +
+ + 19. [Optional] Property GitProxy configuration file > configurationSources + +
+ +| | | +| ------------------------- | ------------------------------------------------------- | +| **Type** | `object` | +| **Required** | No | +| **Additional properties** | [[Not allowed]](# "Additional Properties not allowed.") | + +**Description:** Configuration for dynamic loading from external sources + +
+ + 19.1. [Optional] Property configurationSources > enabled + +
+ +| | | +| ------------ | --------- | +| **Type** | `boolean` | +| **Required** | No | + +**Description:** Enable/disable dynamic configuration loading + +
+
+ +
+ + 19.2. [Optional] Property configurationSources > reloadIntervalSeconds + +
+ +| | | +| ------------ | -------- | +| **Type** | `number` | +| **Required** | No | + +**Description:** How often to check for configuration updates (in seconds) + +
+
+ +
+ + 19.3. [Optional] Property configurationSources > merge + +
+ +| | | +| ------------ | --------- | +| **Type** | `boolean` | +| **Required** | No | + +**Description:** When true, merges configurations from all enabled sources. When false, uses the last successful configuration load + +
+
+ +
+ + 19.4. [Optional] Property configurationSources > sources + +
+ +| | | +| ------------ | ------- | +| **Type** | `array` | +| **Required** | No | + +**Description:** Array of configuration sources to load from + +Each item in the array must be an object with the following properties: + +- `type`: (Required) Type of configuration source (`"file"`, `"http"`, or `"git"`) +- `enabled`: (Required) Whether this source is enabled +- `path`: (Required for `file` type) Path to the configuration file +- `url`: (Required for `http` type) URL of the configuration endpoint +- `repository`: (Required for `git` type) Git repository URL +- `branch`: (Optional for `git` type) Branch to use +- `path`: (Required for `git` type) Path to configuration file in repository +- `headers`: (Optional for `http` type) HTTP headers to include +- `auth`: (Optional) Authentication configuration + - For `http` type: + - `type`: `"bearer"` + - `token`: Bearer token value + - For `git` type: + - `type`: `"ssh"` + - `privateKeyPath`: Path to SSH private key + +
+
+ +
+
+ +--- + Generated using [json-schema-for-humans](https://github.com/coveooss/json-schema-for-humans) on 2025-05-01 at 18:17:32 +0100 From 199965359c3c54ecd5df2797cc4066c51add2434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Tue, 20 May 2025 15:02:24 +0200 Subject: [PATCH 76/93] fix: fixes failing CI build as cert is not configured Default value should be false, and set to true when cert path is configured --- proxy.config.json | 2 +- test/chain.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/proxy.config.json b/proxy.config.json index ed3238354..2a45cefac 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -136,7 +136,7 @@ "csrfProtection": true, "plugins": [], "tls": { - "enabled": true, + "enabled": false, "key": "certs/key.pem", "cert": "certs/cert.pem" } diff --git a/test/chain.test.js b/test/chain.test.js index d5c070eb2..4c4dbc3a8 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -15,7 +15,7 @@ const mockLoader = { ], }; -const initMockPushProcessors = () => { +const initMockPushProcessors = (sinon) => { const mockPushProcessors = { parsePush: sinon.stub(), audit: sinon.stub(), From a4cfa78ae702c6d6ddb7d14c9cd2c7b41567156a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Tue, 20 May 2025 15:12:15 +0200 Subject: [PATCH 77/93] fix: rebased to latest main and fixed conflicts --- test/chain.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/chain.test.js b/test/chain.test.js index 4c4dbc3a8..1fc749248 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -28,6 +28,7 @@ const initMockPushProcessors = (sinon) => { writePack: sinon.stub(), preReceive: sinon.stub(), getDiff: sinon.stub(), + gitleaks: sinon.stub(), clearBareClone: sinon.stub(), scanDiff: sinon.stub(), blockForAuth: sinon.stub(), @@ -43,6 +44,7 @@ const initMockPushProcessors = (sinon) => { mockPushProcessors.writePack.displayName = 'writePack'; mockPushProcessors.preReceive.displayName = 'preReceive'; mockPushProcessors.getDiff.displayName = 'getDiff'; + mockPushProcessors.gitleaks.displayName = 'gitleaks'; mockPushProcessors.clearBareClone.displayName = 'clearBareClone'; mockPushProcessors.scanDiff.displayName = 'scanDiff'; mockPushProcessors.blockForAuth.displayName = 'blockForAuth'; @@ -69,7 +71,7 @@ describe('proxy chain', function () { // Create a new sandbox for each test sandboxSinon = sinon.createSandbox(); // Initialize the mock push processors - mockPushProcessors = initMockPushProcessors(); + mockPushProcessors = initMockPushProcessors(sandboxSinon); // Re-import the processors module after clearing the cache processors = await import('../src/proxy/processors'); From 0883127c8a4a07b29c97789b869b5da57c8549d0 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Tue, 20 May 2025 15:57:53 +0100 Subject: [PATCH 78/93] chore: bump by minor to v1.14.0 --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a566172e..57cb15249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/git-proxy", - "version": "1.13.0", + "version": "1.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/git-proxy", - "version": "1.13.0", + "version": "1.14.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" @@ -25,6 +25,7 @@ "connect-mongo": "^5.1.0", "cors": "^2.8.5", "diff2html": "^3.4.33", + "env-paths": "^2.2.1", "express": "^4.18.2", "express-http-proxy": "^2.0.0", "express-rate-limit": "^7.1.5", @@ -6168,7 +6169,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/package.json b/package.json index 4c62e9fa3..4fb28ca04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@finos/git-proxy", - "version": "1.13.0", + "version": "1.14.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { "cli": "node ./packages/git-proxy-cli/index.js", From 0ddd27b746ce177b85559b1e1108c6a0bc6adb10 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 12:53:07 +0900 Subject: [PATCH 79/93] fix(auth): fix bug when calling createUser on admin creation --- src/service/passport/local.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/service/passport/local.js b/src/service/passport/local.js index 979f31354..8fc0b369c 100644 --- a/src/service/passport/local.js +++ b/src/service/passport/local.js @@ -38,12 +38,6 @@ const configure = async (passport) => { } }); - const admin = await db.findUser('admin'); - - if (!admin) { - await db.createUser('admin', 'admin', 'admin@place.com', 'none', true); - } - passport.type = 'local'; return passport; }; @@ -54,7 +48,7 @@ const configure = async (passport) => { const createDefaultAdmin = async () => { const admin = await db.findUser("admin"); if (!admin) { - await db.createUser("admin", "admin", "admin@place.com", "none", true, true, true, true); + await db.createUser("admin", "admin", "admin@place.com", "none", true); } }; From e32408ccb063bd0b75991f6435f647f0ce923853 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 13:17:51 +0900 Subject: [PATCH 80/93] chore(auth): add sample oidc config --- proxy.config.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/proxy.config.json b/proxy.config.json index 2a45cefac..9cc017277 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -53,6 +53,17 @@ "baseDN": "", "searchBase": "" } + }, + { + "type": "openidconnect", + "enabled": false, + "oidcConfig": { + "issuer": "", + "clientID": "", + "clientSecret": "", + "callbackURL": "", + "scope": "" + } } ], "api": { From c83421dc57ce8f4d937ee241fd0d3217fa1a41e5 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 13:26:54 +0900 Subject: [PATCH 81/93] fix: admin to dashboard rename issues --- cypress/e2e/autoApproved.cy.js | 2 +- website/docs/quickstart/approve.mdx | 4 ++-- website/docs/quickstart/intercept.mdx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/autoApproved.cy.js b/cypress/e2e/autoApproved.cy.js index ae67f3ecd..8d830af6b 100644 --- a/cypress/e2e/autoApproved.cy.js +++ b/cypress/e2e/autoApproved.cy.js @@ -45,7 +45,7 @@ describe('Auto-Approved Push Test', () => { }); it('should display auto-approved message and verify tooltip contains the expected timestamp', () => { - cy.visit('/admin/push/123'); + cy.visit('/dashboard/push/123'); cy.wait('@getPush'); diff --git a/website/docs/quickstart/approve.mdx b/website/docs/quickstart/approve.mdx index 8f01e96a4..ebcd59ced 100644 --- a/website/docs/quickstart/approve.mdx +++ b/website/docs/quickstart/approve.mdx @@ -21,7 +21,7 @@ All pushes that flow through GitProxy require an approval (authorisation). Until Following on from [Push via GitProxy](/docs/quickstart/intercept#push-via-git-proxy), a unique & shareable link is generated: ``` -http://localhost:8080/admin/push/0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f +http://localhost:8080/dashboard/push/0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f ``` The `ID` for your push corresponds to the last part of the URL: @@ -174,7 +174,7 @@ Following on from [Push via GitProxy](/docs/quickstart/intercept#push-via-git-pr remote: GitProxy has received your push ✅ remote: remote: 🔗 Shareable Link -remote: http://localhost:8080/admin/push/000000__b12557 +remote: http://localhost:8080/dashboard/push/000000__b12557 ``` Insert the URL directly into your web browser. diff --git a/website/docs/quickstart/intercept.mdx b/website/docs/quickstart/intercept.mdx index d3b5534bc..1ac8e6016 100644 --- a/website/docs/quickstart/intercept.mdx +++ b/website/docs/quickstart/intercept.mdx @@ -93,7 +93,7 @@ remote: remote: GitProxy has received your push ✅ remote: remote: 🔗 Shareable Link -remote: http://localhost:8080/admin/push/000000__b12557 +remote: http://localhost:8080/dashboard/push/000000__b12557 remote: ``` From bab00618fbd7c7804fc5fc44b307379b4b759152 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 13:37:01 +0900 Subject: [PATCH 82/93] fix: failing Cypress test --- cypress/e2e/autoApproved.cy.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cypress/e2e/autoApproved.cy.js b/cypress/e2e/autoApproved.cy.js index 8d830af6b..65d9d65a1 100644 --- a/cypress/e2e/autoApproved.cy.js +++ b/cypress/e2e/autoApproved.cy.js @@ -2,6 +2,8 @@ import moment from 'moment'; describe('Auto-Approved Push Test', () => { beforeEach(() => { + cy.login('admin', 'admin'); + cy.intercept('GET', '/api/v1/push/123', { statusCode: 200, body: { From 70dd3460b97b15f80b244f1db3f23364eb2a3edf Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 14:44:22 +0900 Subject: [PATCH 83/93] test(auth): add proxyquire for mocking --- package-lock.json | 77 ++++++++++++++++++++++++++++++++++++++++++++--- package.json | 1 + 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 57cb15249..0c3c36e2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "mocha": "^10.8.2", "nyc": "^17.0.0", "prettier": "^3.0.0", + "proxyquire": "^2.1.3", "sinon": "^19.0.2", "ts-mocha": "^11.1.0", "ts-node": "^10.9.2", @@ -7258,6 +7259,20 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/fill-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", + "integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-object": "~1.0.1", + "merge-descriptors": "~1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8411,12 +8426,16 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8597,6 +8616,16 @@ "node": ">=8" } }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -10193,6 +10222,13 @@ "node": ">=10" } }, + "node_modules/module-not-found-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", + "integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==", + "dev": true, + "license": "MIT" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -11338,6 +11374,39 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/proxyquire": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", + "integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-keys": "^1.0.2", + "module-not-found-error": "^1.0.1", + "resolve": "^1.11.1" + } + }, + "node_modules/proxyquire/node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index 4fb28ca04..cde79f2fc 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "mocha": "^10.8.2", "nyc": "^17.0.0", "prettier": "^3.0.0", + "proxyquire": "^2.1.3", "sinon": "^19.0.2", "ts-mocha": "^11.1.0", "ts-node": "^10.9.2", From 71e7e52b623e2dffa4c3d64c95d3e7d47a18bc4a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 14:44:45 +0900 Subject: [PATCH 84/93] test(auth): improve test coverage --- test/testAuthMethods.test.js | 43 ++++++++++++++++++++++++++++++++++++ test/testLogin.test.js | 8 +++++++ 2 files changed, 51 insertions(+) create mode 100644 test/testAuthMethods.test.js diff --git a/test/testAuthMethods.test.js b/test/testAuthMethods.test.js new file mode 100644 index 000000000..a05fd7f33 --- /dev/null +++ b/test/testAuthMethods.test.js @@ -0,0 +1,43 @@ +const chai = require('chai'); +const service = require('../src/service'); +const config = require('../src/config'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire'); + +chai.should(); +const expect = chai.expect; + +describe('auth methods', async () => { + let app; + + before(async function () { + app = await service.start(); + }); + + it('should return a local auth method by default', async function () { + const authMethods = config.getAuthMethods(); + expect(authMethods).to.have.lengthOf(1); + expect(authMethods[0].type).to.equal('local'); + }); + + it('should return an error if no auth methods are enabled', async function () { + const newConfig = JSON.stringify({ + authentication: [ + { type: 'local', enabled: false }, + { type: 'ActiveDirectory', enabled: false }, + { type: 'openidconnect', enabled: false }, + ], + }); + + const fsStub = { + existsSync: sinon.stub().returns(true), + readFileSync: sinon.stub().returns(newConfig), + }; + + const config = proxyquire('../src/config', { + fs: fsStub, + }); + + expect(() => config.getAuthMethods()).to.throw(Error, 'No authentication method enabled'); + }); +}); diff --git a/test/testLogin.test.js b/test/testLogin.test.js index 833184e0b..80a0bdae9 100644 --- a/test/testLogin.test.js +++ b/test/testLogin.test.js @@ -62,6 +62,14 @@ describe('auth', async () => { res.should.have.status(401); }); + + it('should fail to login with invalid credentials', async function () { + const res = await chai.request(app).post('/api/auth/login').send({ + username: 'admin', + password: 'invalid', + }); + res.should.have.status(401); + }); }); after(async function () { From 678d9325d5b5649dee2598e18c05b9bbe880bd13 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 14:51:23 +0900 Subject: [PATCH 85/93] test(auth): fix service close issue --- test/testAuthMethods.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/testAuthMethods.test.js b/test/testAuthMethods.test.js index a05fd7f33..9917158e3 100644 --- a/test/testAuthMethods.test.js +++ b/test/testAuthMethods.test.js @@ -40,4 +40,8 @@ describe('auth methods', async () => { expect(() => config.getAuthMethods()).to.throw(Error, 'No authentication method enabled'); }); + + after(async function () { + await service.httpServer.close(); + }); }); From ee8f2c10a8edbafd3b2402b996836005925a1758 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 15:07:21 +0900 Subject: [PATCH 86/93] test(auth): add extra tests and fix linter issues --- test/testAuthMethods.test.js | 34 ++++++++++++++++++++++++---------- test/testLogin.test.js | 10 +++++++++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/test/testAuthMethods.test.js b/test/testAuthMethods.test.js index 9917158e3..013c79d8d 100644 --- a/test/testAuthMethods.test.js +++ b/test/testAuthMethods.test.js @@ -1,5 +1,4 @@ const chai = require('chai'); -const service = require('../src/service'); const config = require('../src/config'); const sinon = require('sinon'); const proxyquire = require('proxyquire'); @@ -8,12 +7,6 @@ chai.should(); const expect = chai.expect; describe('auth methods', async () => { - let app; - - before(async function () { - app = await service.start(); - }); - it('should return a local auth method by default', async function () { const authMethods = config.getAuthMethods(); expect(authMethods).to.have.lengthOf(1); @@ -41,7 +34,28 @@ describe('auth methods', async () => { expect(() => config.getAuthMethods()).to.throw(Error, 'No authentication method enabled'); }); - after(async function () { - await service.httpServer.close(); - }); + it('should return an array of enabled auth methods when overridden', async function () { + const newConfig = JSON.stringify({ + authentication: [ + { type: 'local', enabled: true }, + { type: 'ActiveDirectory', enabled: true }, + { type: 'openidconnect', enabled: true }, + ], + }); + + const fsStub = { + existsSync: sinon.stub().returns(true), + readFileSync: sinon.stub().returns(newConfig), + }; + + const config = proxyquire('../src/config', { + fs: fsStub, + }); + + const authMethods = config.getAuthMethods(); + expect(authMethods).to.have.lengthOf(3); + expect(authMethods[0].type).to.equal('local'); + expect(authMethods[1].type).to.equal('ActiveDirectory'); + expect(authMethods[2].type).to.equal('openidconnect'); + }) }); diff --git a/test/testLogin.test.js b/test/testLogin.test.js index 80a0bdae9..107bb7256 100644 --- a/test/testLogin.test.js +++ b/test/testLogin.test.js @@ -63,7 +63,15 @@ describe('auth', async () => { res.should.have.status(401); }); - it('should fail to login with invalid credentials', async function () { + it('should fail to login with invalid username', async function () { + const res = await chai.request(app).post('/api/auth/login').send({ + username: 'invalid', + password: 'admin', + }); + res.should.have.status(401); + }); + + it('should fail to login with invalid password', async function () { const res = await chai.request(app).post('/api/auth/login').send({ username: 'admin', password: 'invalid', From e96e876626f5cda32485292d9530f096994034dd Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 15:28:56 +0900 Subject: [PATCH 87/93] fix: replaced loading text with actual spinner and removed debug lines --- src/ui/components/PrivateRoute/PrivateRoute.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/PrivateRoute/PrivateRoute.tsx index 4e7a2f4bf..a55f9e159 100644 --- a/src/ui/components/PrivateRoute/PrivateRoute.tsx +++ b/src/ui/components/PrivateRoute/PrivateRoute.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import CircularProgress from '@material-ui/core/CircularProgress'; import { Navigate } from 'react-router-dom'; import { useAuth } from '../../auth/AuthProvider'; @@ -7,12 +7,10 @@ const PrivateRoute = ({ component: Component, adminOnly = false }) => { console.debug('PrivateRoute', { user, isLoading, adminOnly }); if (isLoading) { - console.debug('Auth is loading, waiting'); - return
Loading...
; // TODO: Add loading spinner + return ; } if (!user) { - console.debug('User not logged in, redirecting to login page'); return ; } From fd962d2e7931ac5903f6456bf98d042aa1ab3cec Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 15:30:15 +0900 Subject: [PATCH 88/93] feat: add snackbar for repo fetching errors --- .../components/PrivateRoute/PrivateRoute.tsx | 2 -- .../views/RepoList/Components/RepoOverview.jsx | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/PrivateRoute/PrivateRoute.tsx index a55f9e159..d09219fd5 100644 --- a/src/ui/components/PrivateRoute/PrivateRoute.tsx +++ b/src/ui/components/PrivateRoute/PrivateRoute.tsx @@ -4,7 +4,6 @@ import { useAuth } from '../../auth/AuthProvider'; const PrivateRoute = ({ component: Component, adminOnly = false }) => { const { user, isLoading } = useAuth(); - console.debug('PrivateRoute', { user, isLoading, adminOnly }); if (isLoading) { return ; @@ -15,7 +14,6 @@ const PrivateRoute = ({ component: Component, adminOnly = false }) => { } if (adminOnly && !user.admin) { - console.debug('User is not an admin, redirecting to not authorized page'); return ; } diff --git a/src/ui/views/RepoList/Components/RepoOverview.jsx b/src/ui/views/RepoList/Components/RepoOverview.jsx index 9e98886df..826f78c97 100644 --- a/src/ui/views/RepoList/Components/RepoOverview.jsx +++ b/src/ui/views/RepoList/Components/RepoOverview.jsx @@ -1,6 +1,5 @@ import React, { useEffect } from 'react'; -import TableCell from '@material-ui/core/TableCell'; -import TableRow from '@material-ui/core/TableRow'; +import { Snackbar, TableCell, TableRow } from '@material-ui/core'; import GridContainer from '../../../components/Grid/GridContainer'; import GridItem from '../../../components/Grid/GridItem'; import { CodeReviewIcon, LawIcon, PeopleIcon } from '@primer/octicons-react'; @@ -572,6 +571,9 @@ import CodeActionButton from '../../../components/CustomButtons/CodeActionButton export default function Repositories(props) { const [github, setGitHub] = React.useState({}); + const [errorMessage, setErrorMessage] = React.useState(''); + const [snackbarOpen, setSnackbarOpen] = React.useState(false); + useEffect(() => { getGitHubRepository(); }, [props.data.project, props.data.name]); @@ -582,8 +584,9 @@ export default function Repositories(props) { .then((res) => { setGitHub(res.data); }) - .catch((err) => { - console.error(`Error fetching GitHub repository ${props.data.project}/${props.data.name}: ${err}`); + .catch((error) => { + setErrorMessage(`Error fetching GitHub repository ${props.data.project}/${props.data.name}: ${error}`); + setSnackbarOpen(true); }); }; @@ -672,6 +675,13 @@ export default function Repositories(props) { + setSnackbarOpen(false)} + message={errorMessage} + /> ); } From 304a2ec0bc1f1252094404e7094b557cb223fa5a Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Wed, 21 May 2025 15:35:51 +0900 Subject: [PATCH 89/93] fix: revert react missing from PrivateRoute scope --- src/ui/components/PrivateRoute/PrivateRoute.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/components/PrivateRoute/PrivateRoute.tsx b/src/ui/components/PrivateRoute/PrivateRoute.tsx index d09219fd5..a9adf1aca 100644 --- a/src/ui/components/PrivateRoute/PrivateRoute.tsx +++ b/src/ui/components/PrivateRoute/PrivateRoute.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import CircularProgress from '@material-ui/core/CircularProgress'; import { Navigate } from 'react-router-dom'; import { useAuth } from '../../auth/AuthProvider'; From e220699975bcdc7806c1663224fbde5d7bd7e8c7 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Wed, 28 May 2025 19:53:58 +0100 Subject: [PATCH 90/93] chore: bump by minor to v1.15.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 57cb15249..2f57df5ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/git-proxy", - "version": "1.14.0", + "version": "1.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/git-proxy", - "version": "1.14.0", + "version": "1.15.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" diff --git a/package.json b/package.json index 4fb28ca04..ad837a2fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@finos/git-proxy", - "version": "1.14.0", + "version": "1.15.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { "cli": "node ./packages/git-proxy-cli/index.js", From ec75f33e948d6237ec3a135144e70c512b120af4 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Thu, 29 May 2025 17:13:36 +0900 Subject: [PATCH 91/93] chore: set default empty config --- proxy.config.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/proxy.config.json b/proxy.config.json index cf76bd982..618603a6a 100644 --- a/proxy.config.json +++ b/proxy.config.json @@ -56,13 +56,13 @@ }, { "type": "openidconnect", - "enabled": true, + "enabled": false, "oidcConfig": { - "issuer": "https://accounts.google.com/", - "clientID": "1009968223893-u92qq6itk7ej5008o4174gjubs5lhorg.apps.googleusercontent.com", - "clientSecret": "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", - "callbackURL": "http://localhost:8080/api/auth/oidc/callback", - "scope": "openid email profile" + "issuer": "", + "clientID": "", + "clientSecret": "", + "callbackURL": "", + "scope": "" } } ], @@ -151,8 +151,8 @@ "type": "jwt", "enabled": false, "jwtConfig": { - "clientID": "1009968223893-u92qq6itk7ej5008o4174gjubs5lhorg.apps.googleusercontent.com", - "authorityURL": "https://accounts.google.com" + "clientID": "", + "authorityURL": "" } } ], From 048446fd8332b54e46cba7f419dd3bc06206f9b9 Mon Sep 17 00:00:00 2001 From: Juan Escalada Date: Thu, 29 May 2025 17:44:41 +0900 Subject: [PATCH 92/93] chore: clean up auth methods --- src/service/passport/activeDirectory.js | 6 ------ src/service/passport/jwtAuthHandler.js | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/service/passport/activeDirectory.js b/src/service/passport/activeDirectory.js index d5c6353d9..eef2f7826 100644 --- a/src/service/passport/activeDirectory.js +++ b/src/service/passport/activeDirectory.js @@ -31,11 +31,6 @@ const configure = (passport) => { profile.id = profile.username; req.user = profile; - console.log( - `passport.activeDirectory: resolved login ${ - profile._json.userPrincipalName - }, profile=${JSON.stringify(profile)}`, - ); // First check to see if the user is in the usergroups const isUser = await ldaphelper.isUserInAdGroup(profile.username, domain, userGroup); @@ -48,7 +43,6 @@ const configure = (passport) => { const isAdmin = await ldaphelper.isUserInAdGroup(profile.username, domain, adminGroup); profile.admin = isAdmin; - console.log(`passport.activeDirectory: ${profile.username} admin=${isAdmin}`); const user = { username: profile.username, diff --git a/src/service/passport/jwtAuthHandler.js b/src/service/passport/jwtAuthHandler.js index 7a4c62635..da9e3bc47 100644 --- a/src/service/passport/jwtAuthHandler.js +++ b/src/service/passport/jwtAuthHandler.js @@ -1,6 +1,7 @@ const axios = require("axios"); const jwt = require("jsonwebtoken"); const jwkToPem = require("jwk-to-pem"); +const config = require('../../config'); /** * Obtain the JSON Web Key Set (JWKS) from the OIDC authority. @@ -65,7 +66,7 @@ async function validateJwt(token, authorityUrl, clientID, expectedAudience) { const jwtAuthHandler = () => { return async (req, res, next) => { - const apiAuthMethods = require('../../config').getAPIAuthMethods(); + const apiAuthMethods = config.getAPIAuthMethods(); const jwtAuthMethod = apiAuthMethods.find((method) => method.type.toLowerCase() === "jwt"); if (!jwtAuthMethod) { return next(); From c760d14dc3b2f6657a0ac92b892411f7e154b1b7 Mon Sep 17 00:00:00 2001 From: Jamie Slome Date: Thu, 29 May 2025 11:10:52 +0100 Subject: [PATCH 93/93] chore: bump by minor to v1.16.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a2c99137..b2bcd87ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/git-proxy", - "version": "1.15.0", + "version": "1.16.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/git-proxy", - "version": "1.15.0", + "version": "1.16.0", "license": "Apache-2.0", "workspaces": [ "./packages/git-proxy-cli" diff --git a/package.json b/package.json index 0f0e28b52..969a0a0ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@finos/git-proxy", - "version": "1.15.0", + "version": "1.16.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { "cli": "node ./packages/git-proxy-cli/index.js",