diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d23102293..2c533748a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,7 @@ jobs: id: test run: | npm run test-coverage-ci + npm run test-coverage-ci --workspaces --if-present - name: Upload test coverage report uses: codecov/codecov-action@v4.3.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9473d9d9e..f51f433bf 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,9 @@ jobs: fetch-depth: 0 - name: Install Dependencies - run: npm i + run: npm install --workspaces - name: Code Linting - run: npm run lint \ No newline at end of file + run: | + npm run lint + npm run lint --workspaces --if-present \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2fccaf785..19102a93c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@finos/git-proxy", "version": "1.1.0", "license": "Apache-2.0", + "workspaces": [ + "./packages/git-proxy-cli" + ], "dependencies": { "@material-ui/core": "^4.11.0", "@material-ui/icons": "4.11.3", @@ -1420,6 +1423,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@finos/git-proxy": { + "resolved": "", + "link": true + }, + "node_modules/@finos/git-proxy-cli": { + "resolved": "packages/git-proxy-cli", + "link": true + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -10480,6 +10491,22 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "packages/git-proxy-cli": { + "name": "@finos/git-proxy-cli", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@finos/git-proxy": "file:../..", + "axios": "^1.6.0", + "yargs": "^17.7.2" + }, + "bin": { + "git-proxy-cli": "index.js" + }, + "devDependencies": { + "chai": "^4.2.0" + } } } } diff --git a/package.json b/package.json index c5906c2ba..7467b1ab9 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.1.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { + "cli": "node ./packages/git-proxy-cli/index.js", "client": "vite --config vite.config.js", "clientinstall": "npm install --prefix client", "server": "node index.js", @@ -18,6 +19,9 @@ "lint": "eslint --fix . --ext .js,.jsx", "gen-schema-doc": "node ./scripts/doc-schema.js" }, + "workspaces": [ + "./packages/git-proxy-cli" + ], "bin": "./index.js", "author": "Paul Groves", "license": "Apache-2.0", diff --git a/packages/git-proxy-cli/index.js b/packages/git-proxy-cli/index.js new file mode 100755 index 000000000..0e02e0283 --- /dev/null +++ b/packages/git-proxy-cli/index.js @@ -0,0 +1,440 @@ +#!/usr/bin/env node +const axios = require('axios'); +const yargs = require('yargs/yargs'); +const { hideBin } = require('yargs/helpers'); +const fs = require('fs'); +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' } = process.env; +const { GIT_PROXY_UI_PORT: uiPort } = + require('@finos/git-proxy/src/config/env').Vars; +const baseUrl = `${uiHost}:${uiPort}`; + +axios.defaults.timeout = 30000; + +/** + * Log in to Git Proxy + * @param {string} username The user name to login with + * @param {string} password The password to use for the login + */ +async function login(username, password) { + try { + let response = await axios.post( + `${baseUrl}/auth/login`, + { + username, + password, + }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + }, + ); + const cookies = response.headers['set-cookie']; + + response = await axios.get(`${baseUrl}/auth/profile`, { + headers: { Cookie: cookies }, + withCredentials: true, + }); + + fs.writeFileSync(GIT_PROXY_COOKIE_FILE, JSON.stringify(cookies), 'utf8'); + + const user = `"${response.data.username}" <${response.data.email}>`; + const isAdmin = response.data.admin ? ' (admin)' : ''; + console.log(`Login ${user}${isAdmin}: OK`); + } catch (error) { + if (error.response) { + console.error(`Error: Login '${username}': '${error.response.status}'`); + process.exitCode = 1; + } else { + console.error(`Error: Login '${username}': '${error.message}'`); + process.exitCode = 2; + } + } +} + +/** + * Prints a JSON list of git pushes filtered based on specified criteria. + * The function filters the pushes based on various statuses such as whether + * the push is allowed, authorised, blocked, canceled, encountered an error, + * or was rejected. + * + * @param {Object} filters - An object containing filter criteria for Git + * pushes. + * @param {boolean} filters.allowPush - If not null, filters for pushes with + * given attribute and status. + * @param {boolean} filters.authorised - If not null, filters for pushes with + * given attribute and status. + * @param {boolean} filters.blocked - If not null, filters for pushes with + * given attribute and status. + * @param {boolean} filters.canceled - If not null, filters for pushes with + * given attribute and status. + * @param {boolean} filters.error - If not null, filters for pushes with given + * attribute and status. + * @param {boolean} filters.rejected - If not null, filters for pushes with + * given attribute and status. + */ +async function getGitPushes(filters) { + if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + console.error('Error: List: Authentication required'); + process.exitCode = 1; + return; + } + + try { + const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8')); + + const response = await axios.get(`${baseUrl}/api/v1/push/`, { + headers: { Cookie: cookies }, + params: filters, + }); + + const records = []; + response.data?.forEach((push) => { + const record = {}; + record.id = push.id; + record.timestamp = push.timestamp; + record.url = push.url; + record.allowPush = push.allowPush; + record.authorised = push.authorised; + record.blocked = push.blocked; + record.canceled = push.canceled; + record.error = push.error; + record.rejected = push.rejected; + + record.lastStep = { + stepName: push.lastStep?.stepName, + error: push.lastStep?.error, + errorMessage: push.lastStep?.errorMessage, + blocked: push.lastStep?.blocked, + blockedMessage: push.lastStep?.blockedMessage, + }; + + record.commitData = []; + push.commitData?.forEach((pushCommitDataRecord) => { + record.commitData.push({ + message: pushCommitDataRecord.message, + committer: pushCommitDataRecord.committer, + }); + }); + + records.push(record); + }); + + console.log(`${util.inspect(records, false, null, false)}`); + } catch (error) { + // default error + let errorMessage = `Error: List: '${error.message}'`; + process.exitCode = 2; + + if (error.response && error.response.status == 401) { + errorMessage = 'Error: List: Authentication required'; + process.exitCode = 3; + } + console.error(errorMessage); + } +} + +/** + * Authorise git push by ID + * @param {string} id The ID of the git push to authorise + */ +async function authoriseGitPush(id) { + if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + console.error('Error: Authorise: Authentication required'); + process.exitCode = 1; + return; + } + + try { + const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8')); + + response = await axios.get(`${baseUrl}/api/v1/push/${id}`, { + headers: { Cookie: cookies }, + }); + + response = await axios.post( + `${baseUrl}/api/v1/push/${id}/authorise`, + {}, + { + headers: { Cookie: cookies }, + }, + ); + + console.log(`Authorise: ID: '${id}': OK`); + } catch (error) { + // default error + let errorMessage = `Error: Authorise: '${error.message}'`; + process.exitCode = 2; + + if (error.response) { + switch (error.response.status) { + case 401: + errorMessage = 'Error: Authorise: Authentication required'; + process.exitCode = 3; + break; + case 404: + errorMessage = `Error: Authorise: ID: '${id}': Not Found`; + process.exitCode = 4; + } + } + console.error(errorMessage); + } +} + +/** + * Reject git push by ID + * @param {string} id The ID of the git push to reject + */ +async function rejectGitPush(id) { + if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + console.error('Error: Reject: Authentication required'); + process.exitCode = 1; + return; + } + + try { + const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8')); + + response = await axios.get(`${baseUrl}/api/v1/push/${id}`, { + headers: { Cookie: cookies }, + }); + + response = await axios.post( + `${baseUrl}/api/v1/push/${id}/reject`, + {}, + { + headers: { Cookie: cookies }, + }, + ); + + console.log(`Reject: ID: '${id}': OK`); + } catch (error) { + // default error + let errorMessage = `Error: Reject: '${error.message}'`; + process.exitCode = 2; + + if (error.response) { + switch (error.response.status) { + case 401: + errorMessage = 'Error: Reject: Authentication required'; + process.exitCode = 3; + break; + case 404: + errorMessage = `Error: Reject: ID: '${id}': Not Found`; + process.exitCode = 4; + } + } + console.error(errorMessage); + } +} + +/** + * Cancel git push by ID + * @param {string} id The ID of the git push to cancel + */ +async function cancelGitPush(id) { + if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + console.error('Error: Cancel: Authentication required'); + process.exitCode = 1; + return; + } + + try { + const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8')); + + response = await axios.get(`${baseUrl}/api/v1/push/${id}`, { + headers: { Cookie: cookies }, + }); + + response = await axios.post( + `${baseUrl}/api/v1/push/${id}/cancel`, + {}, + { + headers: { Cookie: cookies }, + }, + ); + + console.log(`Cancel: ID: '${id}': OK`); + } catch (error) { + // default error + let errorMessage = `Error: Cancel: '${error.message}'`; + process.exitCode = 2; + + if (error.response) { + switch (error.response.status) { + case 401: + errorMessage = 'Error: Cancel: Authentication required'; + process.exitCode = 3; + break; + case 404: + errorMessage = `Error: Cancel: ID: '${id}': Not Found`; + process.exitCode = 4; + } + } + console.error(errorMessage); + } +} + +/** + * Log out (and clean up) + */ +async function logout() { + if (fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + try { + const cookies = JSON.parse( + fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8'), + ); + fs.writeFileSync(GIT_PROXY_COOKIE_FILE, '*** logged out ***', 'utf8'); + fs.unlinkSync(GIT_PROXY_COOKIE_FILE); + + response = await axios.post( + `${baseUrl}/auth/logout`, + {}, + { + headers: { Cookie: cookies }, + }, + ); + } catch (error) { + console.log(`Warning: Logout: '${error.message}'`); + } + } + + console.log('Logout: OK'); +} + +// Parsing command line arguments +yargs(hideBin(process.argv)) + .command({ + command: 'authorise', + describe: 'Authorise git push by ID', + builder: { + id: { + describe: 'Push ID', + demandOption: true, + type: 'string', + }, + }, + handler(argv) { + authoriseGitPush(argv.id); + }, + }) + .command({ + command: 'cancel', + describe: 'Cancel git push by ID', + builder: { + id: { + describe: 'Push ID', + demandOption: true, + type: 'string', + }, + }, + handler(argv) { + cancelGitPush(argv.id); + }, + }) + .command({ + command: 'config', + describe: 'Print configuration', + handler(argv) { + console.log(`GitProxy URL: ${baseUrl}`); + }, + }) + .command({ + command: 'login', + describe: 'Log in by username/password', + builder: { + username: { + describe: 'Username', + demandOption: true, + type: 'string', + }, + password: { + describe: 'Password', + demandOption: true, + type: 'string', + }, + }, + handler(argv) { + login(argv.username, argv.password); + }, + }) + .command({ + command: 'logout', + describe: 'Log out', + handler(argv) { + logout(); + }, + }) + .command({ + command: 'ls', + describe: 'Get list of git pushes', + builder: { + allowPush: { + describe: `Filter for the "allowPush" flag of the git push on the list`, + demandOption: false, + type: 'boolean', + default: null, + }, + authorised: { + describe: `Filter for the "authorised" flag of the git push on the list`, // eslint-disable-line max-len + demandOption: false, + type: 'boolean', + default: null, + }, + blocked: { + describe: `Filter for the "blocked" flag of the git push on the list`, + demandOption: false, + type: 'boolean', + default: null, + }, + canceled: { + describe: `Filter for the "canceled" flag of the git push on the list`, + demandOption: false, + type: 'boolean', + default: null, + }, + error: { + describe: `Filter for the "error" flag of the git push on the list`, + demandOption: false, + type: 'boolean', + default: null, + }, + rejected: { + describe: `Filter for the "rejected" flag of the git push on the list`, + demandOption: false, + type: 'boolean', + default: null, + }, + }, + handler(argv) { + const filters = { + allowPush: argv.allowPush, + authorised: argv.authorised, + blocked: argv.blocked, + canceled: argv.canceled, + error: argv.error, + rejected: argv.rejected, + }; + getGitPushes(filters); + }, + }) + .command({ + command: 'reject', + describe: 'Reject git push by ID', + builder: { + id: { + describe: 'Push ID', + demandOption: true, + type: 'string', + }, + }, + handler(argv) { + rejectGitPush(argv.id); + }, + }) + .demandCommand(1, 'You need at least one command before moving on') + .strict() + .help().argv; diff --git a/packages/git-proxy-cli/package.json b/packages/git-proxy-cli/package.json new file mode 100644 index 000000000..8cb33729a --- /dev/null +++ b/packages/git-proxy-cli/package.json @@ -0,0 +1,22 @@ +{ + "name": "@finos/git-proxy-cli", + "version": "0.1.0", + "description": "Command line interface tool for FINOS Git Proxy.", + "bin": "./index.js", + "dependencies": { + "axios": "^1.6.0", + "yargs": "^17.7.2", + "@finos/git-proxy": "file:../.." + }, + "devDependencies": { + "chai": "^4.2.0" + }, + "scripts": { + "lint": "eslint --fix . --ext .js,.jsx", + "test": "mocha --exit --timeout 10000", + "test-coverage": "nyc npm run test", + "test-coverage-ci": "nyc --reporter=lcovonly --reporter=text --reporter=html npm run test" + }, + "author": "Miklos Sagi", + "license": "Apache-2.0" +} diff --git a/packages/git-proxy-cli/test/testCli.proxy.config.json b/packages/git-proxy-cli/test/testCli.proxy.config.json new file mode 100644 index 000000000..48073ef57 --- /dev/null +++ b/packages/git-proxy-cli/test/testCli.proxy.config.json @@ -0,0 +1,37 @@ +{ + "tempPassword": { + "sendEmail": false, + "emailConfig": { + } + }, + "authorisedList": [ + { + "project": "msagi", + "name": "git-proxy-test", + "url": "https://github.com/msagi/git-proxy-test.git" + } + ], + "sink": [ + { + "type": "fs", + "params": { + "filepath": "./." + }, + "enabled": true + }, + { + "type": "mongo", + "connectionString": "mongodb://localhost:27017/gitproxy", + "options": { + "useUnifiedTopology": true + }, + "enabled": false + } + ], + "authentication": [ + { + "type": "local", + "enabled": true + } + ] +} diff --git a/packages/git-proxy-cli/test/testCli.test.js b/packages/git-proxy-cli/test/testCli.test.js new file mode 100644 index 000000000..234387867 --- /dev/null +++ b/packages/git-proxy-cli/test/testCli.test.js @@ -0,0 +1,896 @@ +/* eslint-disable max-len */ +const helper = require('./testCliUtils'); + +const path = require('path'); + +// set test proxy config file path *before* loading the proxy +require('../../../src/config/file').configFile = path.join( + process.cwd(), + 'test', + 'testCli.proxy.config.json', +); +const service = require('../../../src/service'); + +/* test constants */ +// push ID which does not exist +const GHOST_PUSH_ID = + '0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f'; + +describe('test git-proxy-cli', function () { + // *** help *** + + describe(`test git-proxy-cli :: help`, function () { + it(`print help if no command or option is given`, async function () { + const cli = `npx -- @finos/git-proxy-cli`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = [ + 'Commands:', + 'Options:', + 'You need at least one command before moving on', + ]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it(`print help if invalid command or option is given`, async function () { + const cli = `npx -- @finos/git-proxy-cli invalid --invalid`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = [ + 'Commands:', + 'Options:', + 'Unknown arguments: invalid, invalid', + ]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it(`print help if "--help" option is given`, async function () { + const cli = `npx -- @finos/git-proxy-cli invalid --help`; + const expectedExitCode = 0; + const expectedMessages = ['Commands:', 'Options:']; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + }); + + // *** version *** + + describe(`test git-proxy-cli :: version`, function () { + it(`"--version" option prints version details `, async function () { + const cli = `npx -- @finos/git-proxy-cli --version`; + const expectedExitCode = 0; + const expectedMessages = ['0.1.0']; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + }); + + // *** cofiguration *** + + describe('test git-proxy-cli :: configuration', function () { + it(`"config" command prints configuration details`, async function () { + const cli = `npx -- @finos/git-proxy-cli config`; + const expectedExitCode = 0; + const expectedMessages = ['GitProxy URL:']; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + }); + + // *** login *** + + describe('test git-proxy-cli :: login', function () { + const testUser = 'testuser'; + const testPassword = 'testpassword'; + const testEmail = 'jane.doe@email.com'; + + before(async function () { + await helper.addUserToDb( + testUser, + testPassword, + testEmail, + 'testGitAccount', + ); + }); + + it('login shoud fail when server is down', async function () { + const username = 'admin'; + const password = 'admin'; + const cli = `npx -- @finos/git-proxy-cli login --username ${username} --password ${password}`; + const expectedExitCode = 2; + const expectedMessages = null; + const expectedErrorMessages = [`Error: Login '${username}':`]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('login shoud fail with invalid credentials', async function () { + const username = 'unkn0wn'; + const password = 'p4ssw0rd'; + const cli = `npx -- @finos/git-proxy-cli login --username ${username} --password ${password}`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = [`Error: Login '${username}': '401'`]; + try { + await helper.startServer(service); + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('login shoud be successful with valid credentials (admin)', async function () { + const username = 'admin'; + const password = 'admin'; + const cli = `npx -- @finos/git-proxy-cli login --username ${username} --password ${password}`; + const expectedExitCode = 0; + const expectedMessages = [ + `Login "${username}" (admin): OK`, + ]; + const expectedErrorMessages = null; + try { + await helper.startServer(service); + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('login shoud be successful with valid credentials (non-admin)', async function () { + const cli = `npx -- @finos/git-proxy-cli login --username ${testUser} --password ${testPassword}`; + const expectedExitCode = 0; + const expectedMessages = [`Login "${testUser}" <${testEmail}>: OK`]; + const expectedErrorMessages = null; + try { + await helper.startServer(service); + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + }); + + // *** logout *** + + describe('test git-proxy-cli :: logout', function () { + it('logout shoud succeed when server is down (and not logged in before)', async function () { + await helper.removeCookiesFile(); + + const cli = `npx -- @finos/git-proxy-cli logout`; + const expectedExitCode = 0; + const expectedMessages = [`Logout: OK`]; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('logout should succeed when server is down (but logged in before)', async function () { + try { + 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 logout`; + const expectedExitCode = 0; + const expectedMessages = [`Logout: OK`]; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('logout should succeed when not authenticated (server is up)', async function () { + try { + await helper.createCookiesFileWithExpiredCookie(); + + const cli = `npx -- @finos/git-proxy-cli logout`; + const expectedExitCode = 0; + const expectedMessages = [`Logout: OK`]; + const expectedErrorMessages = null; + await helper.startServer(service); + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('logout shoud be successful when authenticated (server is up)', 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 logout`; + const expectedExitCode = 0; + const expectedMessages = [`Logout: OK`]; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + }); + + // *** authorise *** + + describe('test git-proxy-cli :: authorise', function () { + it('attempt to authorise 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 id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const expectedExitCode = 2; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Authorise:']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to authorise should fail when not authenticated', async function () { + await helper.removeCookiesFile(); + + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = [ + 'Error: Authorise: Authentication required', + ]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to authorise should fail when not authenticated (server restarted)', async function () { + try { + await helper.createCookiesFileWithExpiredCookie(); + await helper.startServer(service); + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const expectedExitCode = 3; + const expectedMessages = null; + const expectedErrorMessages = [ + 'Error: Authorise: Authentication required', + ]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to authorise should fail when git push ID not found', async function () { + try { + await helper.startServer(service); + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username admin --password admin`, + ); + + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli authorise --id ${id}`; + const expectedExitCode = 4; + const expectedMessages = null; + const expectedErrorMessages = [ + `Error: Authorise: ID: '${id}': Not Found`, + ]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + }); + + // *** cancel *** + + describe('test git-proxy-cli :: cancel', function () { + it('attempt to cancel 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 id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const expectedExitCode = 2; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Cancel:']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to cancel should fail when not authenticated', async function () { + await helper.removeCookiesFile(); + + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Cancel: Authentication required']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to cancel should fail when not authenticated (server restarted)', async function () { + try { + await helper.createCookiesFileWithExpiredCookie(); + await helper.startServer(service); + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const expectedExitCode = 3; + const expectedMessages = null; + const expectedErrorMessages = [ + 'Error: Cancel: Authentication required', + ]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + // }); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to cancel should fail when git push ID not found', async function () { + try { + await helper.startServer(service); + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username admin --password admin`, + ); + + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli cancel --id ${id}`; + const expectedExitCode = 4; + const expectedMessages = null; + const expectedErrorMessages = [`Error: Cancel: ID: '${id}': Not Found`]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + }); + + // *** ls *** + + describe('test git-proxy-cli :: ls (list)', function () { + it('attempt to ls 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 ls`; + const expectedExitCode = 2; + const expectedMessages = null; + const expectedErrorMessages = ['Error: List:']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to ls should fail when not authenticated', async function () { + await helper.removeCookiesFile(); + + const cli = `npx -- @finos/git-proxy-cli ls`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = ['Error: List: Authentication required']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to ls should fail when not authenticated (server restarted)', async function () { + try { + await helper.createCookiesFileWithExpiredCookie(); + await helper.startServer(service); + const cli = `npx -- @finos/git-proxy-cli ls`; + const expectedExitCode = 3; + const expectedMessages = null; + const expectedErrorMessages = ['Error: List: Authentication required']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to ls should fail when invalid option given', 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 ls --invalid`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = ['Options:', 'Unknown argument: invalid']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + }); + + // *** reject *** + + describe('test git-proxy-cli :: reject', function () { + it('attempt to reject 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 id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const expectedExitCode = 2; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Reject:']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to reject should fail when not authenticated', async function () { + await helper.removeCookiesFile(); + + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const expectedExitCode = 1; + const expectedMessages = null; + const expectedErrorMessages = ['Error: Reject: Authentication required']; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + }); + + it('attempt to reject should fail when not authenticated (server restarted)', async function () { + try { + await helper.createCookiesFileWithExpiredCookie(); + await helper.startServer(service); + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const expectedExitCode = 3; + const expectedMessages = null; + const expectedErrorMessages = [ + 'Error: Reject: Authentication required', + ]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to reject should fail when git push ID not found', async function () { + try { + await helper.startServer(service); + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username admin --password admin`, + ); + + const id = GHOST_PUSH_ID; + const cli = `npx -- @finos/git-proxy-cli reject --id ${id}`; + const expectedExitCode = 4; + const expectedMessages = null; + const expectedErrorMessages = [`Error: Reject: ID: '${id}': Not Found`]; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + }); + + // *** tests require push in db *** + + describe('test git-proxy-cli :: git push administration', function () { + const pushId = `0000000000000000000000000000000000000000__${Date.now()}`; + const repo = 'test-repo'; + + before(async function () { + await helper.addGitPushToDb(pushId, repo); + }); + + it('attempt to ls should list existing push', 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 ls --authorised false --blocked true --canceled false --rejected false`; + const expectedExitCode = 0; + const expectedMessages = [ + pushId, + repo, + 'authorised: false', + 'blocked: true', + 'canceled: false', + 'error: false', + 'rejected: false', + ]; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to ls should not list existing push when filtered for authorised', 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 ls --authorised true`; + const expectedExitCode = 0; + const expectedMessages = ['[]']; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to ls should not list existing push when filtered for canceled', 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 ls --canceled true`; + const expectedExitCode = 0; + const expectedMessages = ['[]']; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to ls should not list existing push when filtered for rejected', 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 ls --rejected true`; + const expectedExitCode = 0; + const expectedMessages = ['[]']; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('attempt to ls should not list existing push when filtered for non-blocked', 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 ls --blocked false`; + const expectedExitCode = 0; + const expectedMessages = ['[]']; + const expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('authorise push and test if appears on authorised list', async function () { + try { + await helper.startServer(service); + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username admin --password admin`, + ); + + let cli = `npx -- @finos/git-proxy-cli ls --authorised true --canceled false --rejected false`; + let expectedExitCode = 0; + let expectedMessages = ['[]']; + let expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + + cli = `npx -- @finos/git-proxy-cli authorise --id ${pushId}`; + expectedExitCode = 0; + expectedMessages = [`Authorise: ID: '${pushId}': OK`]; + expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + + cli = `npx -- @finos/git-proxy-cli ls --authorised true --canceled false --rejected false`; + expectedExitCode = 0; + expectedMessages = [pushId, repo]; + expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('reject push and test if appears on rejected list', async function () { + try { + await helper.startServer(service); + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username admin --password admin`, + ); + + let cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled false --rejected true`; + let expectedExitCode = 0; + let expectedMessages = ['[]']; + let expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + + cli = `npx -- @finos/git-proxy-cli reject --id ${pushId}`; + expectedExitCode = 0; + expectedMessages = [`Reject: ID: '${pushId}': OK`]; + expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + + cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled false --rejected true`; + expectedExitCode = 0; + expectedMessages = [pushId, repo]; + expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + } + }); + + it('cancel push and test if appears on canceled list', async function () { + try { + await helper.startServer(service); + await helper.runCli( + `npx -- @finos/git-proxy-cli login --username admin --password admin`, + ); + + let cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled true --rejected false`; + let expectedExitCode = 0; + let expectedMessages = ['[]']; + let expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + + cli = `npx -- @finos/git-proxy-cli cancel --id ${pushId}`; + expectedExitCode = 0; + expectedMessages = [`Cancel: ID: '${pushId}': OK`]; + expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + + cli = `npx -- @finos/git-proxy-cli ls --authorised false --canceled true --rejected false`; + expectedExitCode = 0; + expectedMessages = [pushId, repo]; + expectedErrorMessages = null; + await helper.runCli( + cli, + expectedExitCode, + expectedMessages, + expectedErrorMessages, + ); + } finally { + await helper.closeServer(service.httpServer); + await helper.removeCookiesFile(); + } + }); + }); +}); diff --git a/packages/git-proxy-cli/test/testCliUtils.js b/packages/git-proxy-cli/test/testCliUtils.js new file mode 100644 index 000000000..376ecea48 --- /dev/null +++ b/packages/git-proxy-cli/test/testCliUtils.js @@ -0,0 +1,217 @@ +const fs = require('fs'); +const util = require('util'); +const { exec } = require('child_process'); +const execAsync = util.promisify(exec); +const { expect } = require('chai'); + +const actions = require('../../../src/proxy/actions/Action'); +const steps = require('../../../src/proxy/actions/Step'); +const processor = require('../../../src/proxy/processors/push-action/audit'); +const db = require('../../../src/db'); + +// cookie file name +const GIT_PROXY_COOKIE_FILE = 'git-proxy-cookie'; + +/** + * @async + * @param {string} cli - The CLI command to be executed. + * @param {number} expectedExitCode - The expected exit code after the command + * execution. Typically, `0` for successful execution. + * @param {string} expectedMessages - The array of expected messages included + * in the output after the command execution. + * @param {string} expectedErrorMessages - The array of expected messages + * included in the error output after the command execution. + * @param {boolean} debug - Flag to enable detailed logging for debugging. + * @throws {AssertionError} Throws an error if the actual exit code does not + * match the `expectedExitCode`. + */ +async function runCli( + cli, + expectedExitCode = 0, + expectedMessages = null, + expectedErrorMessages = null, + debug = false, +) { + try { + console.log(`cli: ${cli}`); + const { stdout, stderr } = await execAsync(cli); + if (debug) { + console.log(`stdout: ${stdout}`); + console.log(`stderr: ${stderr}`); + } + expect(0).to.equal(expectedExitCode); + if (expectedMessages) { + expectedMessages.forEach((expectedMessage) => { + expect(stdout).to.include(expectedMessage); + }); + } + if (expectedErrorMessages) { + expectedErrorMessages.forEach((expectedErrorMessage) => { + expect(stderr).to.include(expectedErrorMessage); + }); + } + } catch (error) { + const exitCode = error.code; + if (!exitCode) { + // an AssertionError is thrown from failing some of the expectations + // in the 'try' block: forward it to Mocha to process + throw error; + } + if (debug) { + console.log(`error.stdout: ${error.stdout}`); + console.log(`error.stderr: ${error.stderr}`); + } + expect(exitCode).to.equal(expectedExitCode); + if (expectedMessages) { + expectedMessages.forEach((expectedMessage) => { + expect(error.stdout).to.include(expectedMessage); + }); + } + if (expectedErrorMessages) { + expectedErrorMessages.forEach((expectedErrorMessage) => { + expect(error.stderr).to.include(expectedErrorMessage); + }); + } + } +} + +/** + * Starts the server. + * @param {Object} service - The Git Proxy API service to be started. + * @return {Promise} A promise that resolves when the service has + * successfully started. Does not return any value upon resolution. + */ +async function startServer(service) { + await service.start(); +} + +/** + * Closes the specified HTTP server gracefully. This function wraps the + * `close` method of the `http.Server` instance in a promise to facilitate + * async/await usage. It ensures the server stops accepting new connections + * and terminates existing ones before shutting down. + * + * @param {http.Server} server - The `http.Server` instance to close. + * @param {number} waitTime - The wait time after close. + * @return {Promise} A promise that resolves when the server has been + * successfully closed, or rejects if an error occurs during closure. The + * promise does not return any value upon resolution. + * + * @throws {Error} If the server cannot be closed properly or if an error + * occurs during the close operation. + */ +async function closeServer(server, waitTime = 0) { + return new Promise((resolve, reject) => { + server.closeAllConnections(); + server.close((err) => { + if (err) { + console.error('Failed to close the server:', err); + reject(err); // Reject the promise if there's an error + } else { + setTimeout(() => { + console.log(`Server closed successfully (wait time ${waitTime}).`); + resolve(); // Resolve the promise when the server is closed + }, waitTime); + } + }); + }); +} + +/** + * Create local cookies file with an expired connect cookie. + */ +async function createCookiesFileWithExpiredCookie() { + await removeCookiesFile(); + const cookies = [ + // eslint-disable-next-line max-len + 'connect.sid=s%3AuWjJK_VGFbX9-03UfvoSt_HFU3a0vFOd.jd986YQ17Bw4j1xGJn2l9yiF3QPYhayaYcDqGsNgQY4; Path=/; HttpOnly', + ]; + fs.writeFileSync(GIT_PROXY_COOKIE_FILE, JSON.stringify(cookies), 'utf8'); +} + +/** + * Remove local cookies file. + */ +async function removeCookiesFile() { + if (fs.existsSync(GIT_PROXY_COOKIE_FILE)) { + fs.unlinkSync(GIT_PROXY_COOKIE_FILE); + } +} + +/** + * Add a new git push record to the database. + * @param {string} id The ID of the git push. + * @param {string} repo The repository of the git push. + * @param {boolean} debug Flag to enable logging for debugging. + */ +async function addGitPushToDb(id, repo, debug = false) { + const action = new actions.Action( + id, + 'push', // type + 'get', // method + Date.now(), // timestamp + repo, + ); + const step = new steps.Step( + 'authBlock', // stepName + false, // error + null, // errorMessage + true, // blocked + `\n\n\nGit Proxy has received your push:\n\nhttp://localhost:8080/requests/${id}\n\n\n`, // blockedMessage + null, // content + ); + const commitData = []; + commitData.push({ + tree: 'tree test', + parent: 'parent', + author: 'author', + committer: 'committer', + commitTs: 'commitTs', + message: 'message', + }); + action.commitData = commitData; + action.addStep(step); + const result = await processor.exec(null, action); + if (debug) { + console.log(`New git push added to DB: ${util.inspect(result)}`); + } +} + +/** + * Add new user record to the database. + * @param {string} username The user name. + * @param {string} password The user password. + * @param {string} email The user email. + * @param {string} gitAccount The user git account. + * @param {boolean} admin Flag to make the user administrator. + * @param {boolean} debug Flag to enable logging for debugging. + */ +async function addUserToDb( + username, + password, + email, + gitAccount, + admin = false, + debug = false, +) { + const result = await db.createUser( + username, + password, + email, + gitAccount, + admin, + ); + if (debug) { + console.log(`New user added to DB: ${util.inspect(result)}`); + } +} + +module.exports = { + runCli: runCli, + startServer: startServer, + closeServer: closeServer, + addGitPushToDb: addGitPushToDb, + addUserToDb: addUserToDb, + createCookiesFileWithExpiredCookie: createCookiesFileWithExpiredCookie, + removeCookiesFile: removeCookiesFile, +}; diff --git a/src/proxy/index.js b/src/proxy/index.js index b22801039..6943bc772 100644 --- a/src/proxy/index.js +++ b/src/proxy/index.js @@ -27,8 +27,8 @@ const start = async () => { ); if (!found) { await db.createRepo(x); - await db.addUserCanPush('git-proxy', 'admin'); - await db.addUserCanAuthorise('git-proxy', 'admin'); + await db.addUserCanPush(x.name, 'admin'); + await db.addUserCanAuthorise(x.name, 'admin'); } }); diff --git a/src/service/routes/push.js b/src/service/routes/push.js index d4718fc85..8898ba427 100644 --- a/src/service/routes/push.js +++ b/src/service/routes/push.js @@ -30,7 +30,14 @@ router.get('/', async (req, res) => { router.get('/:id', async (req, res) => { if (req.user) { const id = req.params.id; - res.send(await db.getPush(id)); + push = await db.getPush(id); + if (push) { + res.send(push); + } else { + res.status(404).send({ + message: 'not found', + }); + } } else { res.status(401).send({ message: 'not logged in', diff --git a/test/testPush.test.js b/test/testPush.test.js new file mode 100644 index 000000000..c046edd49 --- /dev/null +++ b/test/testPush.test.js @@ -0,0 +1,56 @@ +// Import the dependencies for testing +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const db = require('../src/db'); +const service = require('../src/service'); + +chai.use(chaiHttp); +chai.should(); +const expect = chai.expect; + +describe('auth', async () => { + let app; + let cookie; + + before(async function () { + app = await service.start(); + await db.deleteUser('login-test-user'); + + const res = await chai.request(app).post('/auth/login').send({ + username: 'admin', + password: 'admin', + }); + + expect(res).to.have.cookie('connect.sid'); + res.should.have.status(200); + + // Get the connect cooie + res.headers['set-cookie'].forEach((x) => { + if (x.startsWith('connect')) { + cookie = x.split(';')[0]; + } + }); + }); + + describe('test push API', async function () { + it('should get 404 for unknown push', async function () { + const commitId = + '0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f'; // eslint-disable-line max-len + const res = await chai + .request(app) + .get(`/api/v1/push/${commitId}`) + .set('Cookie', `${cookie}`); + res.should.have.status(404); + }); + }); + + after(async function () { + const res = await chai + .request(app) + .post('/auth/logout') + .set('Cookie', `${cookie}`); + res.should.have.status(200); + + await service.httpServer.close(); + }); +}); diff --git a/website/docs/configuration/overview.mdx b/website/docs/configuration/overview.mdx index b8a93336b..b95785b21 100644 --- a/website/docs/configuration/overview.mdx +++ b/website/docs/configuration/overview.mdx @@ -56,6 +56,14 @@ export GIT_PROXY_SERVER_PORT="9090" Note that `GIT_PROXY_UI_PORT` is needed for both server and UI Node processes, whereas `GIT_PROXY_SERVER_PORT` is only needed by the server process. +By default, Git Proxy CLI connects to Git Proxy running on localhost and default port. This can be +changed by setting the `GIT_PROXY_UI_HOST` and `GIT_PROXY_UI_PORT` environment variables: + +``` +export GIT_PROXY_UI_HOST="http://www.git-proxy.com" +export GIT_PROXY_UI_PORT="5000" +``` + ### Validate configuration To validate your Git Proxy configuration, run: diff --git a/website/docs/installation.mdx b/website/docs/installation.mdx index 163dac0a4..79f018d44 100644 --- a/website/docs/installation.mdx +++ b/website/docs/installation.mdx @@ -11,10 +11,21 @@ To install Git Proxy, you must first install [Node.js](https://nodejs.org/en/dow npm install -g @finos/git-proxy ``` +To install the Git Proxy Command Line Interface (CLI), run: +```bash +npm install -g @finos/git-proxy-cli +``` + ### Install a specific version To install a specific version of Git Proxy, append the version to the end of the install command: ```bash npm install -g @finos/git-proxy@1.1.0 -``` \ No newline at end of file +``` + +To install a specific version of the Git Proxy CLI, append the version to the end of the install command: + +```bash +npm install -g @finos/git-proxy-cli@1.0.0 +``` diff --git a/website/docs/quickstart/approve.mdx b/website/docs/quickstart/approve.mdx index 134295f93..99409a843 100644 --- a/website/docs/quickstart/approve.mdx +++ b/website/docs/quickstart/approve.mdx @@ -47,7 +47,7 @@ Using the [cookie](/docs/quickstart/approve#2-authenticate-with-the-api) generat curl -I -b ./git-proxy-cookie http://localhost:8080/api/v1/push/${ID} ``` -You should receive a `200 OK` in the response. +You should receive a `200 OK` in the response. If `ID` does not exist then you'll receive a `404` error. #### 4. Approve the push with `ID` @@ -73,6 +73,72 @@ Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (2/2), completed with 2 local objects. ``` +## Using the CLI + +### Prerequisites + +- Proxy and REST API are running ([default behaviour](https://github.com/finos/git-proxy/blob/main/index.js)) +- The Git Proxy URL is configured via the GIT_PROXY_UI_HOST (defaults to `http://localhost`) and GIT_PROXY_UI_PORT (defaults to `8080`) environment variables. Note: this documentation assumes that Git Proxy UI is running on `http://git-proxy.com:8080`. +- [Intercepting a push](/docs/quickstart/intercept) instructions have been followed and you've reached [Push via Git Proxy](/docs/quickstart/intercept#push-via-git-proxy) + +### Instructions + +#### 1. Find the tracking `ID` + +Following on from [Push via Git Proxy](/docs/quickstart/intercept#push-via-git-proxy), you'll receive a unique URL: + +``` +http://localhost:8080/requests/0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f +``` + +The `ID` for your push corresponds to the last part of the URL: + +``` +0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f +``` + +#### 2. Authenticate with the CLI + +Use the default & auto-generated Git Proxy username & password credentials to authenticate and obtain a cookie. The cookie value is saved to a file (`git-proxy-cookie`): + +```bash +$ npx -- @finos/git-proxy-cli login --username admin --password admin +Login "admin" (admin): OK +``` + +#### 3. Approve the push with `ID` + +Use the commit `ID` to approve your push with the CLI: + +```bash +$ npx -- @finos/git-proxy-cli authorise --id 0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f +Authorise: ID: '0000000000000000000000000000000000000000__79b4d8953cbc324bcc1eb53d6412ff89666c241f': OK +``` + +#### 4. Re-push your code + +Execute `git push` to send your approved code through Git Proxy to the upstream repository: + +```bash +$ git push +Enumerating objects: 5, done. +Counting objects: 100% (5/5), done. +Delta compression using up to 10 threads +Compressing objects: 100% (3/3), done. +Writing objects: 100% (3/3), 470 bytes | 470.00 KiB/s, done. +Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 +remote: Resolving deltas: 100% (2/2), completed with 2 local objects. +``` + +#### 5. Log out + +Clean up your connect cookie via logging out: + +```bash +$ npx -- @finos/git-proxy-cli logout +Logout: OK +``` + ## Using the UI :::note diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index 281deeb63..d222bc177 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -10,11 +10,19 @@ Once you have followed the [installation](installation) steps, run: git-proxy ``` +To run Git Proxy using the CLI +```bash +git-proxy-cli +``` + ### Using [npx instead of npm](https://www.freecodecamp.org/news/npm-vs-npx-whats-the-difference/) -You can also install & run `git-proxy` in one step: +You can also install & run `git-proxy` and `git-proxy-cli` in two steps: ```bash npx -- @finos/git-proxy ``` +```bash +npx -- @finos/git-proxy-cli +```