From 06b4c1dac202f6d48042ad72eaf830db10def0c9 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Sat, 16 Sep 2023 18:53:40 -0400 Subject: [PATCH] feat: runnable exec + CLI args via yargs + install docs - add "bin" to package.json to support running git-proxy as an executable. Largely taken from #184 and fixes #183 - added --config and --validate command line flags via yargs. These will allow custom user configurations with a default behaviour of loading from the current working dir and/or default settings. --validate uses JSON Schema to validate the config. - update docs to reflect new CLI args and additional details of running the app via npx - load default settings from a module instead of explicit file path - add test for default & user setting merging - bump @finos/git-proxy to 1.1.0 - Remove old X.509 certificate and private key, which I assume was included previously to run git-proxy with TLS enabled via a demo. Can be re-added as needed but probably shouldn't be included in src (even if its for demo only). --- README.md | 36 ++++++- config.schema.json | 79 ++++++++++++++ index.js | 45 ++++++++ package-lock.json | 19 +++- package.json | 7 +- resources/config.json => proxy.config.json | 0 resources/server.cert | 21 ---- resources/server.key | 28 ----- src/config/file.js | 14 +++ src/config/index.js | 39 ++++--- test/testConfig.js | 115 +++++++++++++++++++++ user-settings.json | 1 + 12 files changed, 326 insertions(+), 78 deletions(-) create mode 100644 config.schema.json mode change 100644 => 100755 index.js rename resources/config.json => proxy.config.json (100%) delete mode 100644 resources/server.cert delete mode 100644 resources/server.key create mode 100644 src/config/file.js create mode 100644 test/testConfig.js create mode 100644 user-settings.json diff --git a/README.md b/README.md index b83724f4b..86c02abcd 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,46 @@ Git Proxy is built with a developer-first mindset. By presenting simple-to-follo ## Installation -To install Git Proxy, use the [npm](https://www.npmjs.com/) package manager: +To install Git Proxy, you must have [Node.js 16 or later](https://nodejs.org/en/download) installed. Use [npm](https://www.npmjs.com) to install the package: ```bash -$ npm install @finos/git-proxy +$ npm install -g @finos/git-proxy ``` To install a specific version of Git Proxy, append the version to the end of the `install` command: ```bash -$ npm install @finos/git-proxy@1.0.0 +$ npm install -g @finos/git-proxy@1.1.0 +``` + +To start the server, run `git-proxy`. Alternatively, you can also install & run git-proxy directly using `npx`: + +```bash +$ git-proxy +# Running with npx - if the package isn't already installed, npx will prompt you to confirm installation +$ npx --package=@finos/git-proxy@1.1.0 -- git-proxy +``` + +## Configuration +By default, git-proxy ships with a [default configuration](./proxy.config.json) for demonstration purposes. In most environments, this should be overridden by your user-specific values. + +To set your own values, create a `proxy.config.json` in the current working directory. This will be loaded when you execute `git-proxy` if present. + +If you wish to specify a different file location to use as configuration, use the `-c/--config` command-line argument: + +```bash +$ git-proxy --config /etc/gitproxy/config.json +# With npx +$ npx -- @finos/git-proxy --config /etc/gitproxy/config.json +``` + +### Validation +To validate your configuration against the [included schema](config.schema.json), use the following included script: + +```bash +$ git-proxy --validate +# Run validation against a configuration at a custom file location +$ git-proxy --validate --config /etc/gitproxy/config.json ``` ## Contributing diff --git a/config.schema.json b/config.schema.json new file mode 100644 index 000000000..c1f2cc9fd --- /dev/null +++ b/config.schema.json @@ -0,0 +1,79 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git-proxy.finos.org/config.schema.json", + "title": "Git Proxy", + "description": "Configuration file for modifying the behaviour of git-proxy", + "type": "object", + "properties": { + "authorisedList": { + "description": "List of repositories that are authorised to be pushed to through the proxy.", + "type": "array", + "items": { + "$ref": "#/$defs/authorisedRepo" + } + }, + "sink": { + "description": "List of database sources. The first source in the configuration with enabled=true will be used.", + "type": "array", + "items": { + "$ref": "#/$defs/database" + } + }, + "authentication": { + "description": "List of authentication sources. The first source in the configuration with enabled=true will be used.", + "type": "array", + "items": { + "$ref": "#/$defs/authentication" + } + }, + "tempPassword": { + "description": "Toggle the generation of temporary password for git-proxy admin user", + "type": "object", + "properties": { + "sendEmail": { "type": "boolean" }, + "emailConfig": { + "description": "Generic object to configure nodemailer. For full type information, please see https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nodemailer", + "type": "object" + } + } + } + }, + "additionalProperties": false, + "anyOf": [ + { "required": "authorisedList" }, + { "required": "sink" }, + { "required": "authentication" }, + { "required": "tempPassword" } + ], + "$defs": { + "authorisedRepo": { + "type": "object", + "properties": { + "project": { "type": "string" }, + "name": { "type": "string" }, + "url": { "type": "string" } + }, + "required": [ "project", "name", "url" ] + }, + "database": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "enabled": { "type": "boolean" }, + "connectionString": { "type": "string" }, + "options": { "type": "object" }, + "params": { "type": "object" } + }, + "required": [ "type", "enabled" ] + }, + "authentication": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "enabled": { "type": "boolean" }, + "options": { "type": "object" } + }, + "required": [ "type", "enabled" ] + } + } +} \ No newline at end of file diff --git a/index.js b/index.js old mode 100644 new mode 100755 index 51d572850..6588bd626 --- a/index.js +++ b/index.js @@ -1,3 +1,48 @@ +#!/usr/bin/env node +/* eslint-disable max-len */ +const argv = require('yargs/yargs')(process.argv.slice(2)) + .usage('Usage: $0 [options]') + .options({ + validate: { + description: + 'Check the proxy.config.json file in the current working directory for validation errors.', + required: false, + alias: 'v', + }, + config: { + description: 'Path to custom git-proxy configuration file.', + default: 'proxy.config.json', + required: false, + alias: 'c', + }, + }).argv; + +require('./src/config/file').configFile = argv.c ? argv.c : undefined; + +if (argv.v) { + const fs = require('fs'); + const path = require('path'); + const validate = require('jsonschema').validate; + const configFile = require('./src/config/file').configFile; + + if (!fs.existsSync(configFile)) { + console.error( + `Config file ${configFile} doesn't exist, nothing to validate! Did you forget -c/--config?`, + ); + process.exit(1); + } + + const config = JSON.parse(fs.readFileSync(configFile)); + const schema = JSON.parse( + fs.readFileSync(path.join(__dirname, 'config.schema.json')), + ); + + validate(config, schema, { required: true, throwError: true }); + + console.log(`${configFile} is valid`); + process.exit(0); +} + const proxy = require('./src/proxy'); const service = require('./src/service'); diff --git a/package-lock.json b/package-lock.json index 17031e62f..6e4d2fb8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@finos/git-proxy", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@finos/git-proxy", - "version": "1.0.0", + "version": "1.1.0", "license": "Apache-2.0", "dependencies": { "@material-ui/core": "^4.11.0", @@ -27,6 +27,7 @@ "express-session": "^1.17.1", "generate-password": "^1.5.1", "history": "5.3.0", + "jsonschema": "^1.4.1", "load-plugin": "^5.1.0", "lodash": "^4.17.21", "moment": "^2.29.4", @@ -42,7 +43,11 @@ "react-dom": "^16.13.1", "react-html-parser": "^2.0.2", "react-router-dom": "6.16.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "git-proxy": "index.js" }, "devDependencies": { "@babel/eslint-parser": "^7.22.9", @@ -5995,6 +6000,14 @@ "node >= 0.2.0" ] }, + "node_modules/jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "engines": { + "node": "*" + } + }, "node_modules/JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", diff --git a/package.json b/package.json index 0ce9f2efb..164fd7549 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@finos/git-proxy", - "version": "1.0.0", + "version": "1.1.0", "description": "Deploy custom push protections and policies on top of Git.", "scripts": { "client": "vite --config vite.config.js", @@ -15,6 +15,7 @@ "prepare": "node ./scripts/prepare.js", "lint": "eslint --fix . --ext .js,.jsx" }, + "bin": "./index.js", "author": "Paul Groves", "license": "Apache-2.0", "dependencies": { @@ -37,6 +38,7 @@ "generate-password": "^1.5.1", "history": "5.3.0", "load-plugin": "^5.1.0", + "jsonschema": "^1.4.1", "lodash": "^4.17.21", "moment": "^2.29.4", "mongodb": "^5.0.0", @@ -51,7 +53,8 @@ "react-dom": "^16.13.1", "react-html-parser": "^2.0.2", "react-router-dom": "6.16.0", - "uuid": "^9.0.0" + "uuid": "^9.0.0", + "yargs": "^17.7.2" }, "devDependencies": { "@babel/eslint-parser": "^7.22.9", diff --git a/resources/config.json b/proxy.config.json similarity index 100% rename from resources/config.json rename to proxy.config.json diff --git a/resources/server.cert b/resources/server.cert deleted file mode 100644 index aeb2d089c..000000000 --- a/resources/server.cert +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDgzCCAmugAwIBAgIUSUNCW7+UIzWMqSsBswEdWxplmYYwDQYJKoZIhvcNAQEL -BQAwUTELMAkGA1UEBhMCdWsxDjAMBgNVBAgMBWJ1Y2tzMQ8wDQYDVQQHDAZtYXJs -b3cxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA0MTcx -NTEyMjhaFw0yMDA1MTcxNTEyMjhaMFExCzAJBgNVBAYTAnVrMQ4wDAYDVQQIDAVi -dWNrczEPMA0GA1UEBwwGbWFybG93MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz -IFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/eVtu1NCJ -7gHa+OFopgvkm7PDGC2ShU7bbCJeXfv05aIY+Qgygm5dSRozUaeH4C+v8MgAnPnV -yCeK2P2CP4O1Kmw+GEEr0BJTLzzHOgigLJ5POkj/u70skPFz5UoF/i4kimVO1jS/ -jjZEihmssz9WAMg6XoYsNDLCUOETYL0KdPSFrN8Y7Qtwr/l53+1blVlPgrkTDJCz -zNcZqQWbW/wPEnTcuK/XT4jHX8h0trS5pKf6G3xP0rfnHcXJFdzIh5eVbg0svL0C -MnP0EyB+EeXDrda0/mxXYcmWx3nUqiJCJExR2+UeB2R/zwUFXAag7AMJh6BkK823 -bCs4WCViYMlTAgMBAAGjUzBRMB0GA1UdDgQWBBRHUkx9G3VBXY6CausRfrUG5dDs -uzAfBgNVHSMEGDAWgBRHUkx9G3VBXY6CausRfrUG5dDsuzAPBgNVHRMBAf8EBTAD -AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBpV25jdW4bgRRfux5eian/Etz02ZkLBC78 -mat1slV5GekzwiGdp6GkEYMsodx7mJOr1zDRSf5LBn+XeCeTCyS5wmTiQ9PqlDKI -ZoeLy3iSM7sain52BmVPZR9UcO+Ja+4f1LITnVnTHaalZR1Abb7nrtIO7hOeX8z4 -TOiQ1RExGFacSB/jIX3V62o8yOO2c48oFF6Ybjfu4ellmy1g0cygk5hhwoq4OtOK -gNMM+ThD862ox54hEsZLZQu1PpfRFWamgiJTAZG1Pw9uZLz7lja0mhmDWMpP6FDT -zaT6J3N4J97kMSCRLVUnmz9JNvkp26otxie67/91psEZnZA70BwP ------END CERTIFICATE----- \ No newline at end of file diff --git a/resources/server.key b/resources/server.key deleted file mode 100644 index b706c2a02..000000000 --- a/resources/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC/eVtu1NCJ7gHa -+OFopgvkm7PDGC2ShU7bbCJeXfv05aIY+Qgygm5dSRozUaeH4C+v8MgAnPnVyCeK -2P2CP4O1Kmw+GEEr0BJTLzzHOgigLJ5POkj/u70skPFz5UoF/i4kimVO1jS/jjZE -ihmssz9WAMg6XoYsNDLCUOETYL0KdPSFrN8Y7Qtwr/l53+1blVlPgrkTDJCzzNcZ -qQWbW/wPEnTcuK/XT4jHX8h0trS5pKf6G3xP0rfnHcXJFdzIh5eVbg0svL0CMnP0 -EyB+EeXDrda0/mxXYcmWx3nUqiJCJExR2+UeB2R/zwUFXAag7AMJh6BkK823bCs4 -WCViYMlTAgMBAAECggEBAIXN4s1SvuCEiJtjLPIah1kcTcTaUo5/xhmkOWhEuVvs -VRiqfsX9S+64tSyDtVVIn0qOMtXq3NQ+aROoi4/HntytZrMF9BUP9J5Y9loq/fgg -9ghbrMO5iHtqVrEs3EvP0qMKa71qB7aNRPMkpsh/ApWxOjs/7vdZCter+X23LqPs -3Kq8bQtiOvXCUQyORHOEWbdiAMvCrpo7EfqKkFGP0xaau4+kSmRZKlnaA78Hd7am -YX+XaPnu3QTjmlHWAIgbc5zkJS0pwX0xN2qnQ02RLV9zxCdXu6x5W1VocCme+BNS -9Zae2aDLgVeYy4KrAYvWIzb1HXOk6l5hLjAS4IWRH3ECgYEA8JGwMVPDYHzjKRT+ -X4a5a7GleJjSddsuiNdvk6Tx252PLJyFWmbaWeom9OyKhZcre7F+5Ms4MgGaOI3B -vrD+hEsNwZYR2tLS3wsFhhLLTth5rB4xFpDkvztUJe3tqFah3pyD8jos+z/X6v20 -62ArJkegzvzQVNEc3JbpZKolyo0CgYEAy8F+ggO6OnPr5liFNJvfYDJ20BdKg67D -mXRRG3/X+/wM71DH1Mr7GUVS3ZwqfeqHy4OIrY53ABcUlo8Ba6emnPyilAwDiJZG -57ILytgYNiro10Xlf+BUakX2+BVhz93UxZfOBouvYV+RqeQcMxr35K12r153sHdB -QFYN5rdk218CgYB8x4R1QXZAsOZ+o5YBZHb+pikm8VWQrfxoHB6SnWaZvBLMV+9P -YbP2GV7VgW+kNTHnubwQ3luqjGw600RgLZwGcIuVEsr2Do40BJp73Xm4zs3lec+K -XeNYUWSnO88elrjlJ5fE52n3dDkBeVEDGWGoPFTrp/RDWie3P0uV3C837QKBgQCi -H3m7lZ+uNuJyy+hhbc0Uy9KBzKZ7lKkKBuUqTlTaqTjZipsWE9QrzV8b+dBNlDks -k6JDBmJlbffxvCPTNvh5XQM3bT+6hGgynxaG9d596zKNZ44ua55/WOAjkU/ch5Nv -DVTfHHIVtmc+mMRfXYv1JpiS/UWa4ajHujEhbLcRXQKBgQDdOSGv3/JBk3rkOrKX -HdA3h7Ao2SiyaovOzKHbcG46IURpwrZFKQ7THFmeXellpOKX0qSiwKWb7o+m2pmZ -8G8op2k3a4vxZYhCzJKY2L3kFQ0EBbxrIdD77PWXf2EyABQfsu+bQ+JKdqUNagui -qLTyi7E5Lp0ZqsGMHe6IiSvWxg== ------END PRIVATE KEY----- \ No newline at end of file diff --git a/src/config/file.js b/src/config/file.js new file mode 100644 index 000000000..fd1f284d0 --- /dev/null +++ b/src/config/file.js @@ -0,0 +1,14 @@ +const path = require('path'); +// eslint-disable-next-line prefer-const +let configFile = undefined; + +module.exports = { + get configFile() { + return configFile + ? configFile + : path.join(process.cwd(), 'proxy.config.json'); + }, + set configFile(file) { + configFile = file; + }, +}; diff --git a/src/config/index.js b/src/config/index.js index b39bbd0e4..e94f38c93 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,24 +1,21 @@ const fs = require('fs'); -const proxySettings = JSON.parse(fs.readFileSync('./resources/config.json')); -let _userSettings = null; -let _authorisedList = proxySettings.authorisedList; -let _database = proxySettings.sink; -let _authentication = proxySettings.authentication; -let _tempPassword = proxySettings.tempPassword; +const defaultSettings = require('../../proxy.config.json'); +const userSettingsPath = require('./file').configFile; -const userSettings = () => { - const path = './user-settings.json'; - if (_userSettings === null && fs.existsSync(path)) { - _userSettings = JSON.parse(fs.readFileSync(path)); - } - return _userSettings; -}; +let _userSettings = null; +if (fs.existsSync(userSettingsPath)) { + _userSettings = JSON.parse(fs.readFileSync(userSettingsPath)); +} +let _authorisedList = defaultSettings.authorisedList; +let _database = defaultSettings.sink; +let _authentication = defaultSettings.authentication; +let _tempPassword = defaultSettings.tempPassword; // Gets a list of authorised repositories const getAuthorisedList = () => { - if (userSettings !== null && userSettings.authorisedList) { - _authorisedList = userSettings.authorisedList; + if (_userSettings !== null && _userSettings.authorisedList) { + _authorisedList = _userSettings.authorisedList; } return _authorisedList; @@ -26,8 +23,8 @@ const getAuthorisedList = () => { // Gets a list of authorised repositories const getTempPasswordConfig = () => { - if (userSettings !== null && userSettings.tempPassword) { - _tempPassword = userSettings.tempPassword; + if (_userSettings !== null && _userSettings.tempPassword) { + _tempPassword = _userSettings.tempPassword; } return _tempPassword; @@ -35,8 +32,8 @@ const getTempPasswordConfig = () => { // Gets the configuared data sink, defaults to filesystem const getDatabase = () => { - if (userSettings !== null && userSettings.sink) { - _database = userSettings.database; + if (_userSettings !== null && _userSettings.sink) { + _database = _userSettings.sink; } for (const ix in _database) { if (ix) { @@ -52,8 +49,8 @@ const getDatabase = () => { // Gets the configuared data sink, defaults to filesystem const getAuthentication = () => { - if (userSettings !== null && userSettings.sink) { - _authentication = userSettings.authentication; + if (_userSettings !== null && _userSettings.authentication) { + _authentication = _userSettings.authentication; } for (const ix in _authentication) { if (!ix) continue; diff --git a/test/testConfig.js b/test/testConfig.js new file mode 100644 index 000000000..d0afb0618 --- /dev/null +++ b/test/testConfig.js @@ -0,0 +1,115 @@ +/* eslint-disable max-len */ +const chai = require('chai'); +const fs = require('fs'); +const path = require('path'); +const defaultSettings = require('../proxy.config.json'); + +chai.should(); +const expect = chai.expect; + +describe('default configuration', function () { + it('should use default values if no user-settings.json file exists', function () { + const config = require('../src/config'); + + expect(config.getAuthentication()).to.be.eql( + defaultSettings.authentication[0], + ); + 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, + ); + }); + after(function () { + delete require.cache[require.resolve('../src/config')]; + }); +}); + +describe('user configuration', function () { + let tempDir; + let tempUserFile; + + beforeEach(function () { + tempDir = fs.mkdtempSync('gitproxy-test'); + tempUserFile = path.join(tempDir, 'test-settings.json'); + require('../src/config/file').configFile = tempUserFile; + }); + + it('should override default settings for authorisedList', function () { + const user = { + authorisedList: [ + { + project: 'foo', + name: 'bar', + url: 'https://github.com/foo/bar.git', + }, + ], + }; + fs.writeFileSync(tempUserFile, JSON.stringify(user)); + + const config = require('../src/config'); + + expect(config.getAuthorisedList()).to.be.eql(user.authorisedList); + expect(config.getAuthentication()).to.be.eql( + defaultSettings.authentication[0], + ); + expect(config.getDatabase()).to.be.eql(defaultSettings.sink[0]); + expect(config.getTempPasswordConfig()).to.be.eql( + defaultSettings.tempPassword, + ); + }); + + it('should override default settings for authentication', function () { + const user = { + authentication: [ + { + type: 'google', + enabled: true, + }, + ], + }; + fs.writeFileSync(tempUserFile, JSON.stringify(user)); + + const config = require('../src/config'); + + expect(config.getAuthentication()).to.be.eql(user.authentication[0]); + expect(config.getAuthentication()).to.not.be.eql( + defaultSettings.authentication[0], + ); + expect(config.getDatabase()).to.be.eql(defaultSettings.sink[0]); + expect(config.getTempPasswordConfig()).to.be.eql( + defaultSettings.tempPassword, + ); + }); + + it('should override default settings for database', function () { + const user = { + sink: [ + { + type: 'postgres', + enabled: true, + }, + ], + }; + fs.writeFileSync(tempUserFile, JSON.stringify(user)); + + const config = require('../src/config'); + + 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.getTempPasswordConfig()).to.be.eql( + defaultSettings.tempPassword, + ); + }); + + afterEach(function () { + fs.rmSync(tempUserFile); + fs.rmdirSync(tempDir); + delete require.cache[require.resolve('../src/config')]; + }); +}); diff --git a/user-settings.json b/user-settings.json new file mode 100644 index 000000000..1d958b6c1 --- /dev/null +++ b/user-settings.json @@ -0,0 +1 @@ +{ "authorisedList": [ { "project": "foo", "name": "bar", "url": "https://github.com/foo/bar.git" } ] }