From d4d902054116c73c253230b6f4ac9c3fa32d67a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Tue, 15 Apr 2025 14:30:01 +0200 Subject: [PATCH 1/6] feat: adds create user endpoint and cli command --- packages/git-proxy-cli/index.js | 88 +++++++++++ packages/git-proxy-cli/test/testCli.test.js | 108 +++++++++++++ src/service/routes/auth.js | 31 ++++ test/testLogin.test.js | 159 ++++++++++++++++++++ 4 files changed, 386 insertions(+) diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.js index 142a58a33..6ee1d0539 100755 --- a/packages/git-proxy-cli/index.js +++ b/packages/git-proxy-cli/index.js @@ -330,6 +330,59 @@ async function reloadConfig() { } } +/** + * Create a new user + * @param {string} username The username for the new user + * @param {string} password The password for the new user + * @param {string} email The email for the new user + * @param {string} gitAccount The git account for the new user + * @param {boolean} [admin=false] Whether the user should be an admin (optional) + */ +async function createUser(username, password, email, gitAccount, admin = false) { + if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + console.error('Error: Create User: Authentication required'); + process.exitCode = 1; + return; + } + + try { + const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8')); + + const response = await axios.post( + `${baseUrl}/api/auth/create-user`, + { + username, + password, + email, + gitAccount, + admin, + }, + { + headers: { Cookie: cookies }, + }, + ); + + console.log(`User '${username}' created successfully`); + } catch (error) { + let errorMessage = `Error: Create User: '${error.message}'`; + process.exitCode = 2; + + if (error.response) { + switch (error.response.status) { + case 401: + errorMessage = 'Error: Create User: Authentication required'; + process.exitCode = 3; + break; + case 400: + errorMessage = `Error: Create User: ${error.response.data.message}`; + process.exitCode = 4; + break; + } + } + console.error(errorMessage); + } +} + // Parsing command line arguments yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused-expressions .command({ @@ -465,6 +518,41 @@ yargs(hideBin(process.argv)) // eslint-disable-line @typescript-eslint/no-unused description: 'Reload GitProxy configuration without restarting', action: reloadConfig, }) + .command({ + command: 'create-user', + describe: 'Create a new user', + builder: { + username: { + describe: 'Username for the new user', + demandOption: true, + type: 'string', + }, + password: { + describe: 'Password for the new user', + demandOption: true, + type: 'string', + }, + email: { + describe: 'Email for the new user', + demandOption: true, + type: 'string', + }, + gitAccount: { + describe: 'Git account for the new user', + demandOption: true, + type: 'string', + }, + admin: { + describe: 'Whether the user should be an admin (optional)', + demandOption: false, + type: 'boolean', + default: false, + }, + }, + handler(argv) { + createUser(argv.username, argv.password, argv.email, argv.gitAccount, argv.admin); + }, + }) .demandCommand(1, 'You need at least one command before moving on') .strict() .help().argv; diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.js index fbfce0fe3..897dde9c2 100644 --- a/packages/git-proxy-cli/test/testCli.test.js +++ b/packages/git-proxy-cli/test/testCli.test.js @@ -483,6 +483,114 @@ describe('test git-proxy-cli', function () { }); }); + // *** create user *** + + describe('test git-proxy-cli :: create-user', function () { + it('attempt to create user should fail when server is down', async function () { + try { + // start server -> login -> stop server + await helper.startServer(service); + await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + } finally { + await helper.closeServer(service.httpServer); + } + + const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --password newpass --email new@email.com --gitAccount newgit`; + const expectedExitCode = 2; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Create User:']; + await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); + }); + + it('attempt to create user should fail when not authenticated', async function () { + await helper.removeCookiesFile(); + + const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --password newpass --email new@email.com --gitAccount newgit`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Create User: Authentication required']; + await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); + }); + + it('attempt to create user should fail when not admin', async function () { + try { + await helper.startServer(service); + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username testuser --password testpassword`, + ); + + const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --password newpass --email new@email.com --gitAccount newgit`; + const expectedExitCode = 3; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Create User: Authentication required']; + await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to create user should fail with missing required fields', async function () { + try { + await helper.startServer(service); + await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + + const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --email new@email.com --gitAccount newgit`; + const expectedExitCode = 4; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Create User: Missing required fields']; + await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('should successfully create a new user', async function () { + try { + await helper.startServer(service); + await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + + const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --password newpass --email new@email.com --gitAccount newgit`; + const expectedExitCode = 0; + const expectedMessages = ["User 'newuser' created successfully"]; + const expectedErrorMessages = null; + await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); + + // Verify we can login with the new user + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username newuser --password newpass`, + 0, + [`Login "newuser" : OK`], + null, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('should successfully create a new admin user', async function () { + try { + await helper.startServer(service); + await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); + + const cli = `npx -- @finos/git-proxy-cli create-user --username newadmin --password newpass --email newadmin@email.com --gitAccount newgit --admin`; + const expectedExitCode = 0; + const expectedMessages = ["User 'newadmin' created successfully"]; + const expectedErrorMessages = null; + await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); + + // Verify we can login with the new admin user + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username newadmin --password newpass`, + 0, + [`Login "newadmin" (admin): OK`], + null, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + }); + // *** tests require push in db *** describe('test git-proxy-cli :: git push administration', function () { diff --git a/src/service/routes/auth.js b/src/service/routes/auth.js index 2d9bceb70..e3398e59e 100644 --- a/src/service/routes/auth.js +++ b/src/service/routes/auth.js @@ -168,6 +168,37 @@ router.get('/me', async (req, res) => { } }); +router.post('/create-user', async (req, res) => { + if (!req.user || !req.user.admin) { + return res.status(401).send({ + message: 'You are not authorized to perform this action...', + }); + } + + try { + const { username, password, email, gitAccount, admin: isAdmin = false } = req.body; + + if (!username || !password || !email || !gitAccount) { + return res.status(400).send({ + message: 'Missing required fields: username, password, email, and gitAccount are required', + }); + } + + await db.createUser(username, password, email, gitAccount, isAdmin); + res.status(201).send({ + message: 'User created successfully', + username, + }); + } catch (error) { + console.error('Error creating user:', error); + res.status(400).send({ + message: error.message || 'Failed to create user', + }); + } +}); + +module.exports = router; + module.exports = { router, loginSuccessHandler diff --git a/test/testLogin.test.js b/test/testLogin.test.js index dea0cfc75..4cafc6e35 100644 --- a/test/testLogin.test.js +++ b/test/testLogin.test.js @@ -122,6 +122,165 @@ describe('auth', async () => { }); }); + describe('test create user', async function () { + beforeEach(async function () { + await db.deleteUser('newuser'); + await db.deleteUser('nonadmin'); + }); + + it('should fail to create user when not authenticated', async function () { + const res = await chai.request(app).post('/api/auth/create-user').send({ + username: 'newuser', + password: 'newpass', + email: 'new@email.com', + gitAccount: 'newgit', + }); + + res.should.have.status(401); + res.body.should.have + .property('message') + .eql('You are not authorized to perform this action...'); + }); + + it('should fail to create user when not admin', async function () { + await db.deleteUser('nonadmin'); + await db.createUser('nonadmin', 'nonadmin', 'nonadmin@test.com', 'nonadmin', false); + + // First login as non-admin user + const loginRes = await chai.request(app).post('/api/auth/login').send({ + username: 'nonadmin', + password: 'nonadmin', + }); + + loginRes.should.have.status(200); + + let nonAdminCookie; + // Get the connect cooie + loginRes.headers['set-cookie'].forEach((x) => { + if (x.startsWith('connect')) { + nonAdminCookie = x.split(';')[0]; + } + }); + + console.log('nonAdminCookie', nonAdminCookie); + + const res = await chai + .request(app) + .post('/api/auth/create-user') + .set('Cookie', nonAdminCookie) + .send({ + username: 'newuser', + password: 'newpass', + email: 'new@email.com', + gitAccount: 'newgit', + }); + + res.should.have.status(401); + res.body.should.have + .property('message') + .eql('You are not authorized to perform this action...'); + }); + + it('should fail to create user with missing required fields', async function () { + // First login as admin + const loginRes = await chai.request(app).post('/api/auth/login').send({ + username: 'admin', + password: 'admin', + }); + + const adminCookie = loginRes.headers['set-cookie'][0].split(';')[0]; + + const res = await chai + .request(app) + .post('/api/auth/create-user') + .set('Cookie', adminCookie) + .send({ + username: 'newuser', + // missing password + email: 'new@email.com', + gitAccount: 'newgit', + }); + + res.should.have.status(400); + res.body.should.have + .property('message') + .eql('Missing required fields: username, password, email, and gitAccount are required'); + }); + + it('should successfully create a new user', async function () { + // First login as admin + const loginRes = await chai.request(app).post('/api/auth/login').send({ + username: 'admin', + password: 'admin', + }); + + const adminCookie = loginRes.headers['set-cookie'][0].split(';')[0]; + + const res = await chai + .request(app) + .post('/api/auth/create-user') + .set('Cookie', adminCookie) + .send({ + username: 'newuser', + password: 'newpass', + email: 'new@email.com', + gitAccount: 'newgit', + admin: false, + }); + + res.should.have.status(201); + res.body.should.have.property('message').eql('User created successfully'); + res.body.should.have.property('username').eql('newuser'); + + // Verify we can login with the new user + const newUserLoginRes = await chai.request(app).post('/api/auth/login').send({ + username: 'newuser', + password: 'newpass', + }); + + newUserLoginRes.should.have.status(200); + }); + + it('should fail to create user when username already exists', async function () { + // First login as admin + const loginRes = await chai.request(app).post('/api/auth/login').send({ + username: 'admin', + password: 'admin', + }); + + const adminCookie = loginRes.headers['set-cookie'][0].split(';')[0]; + + const res = await chai + .request(app) + .post('/api/auth/create-user') + .set('Cookie', adminCookie) + .send({ + username: 'newuser', + password: 'newpass', + email: 'new@email.com', + gitAccount: 'newgit', + admin: false, + }); + + res.should.have.status(201); + + // Verify we can login with the new user + const failCreateRes = await chai + .request(app) + .post('/api/auth/create-user') + .set('Cookie', adminCookie) + .send({ + username: 'newuser', + password: 'newpass', + email: 'new@email.com', + gitAccount: 'newgit', + admin: false, + }); + + failCreateRes.should.have.status(400); + }); + }); + after(async function () { await service.httpServer.close(); }); From cf1702416615d5a12e6db926db978583279cfe7c Mon Sep 17 00:00:00 2001 From: Denis Coric Date: Mon, 1 Sep 2025 14:58:32 +0200 Subject: [PATCH 2/6] refactor: remove unused response variable in createUser function --- packages/git-proxy-cli/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.js index 65dffe25e..66502191f 100755 --- a/packages/git-proxy-cli/index.js +++ b/packages/git-proxy-cli/index.js @@ -351,7 +351,7 @@ async function createUser(username, password, email, gitAccount, admin = false) try { const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8')); - const response = await axios.post( + await axios.post( `${baseUrl}/api/auth/create-user`, { username, From fa4c28fb4f24e5da3d6cfd7e76b1af4b475d6c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Mon, 1 Sep 2025 15:53:36 +0200 Subject: [PATCH 3/6] fix: resolve CLI test failures in GitHub Actions - Add proper test setup/teardown for create-user tests - Fix missing required fields test to use empty password instead of omitting it - Use unique usernames with timestamps to prevent conflicts between test runs - Add proper cleanup for created users in finally blocks - Ensure all 42 CLI tests now pass consistently --- packages/git-proxy-cli/test/testCli.test.js | 40 ++++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.js index 64efac3bf..2432a02e9 100644 --- a/packages/git-proxy-cli/test/testCli.test.js +++ b/packages/git-proxy-cli/test/testCli.test.js @@ -493,6 +493,14 @@ describe('test git-proxy-cli', function () { // *** create user *** describe('test git-proxy-cli :: create-user', function () { + before(async function () { + await helper.addUserToDb(TEST_USER, TEST_PASSWORD, TEST_EMAIL, TEST_GIT_ACCOUNT); + }); + + after(async function () { + await helper.removeUserFromDb(TEST_USER); + }); + it('attempt to create user should fail when server is down', async function () { try { // start server -> login -> stop server @@ -541,7 +549,7 @@ describe('test git-proxy-cli', function () { await helper.startServer(service); await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --email new@email.com --gitAccount newgit`; + const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --password "" --email new@email.com --gitAccount newgit`; const expectedExitCode = 4; const expectedMessages = null; const expectedErrorMessages = ['Error: Create User: Missing required fields']; @@ -552,48 +560,62 @@ describe('test git-proxy-cli', function () { }); it('should successfully create a new user', async function () { + const uniqueUsername = `newuser_${Date.now()}`; try { await helper.startServer(service); await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli create-user --username newuser --password newpass --email new@email.com --gitAccount newgit`; + const cli = `npx -- @finos/git-proxy-cli create-user --username ${uniqueUsername} --password newpass --email new@email.com --gitAccount newgit`; const expectedExitCode = 0; - const expectedMessages = ["User 'newuser' created successfully"]; + const expectedMessages = [`User '${uniqueUsername}' created successfully`]; const expectedErrorMessages = null; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); // Verify we can login with the new user await helper.runCli( - `npx -- @finos/git-proxy-cli login --username newuser --password newpass`, + `npx -- @finos/git-proxy-cli login --username ${uniqueUsername} --password newpass`, 0, - [`Login "newuser" : OK`], + [`Login "${uniqueUsername}" : OK`], null, ); } finally { await helper.closeServer(service.httpServer); + // Clean up the created user + try { + await helper.removeUserFromDb(uniqueUsername); + } catch (error) { + // Ignore cleanup errors + } } }); it('should successfully create a new admin user', async function () { + const uniqueUsername = `newadmin_${Date.now()}`; try { await helper.startServer(service); await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli create-user --username newadmin --password newpass --email newadmin@email.com --gitAccount newgit --admin`; + const cli = `npx -- @finos/git-proxy-cli create-user --username ${uniqueUsername} --password newpass --email newadmin@email.com --gitAccount newgit --admin`; const expectedExitCode = 0; - const expectedMessages = ["User 'newadmin' created successfully"]; + const expectedMessages = [`User '${uniqueUsername}' created successfully`]; const expectedErrorMessages = null; await helper.runCli(cli, expectedExitCode, expectedMessages, expectedErrorMessages); // Verify we can login with the new admin user await helper.runCli( - `npx -- @finos/git-proxy-cli login --username newadmin --password newpass`, + `npx -- @finos/git-proxy-cli login --username ${uniqueUsername} --password newpass`, 0, - [`Login "newadmin" (admin): OK`], + [`Login "${uniqueUsername}" (admin): OK`], null, ); } finally { await helper.closeServer(service.httpServer); + // Clean up the created user + try { + await helper.removeUserFromDb(uniqueUsername); + } catch (error) { + // Ignore cleanup errors + } } }); }); From d3aeab52e65c8cba13477ad6723d7561c25966b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Wed, 3 Sep 2025 14:54:21 +0200 Subject: [PATCH 4/6] fix: Update packages/git-proxy-cli/test/testCli.test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes flaky test due to duplicate emails Co-authored-by: Juan Escalada <97265671+jescalada@users.noreply.github.com> Signed-off-by: Denis Ćorić --- packages/git-proxy-cli/test/testCli.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.js index 2432a02e9..82975c0b6 100644 --- a/packages/git-proxy-cli/test/testCli.test.js +++ b/packages/git-proxy-cli/test/testCli.test.js @@ -595,7 +595,7 @@ describe('test git-proxy-cli', function () { await helper.startServer(service); await helper.runCli(`npx -- @finos/git-proxy-cli login --username admin --password admin`); - const cli = `npx -- @finos/git-proxy-cli create-user --username ${uniqueUsername} --password newpass --email newadmin@email.com --gitAccount newgit --admin`; + const cli = `npx -- @finos/git-proxy-cli create-user --username ${uniqueUsername} --password newpass --email ${uniqueUsername}@email.com --gitAccount newgit --admin`; const expectedExitCode = 0; const expectedMessages = [`User '${uniqueUsername}' created successfully`]; const expectedErrorMessages = null; From 193e7340a4d143149bf3c2cc9fc93f123071d454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Wed, 3 Sep 2025 14:55:41 +0200 Subject: [PATCH 5/6] fix: Update packages/git-proxy-cli/test/testCli.test.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes flaky test due to duplicate email Co-authored-by: Juan Escalada <97265671+jescalada@users.noreply.github.com> Signed-off-by: Denis Ćorić --- packages/git-proxy-cli/test/testCli.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.js index 82975c0b6..6865a53ae 100644 --- a/packages/git-proxy-cli/test/testCli.test.js +++ b/packages/git-proxy-cli/test/testCli.test.js @@ -605,7 +605,7 @@ describe('test git-proxy-cli', function () { await helper.runCli( `npx -- @finos/git-proxy-cli login --username ${uniqueUsername} --password newpass`, 0, - [`Login "${uniqueUsername}" (admin): OK`], + [`Login "${uniqueUsername}" <${uniqueUsername}@email.com> (admin): OK`], null, ); } finally { From 373e8fe20ddfc7a63d7219c08c0dc1cbfc0fa32e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C4=86ori=C4=87?= Date: Wed, 3 Sep 2025 14:56:56 +0200 Subject: [PATCH 6/6] fix: added error log for failing cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an error log in case of failed DB cleanup Co-authored-by: Juan Escalada <97265671+jescalada@users.noreply.github.com> Signed-off-by: Denis Ćorić --- packages/git-proxy-cli/test/testCli.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.js index 6865a53ae..02d067579 100644 --- a/packages/git-proxy-cli/test/testCli.test.js +++ b/packages/git-proxy-cli/test/testCli.test.js @@ -614,7 +614,7 @@ describe('test git-proxy-cli', function () { try { await helper.removeUserFromDb(uniqueUsername); } catch (error) { - // Ignore cleanup errors + console.error('Error cleaning up user', error); } } });