Skip to content

feat: SSH UI #52

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: denis-coric/ssh-flow
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export const getSSHConfig = () => {
}
return _sshConfig;
};
export const getPublicSSHConfig = () => {
if (_userSettings !== null && _userSettings.ssh) {
_sshConfig = _userSettings.ssh;
}
const { enabled = false, port = 22 } = _sshConfig;
return { enabled, port };
};

// Gets a list of authorised repositories
export const getAuthorisedList = () => {
Expand Down Expand Up @@ -104,7 +111,7 @@ export const getDatabase = () => {

/**
* Get the list of enabled authentication methods
*
*
* At least one authentication method must be enabled.
* @return {Array} List of enabled authentication methods
*/
Expand All @@ -116,15 +123,15 @@ export const getAuthMethods = () => {
const enabledAuthMethods = _authentication.filter((auth) => auth.enabled);

if (enabledAuthMethods.length === 0) {
throw new Error("No authentication method enabled");
throw new Error('No authentication method enabled');
}

return enabledAuthMethods;
};

/**
* Get the list of enabled authentication methods for API endpoints
*
*
* If no API authentication methods are enabled, all endpoints are public.
* @return {Array} List of enabled authentication methods
*/
Expand All @@ -133,7 +140,7 @@ export const getAPIAuthMethods = () => {
_apiAuthentication = _userSettings.apiAuthentication;
}

const enabledAuthMethods = _apiAuthentication.filter(auth => auth.enabled);
const enabledAuthMethods = _apiAuthentication.filter((auth) => auth.enabled);

return enabledAuthMethods;
};
Expand Down
1 change: 1 addition & 0 deletions src/db/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,6 @@ export const {
updateUser,
addPublicKey,
removePublicKey,
getPublicKeys,
findUserBySSHKey,
} = users;
28 changes: 13 additions & 15 deletions src/db/file/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ export const findUser = (username: string) => {
if (err) {
reject(err);
} else {
if (!doc) {
resolve(null);
} else {
resolve(doc);
}
resolve(doc || null);
}
});
});
Expand All @@ -43,11 +39,7 @@ export const findUserByOIDC = function (oidcId: string) {
if (err) {
reject(err);
} else {
if (!doc) {
resolve(null);
} else {
resolve(doc);
}
resolve(doc || null);
}
});
});
Expand Down Expand Up @@ -140,15 +132,21 @@ export const getUsers = (query: any = {}) => {
db.find(query, (err: Error, docs: User[]) => {
// ignore for code coverage as neDB rarely returns errors even for an invalid query
/* istanbul ignore if */
if (err) {
reject(err);
} else {
resolve(docs);
}
if (err) reject(err);
else resolve(docs);
});
});
};

export const getPublicKeys = (username: string): Promise<string[]> => {
return findUser(username).then((user) => {
if (!user) {
throw new Error('User not found');
}
return user.publicKeys || [];
});
};

export const addPublicKey = function (username: string, publicKey: string) {
return new Promise<User>((resolve, reject) => {
findUser(username)
Expand Down
1 change: 1 addition & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,6 @@ export const {
getSessionStore,
addPublicKey,
removePublicKey,
getPublicKeys,
findUserBySSHKey,
} = sink;
8 changes: 8 additions & 0 deletions src/db/mongo/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,11 @@ export const findUserBySSHKey = async function (sshKey: string) {
const collection = await connect(collectionName);
return collection.findOne({ publicKeys: { $eq: sshKey } });
};

export const getPublicKeys = async function (username: string): Promise<string[]> {
const user = await findUser(username);
if (!user) {
throw new Error('User not found');
}
return user.publicKeys || [];
};
76 changes: 76 additions & 0 deletions src/service/routes/users.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import crypto from 'node:crypto';

const express = require('express');
const router = new express.Router();
const db = require('../../db');
Expand Down Expand Up @@ -100,4 +102,78 @@ router.delete('/:username/ssh-keys', async (req, res) => {
}
});

router.delete('/:username/ssh-keys/fingerprint', async (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}

const targetUsername = req.params.username.toLowerCase();

if (req.user.username !== targetUsername && !req.user.admin) {
return res.status(403).json({ error: 'Not authorized to remove keys for this user' });
}

const { fingerprint } = req.body;
if (!fingerprint) {
return res.status(400).json({ error: 'Fingerprint is required' });
}

try {
const keys = await db.getPublicKeys(targetUsername);
console.log(`Found ${keys} keys for user ${targetUsername}`);
const keyToDelete = keys.find((k) => {
const keyFingerprint = sshFingerprintSHA256(k);
return keyFingerprint === fingerprint;
});

if (!keyToDelete) {
return res.status(404).json({ error: 'SSH key not found for supplied fingerprint' });
}

await db.removePublicKey(targetUsername, keyToDelete);
res.status(200).json({ message: 'SSH key removed successfully' });
} catch (err) {
console.error('Error removing SSH key:', err);
res.status(500).json({ error: 'Failed to remove SSH key' });
}
});

// Utility: compute the fingerprint "SHA256:<digest>"
function sshFingerprintSHA256(pubKey) {
if (!pubKey) return '';

// OpenSSH keys are: "<algorithm> <base64-blob> [comment]"
const b64 = pubKey.trim().split(/\s+/)[1];
if (!b64) return '';

const raw = Buffer.from(b64, 'base64'); // raw key bytes
const hash = crypto.createHash('sha256').update(raw).digest('base64');

return 'SHA256:' + hash.replace(/=+$/, '');
}

// Return only fingerprints & metadata,
router.get('/:username/ssh-keys', async (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}

const targetUsername = req.params.username.toLowerCase();

// A user can view their own keys; admins can view anyone's
if (req.user.username !== targetUsername && !req.user.admin) {
return res.status(403).json({ error: 'Not authorized to view keys for this user' });
}

try {
const keys = await db.getPublicKeys(targetUsername);
const result = keys.map((k) => sshFingerprintSHA256(k));

res.status(200).json({ publicKeys: result });
} catch (err) {
console.error('Error fetching SSH keys:', err);
res.status(500).json({ error: 'Failed to fetch SSH keys' });
}
});

module.exports = router;
Loading
Loading