Skip to content

Commit 5d680a6

Browse files
committed
feat: add SSH configuration and key management features
- Introduce .nvmrc file to specify Node.js version - Add settings.local.json for SSH permissions configuration - Enhance SSH interface in config.ts to include detailed host key settings - Update SSH server to improve error handling and command processing - Extend tests to cover new SSH key capture functionality and database user updates - Include package-lock.json for test package dependencies
1 parent 18b52ab commit 5d680a6

File tree

8 files changed

+228
-57
lines changed

8 files changed

+228
-57
lines changed

.claude/settings.local.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": ["Bash(npm test)", "Bash(npm test:*)"],
4+
"deny": [],
5+
"ask": []
6+
}
7+
}

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v20

src/config/generated/config.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,40 @@ export interface Database {
279279
[property: string]: any;
280280
}
281281

282+
/**
283+
* SSH proxy server configuration
284+
*/
285+
export interface SSH {
286+
/**
287+
* Enable SSH proxy server
288+
*/
289+
enabled: boolean;
290+
/**
291+
* SSH host key configuration
292+
*/
293+
hostKey?: HostKey;
294+
/**
295+
* Port for SSH proxy server to listen on
296+
*/
297+
port?: number;
298+
[property: string]: any;
299+
}
300+
301+
/**
302+
* SSH host key configuration
303+
*/
304+
export interface HostKey {
305+
/**
306+
* Path to private SSH host key
307+
*/
308+
privateKeyPath: string;
309+
/**
310+
* Path to public SSH host key
311+
*/
312+
publicKeyPath: string;
313+
[property: string]: any;
314+
}
315+
282316
/**
283317
* Toggle the generation of temporary password for git-proxy admin user
284318
*/
@@ -302,25 +336,6 @@ export interface TLS {
302336
[property: string]: any;
303337
}
304338

305-
/**
306-
* SSH proxy server configuration
307-
*/
308-
export interface SSH {
309-
enabled?: boolean;
310-
port?: number;
311-
hostKey?: SSHHostKey;
312-
[property: string]: any;
313-
}
314-
315-
/**
316-
* SSH host key configuration
317-
*/
318-
export interface SSHHostKey {
319-
privateKeyPath: string;
320-
publicKeyPath: string;
321-
[property: string]: any;
322-
}
323-
324339
/**
325340
* UI routes that require authentication (logged in or admin)
326341
*/
@@ -541,6 +556,7 @@ const typeMap: any = {
541556
{ json: 'rateLimit', js: 'rateLimit', typ: u(undefined, r('RateLimit')) },
542557
{ json: 'sessionMaxAgeHours', js: 'sessionMaxAgeHours', typ: u(undefined, 3.14) },
543558
{ json: 'sink', js: 'sink', typ: u(undefined, a(r('Database'))) },
559+
{ json: 'ssh', js: 'ssh', typ: u(undefined, r('SSH')) },
544560
{ json: 'sslCertPemPath', js: 'sslCertPemPath', typ: u(undefined, '') },
545561
{ json: 'sslKeyPemPath', js: 'sslKeyPemPath', typ: u(undefined, '') },
546562
{ json: 'tempPassword', js: 'tempPassword', typ: u(undefined, r('TempPassword')) },
@@ -625,6 +641,21 @@ const typeMap: any = {
625641
],
626642
'any',
627643
),
644+
SSH: o(
645+
[
646+
{ json: 'enabled', js: 'enabled', typ: true },
647+
{ json: 'hostKey', js: 'hostKey', typ: u(undefined, r('HostKey')) },
648+
{ json: 'port', js: 'port', typ: u(undefined, 3.14) },
649+
],
650+
'any',
651+
),
652+
HostKey: o(
653+
[
654+
{ json: 'privateKeyPath', js: 'privateKeyPath', typ: '' },
655+
{ json: 'publicKeyPath', js: 'publicKeyPath', typ: '' },
656+
],
657+
'any',
658+
),
628659
TempPassword: o(
629660
[
630661
{ json: 'emailConfig', js: 'emailConfig', typ: u(undefined, m('any')) },

src/proxy/ssh/server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export class SSHServer {
250250
});
251251
}
252252

253-
private async handleCommand(
253+
public async handleCommand(
254254
command: string,
255255
stream: ssh2.ServerChannel,
256256
client: ClientWithUser,
@@ -361,7 +361,7 @@ export class SSHServer {
361361
`[SSH] Chain execution failed for user ${client.authenticatedUser?.username}:`,
362362
chainError,
363363
);
364-
stream.stderr.write(`Access denied: ${chainError}\n`);
364+
stream.stderr.write(`Access denied: ${chainError.message || chainError}\n`);
365365
stream.exit(1);
366366
stream.end();
367367
return;

test/chain.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const initMockPushProcessors = (sinon) => {
3333
gitleaks: sinon.stub(),
3434
clearBareClone: sinon.stub(),
3535
scanDiff: sinon.stub(),
36+
captureSSHKey: sinon.stub(),
3637
blockForAuth: sinon.stub(),
3738
};
3839
mockPushProcessors.parsePush.displayName = 'parsePush';
@@ -51,6 +52,7 @@ const initMockPushProcessors = (sinon) => {
5152
mockPushProcessors.gitleaks.displayName = 'gitleaks';
5253
mockPushProcessors.clearBareClone.displayName = 'clearBareClone';
5354
mockPushProcessors.scanDiff.displayName = 'scanDiff';
55+
mockPushProcessors.captureSSHKey.displayName = 'captureSSHKey';
5456
mockPushProcessors.blockForAuth.displayName = 'blockForAuth';
5557
return mockPushProcessors;
5658
};
@@ -219,11 +221,13 @@ describe('proxy chain', function () {
219221
mockPushProcessors.gitleaks.resolves(continuingAction);
220222
mockPushProcessors.clearBareClone.resolves(continuingAction);
221223
mockPushProcessors.scanDiff.resolves(continuingAction);
224+
mockPushProcessors.captureSSHKey.resolves(continuingAction);
222225
mockPushProcessors.blockForAuth.resolves(continuingAction);
223226

224227
const result = await chain.executeChain(req);
225228

226229
expect(mockPreProcessors.parseAction.called).to.be.true;
230+
console.log(mockPushProcessors);
227231
expect(mockPushProcessors.parsePush.called).to.be.true;
228232
expect(mockPushProcessors.checkEmptyBranch.called).to.be.true;
229233
expect(mockPushProcessors.checkRepoInAuthorisedList.called).to.be.true;
@@ -239,6 +243,7 @@ describe('proxy chain', function () {
239243
expect(mockPushProcessors.gitleaks.called).to.be.true;
240244
expect(mockPushProcessors.clearBareClone.called).to.be.true;
241245
expect(mockPushProcessors.scanDiff.called).to.be.true;
246+
expect(mockPushProcessors.captureSSHKey.called).to.be.true;
242247
expect(mockPushProcessors.blockForAuth.called).to.be.true;
243248
expect(mockPushProcessors.audit.called).to.be.true;
244249

@@ -320,6 +325,7 @@ describe('proxy chain', function () {
320325
mockPushProcessors.gitleaks.resolves(action);
321326
mockPushProcessors.clearBareClone.resolves(action);
322327
mockPushProcessors.scanDiff.resolves(action);
328+
mockPushProcessors.captureSSHKey.resolves(action);
323329
mockPushProcessors.blockForAuth.resolves(action);
324330
const dbStub = sinon.stub(db, 'authorise').resolves(true);
325331

@@ -368,6 +374,7 @@ describe('proxy chain', function () {
368374
mockPushProcessors.gitleaks.resolves(action);
369375
mockPushProcessors.clearBareClone.resolves(action);
370376
mockPushProcessors.scanDiff.resolves(action);
377+
mockPushProcessors.captureSSHKey.resolves(action);
371378
mockPushProcessors.blockForAuth.resolves(action);
372379

373380
const dbStub = sinon.stub(db, 'reject').resolves(true);
@@ -417,6 +424,7 @@ describe('proxy chain', function () {
417424
mockPushProcessors.gitleaks.resolves(action);
418425
mockPushProcessors.clearBareClone.resolves(action);
419426
mockPushProcessors.scanDiff.resolves(action);
427+
mockPushProcessors.captureSSHKey.resolves(action);
420428
mockPushProcessors.blockForAuth.resolves(action);
421429

422430
const error = new Error('Database error');
@@ -465,6 +473,7 @@ describe('proxy chain', function () {
465473
mockPushProcessors.gitleaks.resolves(action);
466474
mockPushProcessors.clearBareClone.resolves(action);
467475
mockPushProcessors.scanDiff.resolves(action);
476+
mockPushProcessors.captureSSHKey.resolves(action);
468477
mockPushProcessors.blockForAuth.resolves(action);
469478

470479
const error = new Error('Database error');

test/fixtures/test-package/package-lock.json

Lines changed: 135 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)