From 9ae58e89ba9a4647ec0e8ab7bcc6b080da01bf7b Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 6 Mar 2025 18:21:58 +0100 Subject: [PATCH 01/16] feat: add autoapproved field in Action --- src/proxy/actions/Action.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/proxy/actions/Action.js b/src/proxy/actions/Action.js index 50b0e8fa8..9c51d2ee0 100644 --- a/src/proxy/actions/Action.js +++ b/src/proxy/actions/Action.js @@ -14,6 +14,7 @@ class Action { authorised = false; canceled = false; rejected = false; + autoApproved = false; commitFrom; commitTo; branch; @@ -104,6 +105,13 @@ class Action { this.blocked = false; } + /** + *` + */ + setAllowAutoApprover() { + this.autoApproved = true; + } + /** * @return {bool} */ From cedeff608a5c6711768d7989b1583d440ad8cefe Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 6 Mar 2025 18:22:22 +0100 Subject: [PATCH 02/16] feat: handle different exit codes in pre receive --- .../processors/push-action/preReceive.js | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/proxy/processors/push-action/preReceive.js b/src/proxy/processors/push-action/preReceive.js index 9eed889da..4135f52aa 100644 --- a/src/proxy/processors/push-action/preReceive.js +++ b/src/proxy/processors/push-action/preReceive.js @@ -37,17 +37,31 @@ const exec = async (req, action, hookFilePath = './hooks/pre-receive.sh') => { const stderrTrimmed = stderr ? stderr.trim() : ''; const stdoutTrimmed = stdout ? stdout.trim() : ''; - if (status !== 0) { + step.log(`Hook exited with status ${status}`); + + if (status === 1) { + step.log('Push requires manual approval.'); + action.addStep(step); + return action; + } else if (status === 2) { step.error = true; + step.log('Push rejected by pre-receive hook.'); step.log(`Hook stderr: ${stderrTrimmed}`); - step.setError(stdoutTrimmed); + step.setError(stdoutTrimmed || 'Pre-receive hook rejected the push.'); + action.addStep(step); + return action; + } else if (status === 0) { + step.log('Push automatically approved by pre-receive hook.'); + action.addStep(step); + action.setAllowAutoApprover(); + return action; + } else { + step.error = true; + step.log(`Unexpected hook status: ${status}`); + step.setError(stdoutTrimmed || 'Unknown pre-receive hook error.'); action.addStep(step); return action; } - - step.log('Pre-receive hook executed successfully'); - action.addStep(step); - return action; } catch (error) { step.error = true; step.setError(`Hook execution error: ${error.message}`); From af74f3c59b8b46c0fcacb61106153f0c70b2025b Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 6 Mar 2025 18:22:49 +0100 Subject: [PATCH 03/16] feat: push if autoApproved=true --- src/proxy/chain.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/proxy/chain.js b/src/proxy/chain.js index 7099fdd8f..0a6e73285 100644 --- a/src/proxy/chain.js +++ b/src/proxy/chain.js @@ -1,4 +1,5 @@ const proc = require('./processors'); +const db = require('../db'); const pushActionChain = [ proc.push.parsePush, @@ -20,6 +21,22 @@ const pullActionChain = [proc.push.checkRepoInAuthorisedList]; let pluginsInserted = false; +const attemptAutoApproval = async (req, action) => { + try { + const attestation = { + timestamp: new Date(), + autoApproved: true, + }; + await db.authorise(action.id, attestation); + console.log('Push automatically approved by system.'); + + return true; + } catch (error) { + console.error('Error during auto-approval:', error.message); + return false; + } +}; + const executeChain = async (req) => { let action; try { @@ -40,6 +57,9 @@ const executeChain = async (req) => { } } finally { await proc.push.audit(req, action); + if (action.autoApproved) { + attemptAutoApproval(req, action); + } } return action; From f672318cec78cd30451a632a509726ec53d8bc8e Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 6 Mar 2025 18:23:27 +0100 Subject: [PATCH 04/16] feat: change ui to display reviewer just if it is not auto approved --- src/ui/views/PushDetails/PushDetails.jsx | 92 +++++++++++++++--------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/src/ui/views/PushDetails/PushDetails.jsx b/src/ui/views/PushDetails/PushDetails.jsx index 6f02d717b..ee493afdb 100644 --- a/src/ui/views/PushDetails/PushDetails.jsx +++ b/src/ui/views/PushDetails/PushDetails.jsx @@ -173,45 +173,67 @@ export default function Dashboard() { }} > setAttestation(true)} + style={{ + cursor: data.autoApproved ? 'default' : 'pointer', + transform: 'scale(0.65)', + opacity: data.autoApproved ? 0.5 : 1, + }} + onClick={() => { + if (!data.autoApproved) { + setAttestation(true); + } + }} htmlColor='green' /> - - - -
-

+ + {data.autoApproved ? ( + <> +

+

+ Auto-approved by system +

+
+ + ) : ( + <> - {data.attestation.reviewer.gitAccount} - {' '} - approved this contribution -

- - - {moment(data.attestation.timestamp).fromNow()} - - -
- + + +
+

+ + {data.attestation.reviewer.gitAccount} + {' '} + approved this contribution +

+
+ + )} + + + + {moment(data.attestation.timestamp).fromNow()} + + + + {data.autoApproved ? ( + <> + ) : ( + + )} ) : null} From ecf234369a3ef172b6f3ee447f76e803d9b50c4c Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 13 Mar 2025 12:44:03 +0100 Subject: [PATCH 05/16] feat(test): edit tests for preReceive in order to handle auto approval --- test/chain.test.js | 49 +++++++++++++++++++ .../pre-receive-hooks/always-exit-1.sh | 5 ++ .../pre-receive-hooks/always-reject.sh | 2 +- test/preReceive/preReceive.test.js | 31 ++++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100755 test/preReceive/pre-receive-hooks/always-exit-1.sh mode change 100644 => 100755 test/preReceive/pre-receive-hooks/always-reject.sh diff --git a/test/chain.test.js b/test/chain.test.js index df4147ab7..58c408fb6 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -1,6 +1,7 @@ const chai = require('chai'); const sinon = require('sinon'); const { PluginLoader } = require('../src/plugin'); +const db = require('../src/db'); chai.should(); const expect = chai.expect; @@ -245,4 +246,52 @@ describe('proxy chain', function () { expect(mockPushProcessors.parsePush.called).to.be.false; expect(result).to.deep.equal(action); }); + + it('should approve push automatically and record in the database', async function () { + const req = {}; + const action = { + type: 'push', + continue: () => true, + allowPush: false, + setAllowAutoApprover: sinon.stub(), + repoName: 'test-repo', + commitTo: 'newCommitHash', + autoApproved: false, + }; + + mockPreProcessors.parseAction.resolves(action); + mockPushProcessors.parsePush.resolves(action); + mockPushProcessors.checkRepoInAuthorisedList.resolves(action); + mockPushProcessors.checkCommitMessages.resolves(action); + mockPushProcessors.checkAuthorEmails.resolves(action); + mockPushProcessors.checkUserPushPermission.resolves(action); + mockPushProcessors.checkIfWaitingAuth.resolves(action); + mockPushProcessors.pullRemote.resolves(action); + mockPushProcessors.writePack.resolves(action); + + mockPushProcessors.preReceive.resolves({ + ...action, + steps: [{ error: false, logs: ['Push automatically approved by pre-receive hook.'] }], + allowPush: true, + autoApproved: true, + }); + + mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.clearBareClone.resolves(action); + mockPushProcessors.scanDiff.resolves(action); + mockPushProcessors.blockForAuth.resolves(action); + + const dbStub = sinon.stub(db, 'authorise').resolves(true); + + const result = await chain.executeChain(req); + + expect(result.type).to.equal('push'); + expect(result.allowPush).to.be.true; + expect(result.continue).to.be.a('function'); + + expect(dbStub.calledOnce).to.be.true; + expect(dbStub.calledWith(action.id, sinon.match({ autoApproved: true }))).to.be.true; + + dbStub.restore(); + }); }); diff --git a/test/preReceive/pre-receive-hooks/always-exit-1.sh b/test/preReceive/pre-receive-hooks/always-exit-1.sh new file mode 100755 index 000000000..dad59ad99 --- /dev/null +++ b/test/preReceive/pre-receive-hooks/always-exit-1.sh @@ -0,0 +1,5 @@ +#!/bin/bash +while read oldrev newrev refname; do + echo "Push need manual approve to $refname" +done +exit 1 \ No newline at end of file diff --git a/test/preReceive/pre-receive-hooks/always-reject.sh b/test/preReceive/pre-receive-hooks/always-reject.sh old mode 100644 new mode 100755 index d6099cc57..6a5e414e0 --- a/test/preReceive/pre-receive-hooks/always-reject.sh +++ b/test/preReceive/pre-receive-hooks/always-reject.sh @@ -2,4 +2,4 @@ while read oldrev newrev refname; do echo "Push rejected to $refname" done -exit 1 \ No newline at end of file +exit 2 \ No newline at end of file diff --git a/test/preReceive/preReceive.test.js b/test/preReceive/preReceive.test.js index c7e14e7e5..eada1a3f1 100644 --- a/test/preReceive/preReceive.test.js +++ b/test/preReceive/preReceive.test.js @@ -19,6 +19,7 @@ describe('Pre-Receive Hook Execution', function () { addStep: function (step) { this.steps.push(step); }, + setAllowAutoApprover: sinon.stub(), }; }); @@ -26,16 +27,16 @@ describe('Pre-Receive Hook Execution', function () { sinon.restore(); }); - it('should execute hook successfully', async () => { - const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-allow.sh'); + it('should execute hook successfully and require manual approval', async () => { + const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-exit-1.sh'); const result = await exec(req, action, scriptPath); expect(result.steps).to.have.lengthOf(1); expect(result.steps[0].error).to.be.false; - expect( - result.steps[0].logs.some((log) => log.includes('Pre-receive hook executed successfully')), - ).to.be.true; + expect(result.steps[0].logs.some((log) => log.includes('Push requires manual approval.'))).to.be + .true; + expect(action.setAllowAutoApprover.called).to.be.false; }); it('should skip execution when hook file does not exist', async () => { @@ -50,6 +51,7 @@ describe('Pre-Receive Hook Execution', function () { log.includes('Pre-receive hook not found, skipping execution.'), ), ).to.be.true; + expect(action.setAllowAutoApprover.called).to.be.false; }); it('should skip execution when hook directory does not exist', async () => { @@ -64,6 +66,7 @@ describe('Pre-Receive Hook Execution', function () { log.includes('Pre-receive hook not found, skipping execution.'), ), ).to.be.true; + expect(action.setAllowAutoApprover.called).to.be.false; }); it('should fail when hook execution returns an error', async () => { @@ -76,11 +79,13 @@ describe('Pre-Receive Hook Execution', function () { const step = result.steps[0]; expect(step.error).to.be.true; + expect(step.logs.some((log) => log.includes('Push rejected by pre-receive hook.'))).to.be.true; expect(step.logs.some((log) => log.includes('Hook stderr:'))).to.be.true; expect(step.errorMessage).to.exist; expect(action.steps).to.deep.include(step); + expect(action.setAllowAutoApprover.called).to.be.false; }); it('should catch and handle unexpected errors', async () => { @@ -95,5 +100,21 @@ describe('Pre-Receive Hook Execution', function () { expect( result.steps[0].logs.some((log) => log.includes('Hook execution error: Unexpected FS error')), ).to.be.true; + expect(action.setAllowAutoApprover.called).to.be.false; + }); + + it('should approve push automatically when hook returns status 0', async () => { + const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-allow.sh'); + + const result = await exec(req, action, scriptPath); + + expect(result.steps).to.have.lengthOf(1); + expect(result.steps[0].error).to.be.false; + expect( + result.steps[0].logs.some((log) => + log.includes('Push automatically approved by pre-receive hook.'), + ), + ).to.be.true; + expect(action.setAllowAutoApprover.calledOnce).to.be.true; }); }); From c104121a2d145d04cd46e88b1d537647b3de99f4 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Mon, 24 Mar 2025 15:38:56 +0100 Subject: [PATCH 06/16] refactor: rename hooks files for testing --- .../pre-receive-hooks/{always-allow.sh => always-exit-0.sh} | 0 test/preReceive/pre-receive-hooks/always-exit-1.sh | 2 +- .../pre-receive-hooks/{always-reject.sh => always-exit-2.sh} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename test/preReceive/pre-receive-hooks/{always-allow.sh => always-exit-0.sh} (100%) rename test/preReceive/pre-receive-hooks/{always-reject.sh => always-exit-2.sh} (56%) diff --git a/test/preReceive/pre-receive-hooks/always-allow.sh b/test/preReceive/pre-receive-hooks/always-exit-0.sh similarity index 100% rename from test/preReceive/pre-receive-hooks/always-allow.sh rename to test/preReceive/pre-receive-hooks/always-exit-0.sh diff --git a/test/preReceive/pre-receive-hooks/always-exit-1.sh b/test/preReceive/pre-receive-hooks/always-exit-1.sh index dad59ad99..d6099cc57 100755 --- a/test/preReceive/pre-receive-hooks/always-exit-1.sh +++ b/test/preReceive/pre-receive-hooks/always-exit-1.sh @@ -1,5 +1,5 @@ #!/bin/bash while read oldrev newrev refname; do - echo "Push need manual approve to $refname" + echo "Push rejected to $refname" done exit 1 \ No newline at end of file diff --git a/test/preReceive/pre-receive-hooks/always-reject.sh b/test/preReceive/pre-receive-hooks/always-exit-2.sh similarity index 56% rename from test/preReceive/pre-receive-hooks/always-reject.sh rename to test/preReceive/pre-receive-hooks/always-exit-2.sh index 6a5e414e0..35387347f 100755 --- a/test/preReceive/pre-receive-hooks/always-reject.sh +++ b/test/preReceive/pre-receive-hooks/always-exit-2.sh @@ -1,5 +1,5 @@ #!/bin/bash while read oldrev newrev refname; do - echo "Push rejected to $refname" + echo "Push need manual approve to $refname" done exit 2 \ No newline at end of file From e02862442063bf4a1e1aa76dac07fe8b9169575e Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Mon, 24 Mar 2025 15:39:16 +0100 Subject: [PATCH 07/16] feat: add auto rejection --- src/proxy/actions/Action.js | 9 +++++- .../processors/push-action/preReceive.js | 29 +++++++++---------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/proxy/actions/Action.js b/src/proxy/actions/Action.js index 9c51d2ee0..ba34f2938 100644 --- a/src/proxy/actions/Action.js +++ b/src/proxy/actions/Action.js @@ -15,6 +15,7 @@ class Action { canceled = false; rejected = false; autoApproved = false; + autoRejected = false; commitFrom; commitTo; branch; @@ -108,10 +109,16 @@ class Action { /** *` */ - setAllowAutoApprover() { + setAutoApproval() { this.autoApproved = true; } + /** + *` + */ + setAutoRejection() { + this.autoRejected = true; + } /** * @return {bool} */ diff --git a/src/proxy/processors/push-action/preReceive.js b/src/proxy/processors/push-action/preReceive.js index 4135f52aa..8634a6f0b 100644 --- a/src/proxy/processors/push-action/preReceive.js +++ b/src/proxy/processors/push-action/preReceive.js @@ -9,6 +9,7 @@ const sanitizeInput = (_req, action) => { const exec = async (req, action, hookFilePath = './hooks/pre-receive.sh') => { const step = new Step('executeExternalPreReceiveHook'); + let stderrTrimmed = ''; try { const resolvedPath = path.resolve(hookFilePath); @@ -34,37 +35,33 @@ const exec = async (req, action, hookFilePath = './hooks/pre-receive.sh') => { const { stdout, stderr, status } = hookProcess; - const stderrTrimmed = stderr ? stderr.trim() : ''; + stderrTrimmed = stderr ? stderr.trim() : ''; const stdoutTrimmed = stdout ? stdout.trim() : ''; step.log(`Hook exited with status ${status}`); - if (status === 1) { - step.log('Push requires manual approval.'); + if (status === 0) { + step.log('Push automatically approved by pre-receive hook.'); action.addStep(step); - return action; - } else if (status === 2) { - step.error = true; - step.log('Push rejected by pre-receive hook.'); - step.log(`Hook stderr: ${stderrTrimmed}`); - step.setError(stdoutTrimmed || 'Pre-receive hook rejected the push.'); + action.setAutoApproval(); + } else if (status === 1) { + step.log('Push automatically rejected by pre-receive hook.'); action.addStep(step); - return action; - } else if (status === 0) { - step.log('Push automatically approved by pre-receive hook.'); + action.setAutoRejection(); + } else if (status === 2) { + step.log('Push requires manual approval.'); action.addStep(step); - action.setAllowAutoApprover(); - return action; } else { step.error = true; step.log(`Unexpected hook status: ${status}`); step.setError(stdoutTrimmed || 'Unknown pre-receive hook error.'); action.addStep(step); - return action; } + return action; } catch (error) { step.error = true; - step.setError(`Hook execution error: ${error.message}`); + step.log('Push failed, pre-receive hook returned an error.'); + step.setError(`Hook execution error: ${stderrTrimmed || error.message}`); action.addStep(step); return action; } From 6b6653e1b9b7441ff43c6febd39262c41ba23ae4 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Mon, 24 Mar 2025 15:39:41 +0100 Subject: [PATCH 08/16] feat: test auto rejection --- test/chain.test.js | 2 +- test/preReceive/preReceive.test.js | 79 ++++++++++++++++-------------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/test/chain.test.js b/test/chain.test.js index 58c408fb6..da5ebb59f 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -253,7 +253,7 @@ describe('proxy chain', function () { type: 'push', continue: () => true, allowPush: false, - setAllowAutoApprover: sinon.stub(), + setAutoApproval: sinon.stub(), repoName: 'test-repo', commitTo: 'newCommitHash', autoApproved: false, diff --git a/test/preReceive/preReceive.test.js b/test/preReceive/preReceive.test.js index eada1a3f1..42d13b31e 100644 --- a/test/preReceive/preReceive.test.js +++ b/test/preReceive/preReceive.test.js @@ -19,7 +19,8 @@ describe('Pre-Receive Hook Execution', function () { addStep: function (step) { this.steps.push(step); }, - setAllowAutoApprover: sinon.stub(), + setAutoApproval: sinon.stub(), + setAutoRejection: sinon.stub(), }; }); @@ -27,18 +28,6 @@ describe('Pre-Receive Hook Execution', function () { sinon.restore(); }); - it('should execute hook successfully and require manual approval', async () => { - const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-exit-1.sh'); - - const result = await exec(req, action, scriptPath); - - expect(result.steps).to.have.lengthOf(1); - expect(result.steps[0].error).to.be.false; - expect(result.steps[0].logs.some((log) => log.includes('Push requires manual approval.'))).to.be - .true; - expect(action.setAllowAutoApprover.called).to.be.false; - }); - it('should skip execution when hook file does not exist', async () => { const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/missing-hook.sh'); @@ -51,7 +40,8 @@ describe('Pre-Receive Hook Execution', function () { log.includes('Pre-receive hook not found, skipping execution.'), ), ).to.be.true; - expect(action.setAllowAutoApprover.called).to.be.false; + expect(action.setAutoApproval.called).to.be.false; + expect(action.setAutoRejection.called).to.be.false; }); it('should skip execution when hook directory does not exist', async () => { @@ -66,30 +56,12 @@ describe('Pre-Receive Hook Execution', function () { log.includes('Pre-receive hook not found, skipping execution.'), ), ).to.be.true; - expect(action.setAllowAutoApprover.called).to.be.false; - }); - - it('should fail when hook execution returns an error', async () => { - const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-reject.sh'); - - const result = await exec(req, action, scriptPath); - - expect(result.steps).to.have.lengthOf(1); - - const step = result.steps[0]; - - expect(step.error).to.be.true; - expect(step.logs.some((log) => log.includes('Push rejected by pre-receive hook.'))).to.be.true; - expect(step.logs.some((log) => log.includes('Hook stderr:'))).to.be.true; - - expect(step.errorMessage).to.exist; - - expect(action.steps).to.deep.include(step); - expect(action.setAllowAutoApprover.called).to.be.false; + expect(action.setAutoApproval.called).to.be.false; + expect(action.setAutoRejection.called).to.be.false; }); it('should catch and handle unexpected errors', async () => { - const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-allow.sh'); + const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-exit-0.sh'); sinon.stub(require('fs'), 'existsSync').throws(new Error('Unexpected FS error')); @@ -100,11 +72,12 @@ describe('Pre-Receive Hook Execution', function () { expect( result.steps[0].logs.some((log) => log.includes('Hook execution error: Unexpected FS error')), ).to.be.true; - expect(action.setAllowAutoApprover.called).to.be.false; + expect(action.setAutoApproval.called).to.be.false; + expect(action.setAutoRejection.called).to.be.false; }); it('should approve push automatically when hook returns status 0', async () => { - const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-allow.sh'); + const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-exit-0.sh'); const result = await exec(req, action, scriptPath); @@ -115,6 +88,36 @@ describe('Pre-Receive Hook Execution', function () { log.includes('Push automatically approved by pre-receive hook.'), ), ).to.be.true; - expect(action.setAllowAutoApprover.calledOnce).to.be.true; + expect(action.setAutoApproval.calledOnce).to.be.true; + expect(action.setAutoRejection.called).to.be.false; + }); + + it('should reject push automatically when hook returns status 1', async () => { + const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-exit-1.sh'); + + const result = await exec(req, action, scriptPath); + + expect(result.steps).to.have.lengthOf(1); + expect(result.steps[0].error).to.be.false; + expect( + result.steps[0].logs.some((log) => + log.includes('Push automatically rejected by pre-receive hook.'), + ), + ).to.be.true; + expect(action.setAutoRejection.calledOnce).to.be.true; + expect(action.setAutoApproval.called).to.be.false; + }); + + it('should execute hook successfully and require manual approval', async () => { + const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-exit-2.sh'); + + const result = await exec(req, action, scriptPath); + + expect(result.steps).to.have.lengthOf(1); + expect(result.steps[0].error).to.be.false; + expect(result.steps[0].logs.some((log) => log.includes('Push requires manual approval.'))).to.be + .true; + expect(action.setAutoApproval.called).to.be.false; + expect(action.setAutoRejection.called).to.be.false; }); }); From f723307875309e8004277d3a61d97b6ca19ed1a0 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Tue, 25 Mar 2025 15:29:46 +0100 Subject: [PATCH 09/16] refactor: move attempt functions into autoAction.js --- src/proxy/actions/autoActions.js | 38 ++++++++++++++++++++++++++++++++ src/proxy/chain.js | 22 ++++-------------- 2 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 src/proxy/actions/autoActions.js diff --git a/src/proxy/actions/autoActions.js b/src/proxy/actions/autoActions.js new file mode 100644 index 000000000..0591ebe82 --- /dev/null +++ b/src/proxy/actions/autoActions.js @@ -0,0 +1,38 @@ +const db = require('../../db'); + +const attemptAutoApproval = async (action) => { + try { + const attestation = { + timestamp: new Date(), + autoApproved: true, + }; + await db.authorise(action.id, attestation); + console.log('Push automatically approved by system.'); + + return true; + } catch (error) { + console.error('Error during auto-approval:', error.message); + return false; + } +}; + +const attemptAutoRejection = async (action) => { + try { + const attestation = { + timestamp: new Date(), + autoApproved: true, + }; + await db.reject(action.id, attestation); + console.log('Push automatically rejected by system.'); + + return true; + } catch (error) { + console.error('Error during auto-rejection:', error.message); + return false; + } +}; + +module.exports = { + attemptAutoApproval, + attemptAutoRejection, +}; diff --git a/src/proxy/chain.js b/src/proxy/chain.js index 0a6e73285..c9304d330 100644 --- a/src/proxy/chain.js +++ b/src/proxy/chain.js @@ -1,5 +1,5 @@ const proc = require('./processors'); -const db = require('../db'); +const { attemptAutoApproval, attemptAutoRejection } = require('./actions/autoActions'); const pushActionChain = [ proc.push.parsePush, @@ -21,22 +21,6 @@ const pullActionChain = [proc.push.checkRepoInAuthorisedList]; let pluginsInserted = false; -const attemptAutoApproval = async (req, action) => { - try { - const attestation = { - timestamp: new Date(), - autoApproved: true, - }; - await db.authorise(action.id, attestation); - console.log('Push automatically approved by system.'); - - return true; - } catch (error) { - console.error('Error during auto-approval:', error.message); - return false; - } -}; - const executeChain = async (req) => { let action; try { @@ -58,7 +42,9 @@ const executeChain = async (req) => { } finally { await proc.push.audit(req, action); if (action.autoApproved) { - attemptAutoApproval(req, action); + attemptAutoApproval(action); + } else if (action.autoRejected) { + attemptAutoRejection(action); } } From d2174d2f9266e04f70a4eb8a4aefe4063fda3412 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Tue, 25 Mar 2025 17:23:08 +0100 Subject: [PATCH 10/16] feat: add test for auto rejection --- test/chain.test.js | 48 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/test/chain.test.js b/test/chain.test.js index da5ebb59f..5fd8b56a5 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -256,7 +256,6 @@ describe('proxy chain', function () { setAutoApproval: sinon.stub(), repoName: 'test-repo', commitTo: 'newCommitHash', - autoApproved: false, }; mockPreProcessors.parseAction.resolves(action); @@ -290,7 +289,52 @@ describe('proxy chain', function () { expect(result.continue).to.be.a('function'); expect(dbStub.calledOnce).to.be.true; - expect(dbStub.calledWith(action.id, sinon.match({ autoApproved: true }))).to.be.true; + + dbStub.restore(); + }); + + it('should reject push automatically and record in the database', async function () { + const req = {}; + const action = { + type: 'push', + continue: () => true, + allowPush: false, + setAutoRejection: sinon.stub(), + repoName: 'test-repo', + commitTo: 'newCommitHash', + }; + + mockPreProcessors.parseAction.resolves(action); + mockPushProcessors.parsePush.resolves(action); + mockPushProcessors.checkRepoInAuthorisedList.resolves(action); + mockPushProcessors.checkCommitMessages.resolves(action); + mockPushProcessors.checkAuthorEmails.resolves(action); + mockPushProcessors.checkUserPushPermission.resolves(action); + mockPushProcessors.checkIfWaitingAuth.resolves(action); + mockPushProcessors.pullRemote.resolves(action); + mockPushProcessors.writePack.resolves(action); + + mockPushProcessors.preReceive.resolves({ + ...action, + steps: [{ error: false, logs: ['Push automatically rejected by pre-receive hook.'] }], + allowPush: true, + autoRejected: true, + }); + + mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.clearBareClone.resolves(action); + mockPushProcessors.scanDiff.resolves(action); + mockPushProcessors.blockForAuth.resolves(action); + + const dbStub = sinon.stub(db, 'reject').resolves(true); + + const result = await chain.executeChain(req); + + expect(result.type).to.equal('push'); + expect(result.allowPush).to.be.true; + expect(result.continue).to.be.a('function'); + + expect(dbStub.calledOnce).to.be.true; dbStub.restore(); }); From 407e6e209683e462079119de966c122ce4c3ffae Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Tue, 25 Mar 2025 17:34:28 +0100 Subject: [PATCH 11/16] docs: add pre receive docs --- docs/pre-receive.md | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/pre-receive.md diff --git a/docs/pre-receive.md b/docs/pre-receive.md new file mode 100644 index 000000000..40a3466b9 --- /dev/null +++ b/docs/pre-receive.md @@ -0,0 +1,52 @@ +# Pre-Receive Hook Documentation + +## Overview + +The `pre-receive` hook is a critical component of the Git Proxy system. It is executed before changes are accepted into a repository. This hook allows for custom logic to validate or reject incoming changes based on specific criteria, ensuring that only valid and authorized changes are pushed to the repository. + +## Functionality + +The `pre-receive` hook determines the outcome of a push based on the exit status of the hook script: + +- If the script exits with status `0`, the push is automatically approved. +- If the script exits with status `1`, the push is automatically rejected. +- If the script exits with status `2`, the push requires manual approval. +- Any other exit status is treated as an error, and the push is rejected with an appropriate error message. + +## Usage + +To use the `pre-receive` hook, follow these steps: + +- **Create a Hook Script**: + Write a shell script or executable file that implements your custom validation logic. The script must accept input in the format: ` `. + +- **Place the Script**: + Save the script in the appropriate directory, such as `hooks/pre-receive.sh`. + +- **Make the Script Executable**: + Ensure the script has executable permissions. You can do this by running the following command: + + ```bash + chmod +x hooks/pre-receive.sh + ``` + +> **Note**: If the `pre-receive` script does not exist, the hook will not be executed, and the push will proceed without validation. + +## Example Hook Script + +Below is an example of a simple `pre-receive` hook script: + +```bash +#!/bin/bash + +read old_commit new_commit branch_name + +# Example validation: Reject pushes to the main branch +if [ "$branch_name" == "main" ]; then + echo "Pushes to the main branch are not allowed." + exit 1 +fi + +# Approve all other pushes +exit 0 +``` From b72d5c967e38ae5f3c547948582aebf287a5a7af Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Mon, 31 Mar 2025 16:41:33 +0200 Subject: [PATCH 12/16] refactor: moved pre-receeive docs into website/docs --- .../docs/configuration/pre-receive.mdx | 27 +++++++++++-------- website/sidebars.js | 4 +-- 2 files changed, 18 insertions(+), 13 deletions(-) rename docs/pre-receive.md => website/docs/configuration/pre-receive.mdx (54%) diff --git a/docs/pre-receive.md b/website/docs/configuration/pre-receive.mdx similarity index 54% rename from docs/pre-receive.md rename to website/docs/configuration/pre-receive.mdx index 40a3466b9..57240d7ee 100644 --- a/docs/pre-receive.md +++ b/website/docs/configuration/pre-receive.mdx @@ -1,8 +1,10 @@ -# Pre-Receive Hook Documentation +--- +title: 'Pre-Receive Hook' +--- ## Overview -The `pre-receive` hook is a critical component of the Git Proxy system. It is executed before changes are accepted into a repository. This hook allows for custom logic to validate or reject incoming changes based on specific criteria, ensuring that only valid and authorized changes are pushed to the repository. +The `pre-receive` hook is a critical component of the GitProxy system. It is executed before changes are accepted into a repository. This hook allows for custom logic to validate or reject incoming changes based on specific criteria, ensuring that only valid and authorized changes are pushed to the repository. ## Functionality @@ -17,18 +19,21 @@ The `pre-receive` hook determines the outcome of a push based on the exit status To use the `pre-receive` hook, follow these steps: -- **Create a Hook Script**: - Write a shell script or executable file that implements your custom validation logic. The script must accept input in the format: ` `. +### Create a Hook Script -- **Place the Script**: - Save the script in the appropriate directory, such as `hooks/pre-receive.sh`. +Write a shell script or executable file that implements your custom validation logic. The script must accept input in the format: ` `. -- **Make the Script Executable**: - Ensure the script has executable permissions. You can do this by running the following command: +### Place the Script - ```bash - chmod +x hooks/pre-receive.sh - ``` +Save the script in the appropriate directory, such as `hooks/pre-receive.sh`. + +### Make the Script Executable + +Ensure the script has executable permissions. You can do this by running the following command: + +```bash +chmod +x hooks/pre-receive.sh +``` > **Note**: If the `pre-receive` script does not exist, the hook will not be executed, and the push will proceed without validation. diff --git a/website/sidebars.js b/website/sidebars.js index 79db3bb16..6573101d1 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -29,7 +29,7 @@ module.exports = { }, collapsible: true, collapsed: false, - items: ['configuration/overview', 'configuration/reference'], + items: ['configuration/overview', 'configuration/reference', 'configuration/pre-receive'], }, { type: 'category', @@ -44,6 +44,6 @@ module.exports = { collapsible: true, collapsed: false, items: ['development/contributing', 'development/plugins'], - } + }, ], }; From c930f110e8bf120c41110293616960f01ee5e5c2 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 2 Apr 2025 10:01:37 +0200 Subject: [PATCH 13/16] test(cypress): add test for auto-pproved pushes --- cypress/e2e/autoApproved.cy.js | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 cypress/e2e/autoApproved.cy.js diff --git a/cypress/e2e/autoApproved.cy.js b/cypress/e2e/autoApproved.cy.js new file mode 100644 index 000000000..ae67f3ecd --- /dev/null +++ b/cypress/e2e/autoApproved.cy.js @@ -0,0 +1,72 @@ +import moment from 'moment'; + +describe('Auto-Approved Push Test', () => { + beforeEach(() => { + cy.intercept('GET', '/api/v1/push/123', { + statusCode: 200, + body: { + steps: [ + { + stepName: 'diff', + content: '', + }, + ], + error: false, + allowPush: true, + authorised: true, + canceled: false, + rejected: false, + autoApproved: true, + autoRejected: false, + commitFrom: 'commitFrom', + commitTo: 'commitTo', + branch: 'refs/heads/main', + user: 'testUser', + id: 'commitFrom__commitTo', + type: 'push', + method: 'POST', + timestamp: 1696161600000, + project: 'testUser', + repoName: 'test.git', + url: 'https://github.com/testUser/test.git', + repo: 'testUser/test.git', + commitData: [ + { + tree: '1234', + parent: '12345', + }, + ], + attestation: { + timestamp: '2023-10-01T12:00:00Z', + autoApproved: true, + }, + }, + }).as('getPush'); + }); + + it('should display auto-approved message and verify tooltip contains the expected timestamp', () => { + cy.visit('/admin/push/123'); + + cy.wait('@getPush'); + + cy.contains('Auto-approved by system').should('be.visible'); + + cy.get('svg.MuiSvgIcon-root') + .filter((_, el) => getComputedStyle(el).fill === 'rgb(0, 128, 0)') + .invoke('attr', 'style') + .should('include', 'cursor: default') + .and('include', 'opacity: 0.5'); + + const expectedTooltipTimestamp = moment('2023-10-01T12:00:00Z') + .local() + .format('dddd, MMMM Do YYYY, h:mm:ss a'); + + cy.get('kbd') + .trigger('mouseover') + .then(() => { + cy.get('.MuiTooltip-tooltip').should('contain', expectedTooltipTimestamp); + }); + + cy.contains('approved this contribution').should('not.exist'); + }); +}); From 583b8ed3ae6ebd9765750627891692a8202a9d92 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 2 Apr 2025 10:18:57 +0200 Subject: [PATCH 14/16] test: increase coverage for pre-receive --- .../pre-receive-hooks/always-exit-99.sh | 2 ++ test/preReceive/preReceive.test.js | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100755 test/preReceive/pre-receive-hooks/always-exit-99.sh diff --git a/test/preReceive/pre-receive-hooks/always-exit-99.sh b/test/preReceive/pre-receive-hooks/always-exit-99.sh new file mode 100755 index 000000000..266fe467e --- /dev/null +++ b/test/preReceive/pre-receive-hooks/always-exit-99.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exit 99 \ No newline at end of file diff --git a/test/preReceive/preReceive.test.js b/test/preReceive/preReceive.test.js index 42d13b31e..b9cfe0ecb 100644 --- a/test/preReceive/preReceive.test.js +++ b/test/preReceive/preReceive.test.js @@ -120,4 +120,19 @@ describe('Pre-Receive Hook Execution', function () { expect(action.setAutoApproval.called).to.be.false; expect(action.setAutoRejection.called).to.be.false; }); + + it('should handle unexpected hook status codes', async () => { + const scriptPath = path.resolve(__dirname, 'pre-receive-hooks/always-exit-99.sh'); + + const result = await exec(req, action, scriptPath); + + expect(result.steps).to.have.lengthOf(1); + expect(result.steps[0].error).to.be.true; + expect(result.steps[0].logs.some((log) => log.includes('Unexpected hook status: 99'))).to.be + .true; + expect(result.steps[0].logs.some((log) => log.includes('Unknown pre-receive hook error.'))).to + .be.true; + expect(action.setAutoApproval.called).to.be.false; + expect(action.setAutoRejection.called).to.be.false; + }); }); From 913a057b423b4ec2b0f9afd66c1c6db280431341 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 2 Apr 2025 10:55:01 +0200 Subject: [PATCH 15/16] test: add tests for actions --- test/testAction.test.js | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test/testAction.test.js diff --git a/test/testAction.test.js b/test/testAction.test.js new file mode 100644 index 000000000..25b0f4e13 --- /dev/null +++ b/test/testAction.test.js @@ -0,0 +1,48 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const { Action } = require('../src/proxy/actions/Action'); + +describe('Action class - Error Handling', () => { + let action; + let consoleErrorStub; + + beforeEach(() => { + action = new Action('1', 'push', 'method', Date.now(), 'project/repo'); + consoleErrorStub = sinon.stub(console, 'error'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('setAutoApproval() should log an error and return false if an error occurs', () => { + action.setAutoApproval = function () { + try { + throw new Error('Test error'); + } catch (error) { + console.error('Error during auto-approval:', error.message); + return false; + } + }; + + const result = action.setAutoApproval(); + expect(consoleErrorStub.calledOnceWith('Error during auto-approval:', 'Test error')).to.be.true; + expect(result).to.be.false; + }); + + it('setAutoRejection() should log an error and return false if an error occurs', () => { + action.setAutoRejection = function () { + try { + throw new Error('Test error'); + } catch (error) { + console.error('Error during auto-rejection:', error.message); + return false; + } + }; + + const result = action.setAutoRejection(); + expect(consoleErrorStub.calledOnceWith('Error during auto-rejection:', 'Test error')).to.be + .true; + expect(result).to.be.false; + }); +}); From c554ccecda97c53e250ab9e65f83749f6bc9115e Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 2 Apr 2025 11:52:51 +0200 Subject: [PATCH 16/16] test: fix test for actions --- test/chain.test.js | 92 +++++++++++++++++++++++++++++++++++++++++ test/testAction.test.js | 48 --------------------- 2 files changed, 92 insertions(+), 48 deletions(-) delete mode 100644 test/testAction.test.js diff --git a/test/chain.test.js b/test/chain.test.js index 5fd8b56a5..a4f559e73 100644 --- a/test/chain.test.js +++ b/test/chain.test.js @@ -338,4 +338,96 @@ describe('proxy chain', function () { dbStub.restore(); }); + + it('executeChain should handle exceptions in attemptAutoApproval', async function () { + const req = {}; + const action = { + type: 'push', + continue: () => true, + allowPush: false, + setAutoApproval: sinon.stub(), + repoName: 'test-repo', + commitTo: 'newCommitHash', + }; + + mockPreProcessors.parseAction.resolves(action); + mockPushProcessors.parsePush.resolves(action); + mockPushProcessors.checkRepoInAuthorisedList.resolves(action); + mockPushProcessors.checkCommitMessages.resolves(action); + mockPushProcessors.checkAuthorEmails.resolves(action); + mockPushProcessors.checkUserPushPermission.resolves(action); + mockPushProcessors.checkIfWaitingAuth.resolves(action); + mockPushProcessors.pullRemote.resolves(action); + mockPushProcessors.writePack.resolves(action); + + mockPushProcessors.preReceive.resolves({ + ...action, + steps: [{ error: false, logs: ['Push automatically approved by pre-receive hook.'] }], + allowPush: true, + autoApproved: true, + }); + + mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.clearBareClone.resolves(action); + mockPushProcessors.scanDiff.resolves(action); + mockPushProcessors.blockForAuth.resolves(action); + + const error = new Error('Database error'); + + const consoleErrorStub = sinon.stub(console, 'error'); + sinon.stub(db, 'authorise').rejects(error); + await chain.executeChain(req); + expect(consoleErrorStub.calledOnceWith('Error during auto-approval:', error.message)).to.be + .true; + db.authorise.restore(); + consoleErrorStub.restore(); + }); + + it('executeChain should handle exceptions in attemptAutoRejection', async function () { + const req = {}; + const action = { + type: 'push', + continue: () => true, + allowPush: false, + setAutoRejection: sinon.stub(), + repoName: 'test-repo', + commitTo: 'newCommitHash', + autoRejected: true, + }; + + mockPreProcessors.parseAction.resolves(action); + mockPushProcessors.parsePush.resolves(action); + mockPushProcessors.checkRepoInAuthorisedList.resolves(action); + mockPushProcessors.checkCommitMessages.resolves(action); + mockPushProcessors.checkAuthorEmails.resolves(action); + mockPushProcessors.checkUserPushPermission.resolves(action); + mockPushProcessors.checkIfWaitingAuth.resolves(action); + mockPushProcessors.pullRemote.resolves(action); + mockPushProcessors.writePack.resolves(action); + + mockPushProcessors.preReceive.resolves({ + ...action, + steps: [{ error: false, logs: ['Push automatically rejected by pre-receive hook.'] }], + allowPush: false, + autoRejected: true, + }); + + mockPushProcessors.getDiff.resolves(action); + mockPushProcessors.clearBareClone.resolves(action); + mockPushProcessors.scanDiff.resolves(action); + mockPushProcessors.blockForAuth.resolves(action); + + const error = new Error('Database error'); + + const consoleErrorStub = sinon.stub(console, 'error'); + sinon.stub(db, 'reject').rejects(error); + + await chain.executeChain(req); + + expect(consoleErrorStub.calledOnceWith('Error during auto-rejection:', error.message)).to.be + .true; + + db.reject.restore(); + consoleErrorStub.restore(); + }); }); diff --git a/test/testAction.test.js b/test/testAction.test.js deleted file mode 100644 index 25b0f4e13..000000000 --- a/test/testAction.test.js +++ /dev/null @@ -1,48 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const { Action } = require('../src/proxy/actions/Action'); - -describe('Action class - Error Handling', () => { - let action; - let consoleErrorStub; - - beforeEach(() => { - action = new Action('1', 'push', 'method', Date.now(), 'project/repo'); - consoleErrorStub = sinon.stub(console, 'error'); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('setAutoApproval() should log an error and return false if an error occurs', () => { - action.setAutoApproval = function () { - try { - throw new Error('Test error'); - } catch (error) { - console.error('Error during auto-approval:', error.message); - return false; - } - }; - - const result = action.setAutoApproval(); - expect(consoleErrorStub.calledOnceWith('Error during auto-approval:', 'Test error')).to.be.true; - expect(result).to.be.false; - }); - - it('setAutoRejection() should log an error and return false if an error occurs', () => { - action.setAutoRejection = function () { - try { - throw new Error('Test error'); - } catch (error) { - console.error('Error during auto-rejection:', error.message); - return false; - } - }; - - const result = action.setAutoRejection(); - expect(consoleErrorStub.calledOnceWith('Error during auto-rejection:', 'Test error')).to.be - .true; - expect(result).to.be.false; - }); -});