From 0c8d7981cf32921c79eaa99f3754b8a649534b01 Mon Sep 17 00:00:00 2001 From: samaradel Date: Sun, 14 Apr 2024 16:16:24 +0200 Subject: [PATCH 01/24] - Remove Active keys table - Add all keys in one table - generate name for keys which havnt a name - Reduce size of titles - prevent opening the dialog of sshkey details on click on active checkbox --- .../ssh_keys/ManageSshDeployemnt.vue | 4 +-- .../src/components/ssh_keys/SshTable.vue | 8 ++--- packages/playground/src/views/sshkey_view.vue | 30 +++++++------------ 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/playground/src/components/ssh_keys/ManageSshDeployemnt.vue b/packages/playground/src/components/ssh_keys/ManageSshDeployemnt.vue index 461f8b2d7b..54258ebc7d 100644 --- a/packages/playground/src/components/ssh_keys/ManageSshDeployemnt.vue +++ b/packages/playground/src/components/ssh_keys/ManageSshDeployemnt.vue @@ -1,5 +1,5 @@ diff --git a/packages/playground/src/views/sshkey_view.vue b/packages/playground/src/views/sshkey_view.vue index 4c27ac5e9c..6ede3c9121 100644 --- a/packages/playground/src/views/sshkey_view.vue +++ b/packages/playground/src/views/sshkey_view.vue @@ -1,10 +1,10 @@ - diff --git a/packages/playground/src/utils/date.ts b/packages/playground/src/utils/date.ts index 934e5b838c..849f97ca84 100644 --- a/packages/playground/src/utils/date.ts +++ b/packages/playground/src/utils/date.ts @@ -3,7 +3,3 @@ import moment from "moment"; export default function toHumanDate(timeInSeconds: number): string { return moment(timeInSeconds * 1000).format("M/D/YY, h:m A"); } - -export function formatSSHKeyTableCreatedAt(date: Date) { - return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; -} diff --git a/packages/playground/src/utils/ssh.ts b/packages/playground/src/utils/ssh.ts new file mode 100644 index 0000000000..36171e51a7 --- /dev/null +++ b/packages/playground/src/utils/ssh.ts @@ -0,0 +1,174 @@ +import crypto from "crypto"; + +import { useProfileManager } from "@/stores/profile_manager"; +import type { SSHKeyData } from "@/types"; + +import { createCustomToast, ToastType } from "./custom_toast"; +import { getGrid, storeSSH } from "./grid"; +import { downloadAsJson } from "./helpers"; + +/** + * Manages SSH key operations including migration, updating, exporting, deleting, and listing. + */ +class SSHKeysManagement { + /** + * Migrates an old SSH key string to the new SSHKeyData format. + * @param oldKey The old SSH key string to migrate. + * @returns An array containing the migrated SSHKeyData. + */ + migrate(oldKey: string): SSHKeyData[] { + const userKeys: SSHKeyData[] = []; + const parts = oldKey.split(" "); + let keyName = ""; + if (parts.length < 3) { + keyName = this.generateName(); + } else { + keyName = parts[parts.length - 1]; + } + const newKey: SSHKeyData = { + createdAt: this.formatDate(new Date()), + name: keyName, + id: 1, + isActive: true, + publicKey: oldKey, + }; + userKeys.push(newKey); + return userKeys; + } + + /** + * Checks if the SSH key has not been migrated yet. + * @param key The SSH key to check for migration. + * @returns A boolean indicating whether the key has not been migrated. + */ + notMigrated(key: string | SSHKeyData[]): boolean { + return typeof key === "string"; + } + + /** + * Updates SSH keys in the profile and stores them in the grid. + * @param keys The SSH keys to update. + */ + async update(keys: SSHKeyData[]): Promise { + const profileManager = useProfileManager(); + const grid = await getGrid(profileManager.profile!); + if (!grid) { + createCustomToast(`Error occurred because the grid has not initialized yet.`, ToastType.danger); + return; + } + const copiedKeys = keys.map( + ({ fingerPrint, activating, deleting, ...keyWithoutSensitiveProps }) => keyWithoutSensitiveProps, + ); + await storeSSH(grid!, copiedKeys); + profileManager.updateSSH(copiedKeys); + } + + /** + * Generates a random name for an SSH key. + * @returns The generated SSH key name. + */ + generateName(): string { + const words = [ + "moon", + "earth", + "sun", + "star", + "galaxy", + "nebula", + "comet", + "planet", + "asteroid", + "satellite", + "mercury", + "venus", + "mars", + "jupiter", + "saturn", + "uranus", + "neptune", + "pluto", + "meteor", + "cosmos", + ]; + const keyName = words.sort(() => Math.random() - 0.5)[0]; + return keyName; + } + + /** + * Parses an SSH public key string. + * @param publicKey The SSH public key string to parse. + * @returns An object containing the parsed parts of the public key. + */ + parsePublicKey(publicKey: string) { + const parts = publicKey.split(" "); + return { + type: parts[0], + data: parts[1], + comment: parts[2], + }; + } + + /** + * Calculates the fingerprint of an SSH public key. + * @param publicKey The SSH public key string. + * @returns The calculated fingerprint. + */ + calculateFingerprint(publicKey: string): string { + if (publicKey.length) { + const sshPublicKey = this.parsePublicKey(publicKey); + const md5 = crypto.createHash("md5"); + if (sshPublicKey.data) { + md5.update(sshPublicKey.data); + const fingerprint = md5 + .digest("hex") + .replace(/(.{2})(?=.)/g, "$1:") + .toUpperCase(); + return fingerprint; + } + } + return "-"; + } + + /** + * Exports SSH keys as a JSON file. + * @param keys The SSH keys to export. + */ + async export(keys: SSHKeyData[]): Promise { + const exportKeys: SSHKeyData[] = keys.map(({ deleting, activating, ...rest }) => rest); + downloadAsJson(exportKeys, `ssh_keys.json`); + } + + /** + * Retrieves and formats SSH keys from the profile. + * @returns An array of formatted SSHKeyData. + */ + list(): SSHKeyData[] { + const profileManager = useProfileManager(); + const keys = profileManager.profile!.ssh.map(key => ({ + ...key, + fingerPrint: this.calculateFingerprint(key.publicKey), + })); + return keys; + } + + /** + * Formats a date into a string. + * @param date The date to format. + * @returns The formatted date string. + */ + formatDate(date: Date): string { + return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; + } + + /** + * Checks if a given string is a valid SSH key. + * @param key - The SSH key string to be validated. + * @returns True if the key is a valid SSH key, false otherwise. + */ + isValidSSHKey(key: string): boolean { + const sshKeyRegex = /^(ssh-rsa|ssh-dss|ecdsa-[a-zA-Z0-9-]+|ssh-ed25519)\s+(\S+)+\S/; + return sshKeyRegex.test(key); + } +} + +export default SSHKeysManagement; diff --git a/packages/playground/src/utils/strings.ts b/packages/playground/src/utils/strings.ts index 839c664d47..ea3543e008 100644 --- a/packages/playground/src/utils/strings.ts +++ b/packages/playground/src/utils/strings.ts @@ -24,32 +24,3 @@ function generateString(from: string, length: number): string { } return str; } - -export function generateSSHKeyName() { - // List of words to choose from - const words = [ - "moon", - "earth", - "sun", - "star", - "galaxy", - "nebula", - "comet", - "planet", - "asteroid", - "satellite", - "mercury", - "venus", - "mars", - "jupiter", - "saturn", - "uranus", - "neptune", - "pluto", - "meteor", - "cosmos", - ]; - - const keyName = words.sort(() => Math.random() - 0.5)[0]; - return keyName; -} diff --git a/packages/playground/src/utils/validators.ts b/packages/playground/src/utils/validators.ts index f5a95b223a..340a933749 100644 --- a/packages/playground/src/utils/validators.ts +++ b/packages/playground/src/utils/validators.ts @@ -733,16 +733,6 @@ export function pattern(msg: string, config: RegexPattern) { }; } -/** - * Checks if a given string is a valid SSH key. - * @param key - The SSH key string to be validated. - * @returns True if the key is a valid SSH key, false otherwise. - */ -export function isValidSSHKey(key: string): boolean { - const sshKeyRegex = /^(ssh-rsa|ssh-dss|ecdsa-[a-zA-Z0-9-]+|ssh-ed25519)\s+(\S+)\s+(\S+)/; - return sshKeyRegex.test(key); -} - export function isValidDecimalNumber(length: number, msg: string) { return (value: string) => { if (!(value.toString().split(".").length > 1 ? value.toString().split(".")[1].length <= length : true)) { diff --git a/packages/playground/src/views/sshkey_view.vue b/packages/playground/src/views/sshkey_view.vue index 6ede3c9121..6f62b830c0 100644 --- a/packages/playground/src/views/sshkey_view.vue +++ b/packages/playground/src/views/sshkey_view.vue @@ -14,14 +14,7 @@ -
- - - Migrating the old key. This process may require 15 to 30 seconds. Thank you for your patience. - -
- - + Import + Export + @@ -101,10 +102,8 @@ - - openDialog, - closeDialog, - addKey, - viewSelectedKey, - setActiveKey, - setInactiveKey, - deleteKey, - generateSSHKeys, - exportKeys, - migrateOldKey, - calculateFingerprint, - updateSSHKeysInChain, - }; - }, + diff --git a/packages/playground/src/weblets/profile_manager.vue b/packages/playground/src/weblets/profile_manager.vue index 8c0c3765d9..38890ba8f5 100644 --- a/packages/playground/src/weblets/profile_manager.vue +++ b/packages/playground/src/weblets/profile_manager.vue @@ -549,7 +549,7 @@ async function mounted() { password.value = sessionPassword; if (credentials.passwordHash) { - return login(); + return await login(); } } else { activeTab.value = 1; @@ -704,6 +704,14 @@ async function activate(mnemonic: string, keypairType: KeypairType) { profileManager.set({ ...profile, mnemonic }); emit("update:modelValue", false); + // Migrate the ssh-key + const sshKeysManagement = new SSHKeysManagement(); + const profileSSH = profileManager.profile?.ssh; + + if (sshKeysManagement.notMigrated(profileSSH!)) { + const newKeys = sshKeysManagement.migrate(profileSSH as unknown as string); + await sshKeysManagement.update(newKeys); + } } catch (e) { loginError.value = normalizeError(e, "Something went wrong while login."); } finally { @@ -786,7 +794,7 @@ async function __loadBalance(profile?: Profile, tries = 1) { } profileManagerController.set({ loadBalance: __loadBalance }); -function login() { +async function login() { const credentials: Credentials = getCredentials(); if (credentials.mnemonicHash && credentials.passwordHash) { if (credentials.passwordHash === md5(password.value)) { @@ -795,7 +803,7 @@ function login() { const keypairType = credentials.keypairTypeHash ? cryptr.decrypt(credentials.keypairTypeHash) : KeypairType.sr25519; - activate(mnemonic, keypairType as KeypairType); + await activate(mnemonic, keypairType as KeypairType); } } } @@ -808,7 +816,7 @@ async function storeAndLogin() { const grid = await getGrid({ mnemonic: mnemonic.value, keypairType: keypairType.value }); storeEmail(grid!, email.value); setCredentials(md5(password.value), mnemonicHash, keypairTypeHash, md5(email.value)); - activate(mnemonic.value, keypairType.value); + await activate(mnemonic.value, keypairType.value); } catch (e) { if (e instanceof TwinNotExistError) { isNonActiveMnemonic.value = true; @@ -900,6 +908,8 @@ function onScroll(e: UIEvent) { import { TwinNotExistError } from "@threefold/types"; import { capitalize } from "vue"; +import SSHKeysManagement from "@/utils/ssh"; + import QrcodeGenerator from "../components/qrcode_generator.vue"; import type { Profile } from "../stores/profile_manager"; export default { From a75228a372970cd08606dbb7bf324d3d23ed15ed Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Thu, 18 Apr 2024 12:06:21 +0200 Subject: [PATCH 15/24] Fix: Fix bug in listing ssh-keys for a new user, fix issue in displaying user keys in microVM component. --- packages/playground/src/utils/ssh.ts | 13 +++++++++---- packages/playground/src/weblets/micro_vm.vue | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/playground/src/utils/ssh.ts b/packages/playground/src/utils/ssh.ts index 36171e51a7..bc3c27a68d 100644 --- a/packages/playground/src/utils/ssh.ts +++ b/packages/playground/src/utils/ssh.ts @@ -144,10 +144,15 @@ class SSHKeysManagement { */ list(): SSHKeyData[] { const profileManager = useProfileManager(); - const keys = profileManager.profile!.ssh.map(key => ({ - ...key, - fingerPrint: this.calculateFingerprint(key.publicKey), - })); + console.log("profileManager.profile!.ssh", profileManager.profile!.ssh); + let keys: SSHKeyData[] = []; + + if (profileManager.profile!.ssh) { + keys = profileManager.profile!.ssh.map(key => ({ + ...key, + fingerPrint: this.calculateFingerprint(key.publicKey), + })); + } return keys; } diff --git a/packages/playground/src/weblets/micro_vm.vue b/packages/playground/src/weblets/micro_vm.vue index bb238e032d..3d532fbaff 100644 --- a/packages/playground/src/weblets/micro_vm.vue +++ b/packages/playground/src/weblets/micro_vm.vue @@ -182,7 +182,7 @@ + + diff --git a/packages/playground/src/components/ssh_keys/SshTable.vue b/packages/playground/src/components/ssh_keys/SshTable.vue index daa82386b5..bc32f4b517 100644 --- a/packages/playground/src/components/ssh_keys/SshTable.vue +++ b/packages/playground/src/components/ssh_keys/SshTable.vue @@ -179,7 +179,7 @@ export default defineComponent({ key: "name", }, { - title: "Creation Datetime", + title: "Created At", key: "createdAt", }, { diff --git a/packages/playground/src/utils/ssh.ts b/packages/playground/src/utils/ssh.ts index 3c5b528096..fd469eaf41 100644 --- a/packages/playground/src/utils/ssh.ts +++ b/packages/playground/src/utils/ssh.ts @@ -4,24 +4,55 @@ import { useProfileManager } from "@/stores/profile_manager"; import type { SSHKeyData } from "@/types"; import { createCustomToast, ToastType } from "./custom_toast"; -import { getGrid, storeSSH } from "./grid"; +import { getGrid, loadBalance, storeSSH } from "./grid"; import { downloadAsJson } from "./helpers"; /** * Manages SSH key operations including migration, updating, exporting, deleting, and listing. */ class SSHKeysManagement { + private oldKey = ""; + updateCost = 0.01; + private words = [ + "moon", + "earth", + "sun", + "star", + "galaxy", + "nebula", + "comet", + "planet", + "asteroid", + "satellite", + "mercury", + "venus", + "mars", + "jupiter", + "saturn", + "uranus", + "neptune", + "pluto", + "meteor", + "cosmos", + ]; + + constructor() { + const profileManager = useProfileManager(); + this.oldKey = profileManager.profile?.ssh as unknown as string; + } + /** * Migrates an old SSH key string to the new SSHKeyData format. - * @param oldKey The old SSH key string to migrate. * @returns An array containing the migrated SSHKeyData. */ - migrate(oldKey: string): SSHKeyData[] { + migrate(): SSHKeyData[] { const userKeys: SSHKeyData[] = []; - const parts = oldKey.split(" "); + let keyName = ""; + const parts = this.oldKey.split(" "); + if (parts.length < 3) { - keyName = this.generateName(); + keyName = this.generateName()!; } else { keyName = parts[parts.length - 1]; } @@ -30,7 +61,7 @@ class SSHKeysManagement { name: keyName, id: 1, isActive: true, - publicKey: oldKey, + publicKey: this.oldKey, }; userKeys.push(newKey); return userKeys; @@ -38,11 +69,10 @@ class SSHKeysManagement { /** * Checks if the SSH key has not been migrated yet. - * @param key The SSH key to check for migration. * @returns A boolean indicating whether the key has not been migrated. */ - notMigrated(key: string | SSHKeyData[]): boolean { - return typeof key === "string"; + migrated(): boolean { + return typeof this.oldKey !== "string"; } /** @@ -56,42 +86,40 @@ class SSHKeysManagement { createCustomToast(`Error occurred because the grid has not initialized yet.`, ToastType.danger); return; } + + const balance = await loadBalance(grid!); + if (balance.free < this.updateCost) { + createCustomToast( + `Your wallet balance is insufficient to save your SSH key. To avoid losing your SSH key, please recharge your wallet.`, + ToastType.danger, + ); + return; + } + const copiedKeys = keys.map( ({ fingerPrint, activating, deleting, ...keyWithoutSensitiveProps }) => keyWithoutSensitiveProps, ); + await storeSSH(grid!, copiedKeys); profileManager.updateSSH(copiedKeys); } /** - * Generates a random name for an SSH key. + * Generates a random name for an SSH key that is not included in the blocked names. * @returns The generated SSH key name. + * @throws Error if all names are blocked. */ - generateName(): string { - const words = [ - "moon", - "earth", - "sun", - "star", - "galaxy", - "nebula", - "comet", - "planet", - "asteroid", - "satellite", - "mercury", - "venus", - "mars", - "jupiter", - "saturn", - "uranus", - "neptune", - "pluto", - "meteor", - "cosmos", - ]; - const keyName = words.sort(() => Math.random() - 0.5)[0]; - return keyName; + generateName(): string | null { + // Filter out names that are already used + const blockedNames = this.list().map(key => key.name); + const availableNames = this.words.filter(name => !blockedNames.includes(name)); + + if (availableNames.length === 0) { + return null; + } + + // Generate a random name from the available names + return availableNames[Math.floor(Math.random() * availableNames.length)]; } /** @@ -143,15 +171,24 @@ class SSHKeysManagement { * @returns An array of formatted SSHKeyData. */ list(): SSHKeyData[] { - const profileManager = useProfileManager(); let keys: SSHKeyData[] = []; - if (profileManager.profile!.ssh) { - keys = profileManager.profile!.ssh.map(key => ({ - ...key, - fingerPrint: this.calculateFingerprint(key.publicKey), - })); + if (!this.migrated()) { + keys = this.migrate(); + } else { + keys = this.oldKey as unknown as SSHKeyData[]; + } + + // Profile created for the first time. + if (!keys) { + return []; } + + keys = keys.map(key => ({ + ...key, + fingerPrint: this.calculateFingerprint(key.publicKey), + })); + return keys; } @@ -173,6 +210,24 @@ class SSHKeysManagement { const sshKeyRegex = /^(ssh-rsa|ssh-dss|ecdsa-[a-zA-Z0-9-]+|ssh-ed25519)\s+(\S+)+\S/; return sshKeyRegex.test(key); } + + /** + * Checks if a given name is available by checking if there is another key with the same name. + * @param keyName - The key name to be validated. + * @returns True if the keyName is available to use, false otherwise. + */ + availableName(keyName: string): boolean { + return !this.list().some(key => key.name === keyName); + } + + /** + * Checks if a given SSH public key is available by checking if there is another key with the same public key. + * @param publicKey - The SSH public key to be validated. + * @returns True if the publicKey is available to use, false otherwise. + */ + availablePublicKey(publicKey: string): boolean { + return !this.list().some(key => key.publicKey === publicKey); + } } export default SSHKeysManagement; diff --git a/packages/playground/src/views/sshkey_view.vue b/packages/playground/src/views/sshkey_view.vue index 6f62b830c0..3fabbfb7b1 100644 --- a/packages/playground/src/views/sshkey_view.vue +++ b/packages/playground/src/views/sshkey_view.vue @@ -6,10 +6,8 @@ Manage SSH Keys

- Facilitating access to deployed machines involves the incorporation or adaptation of SSH keys, with the - flexibility to manage multiple keys seamlessly, allowing users to switch between them. Moreover, users can - activate individual keys or enable them all, streamlining the process of distributing them to the machines and - effectively managing accessibility to the deployed nodes. + Manage SSH keys easily, switch between them, and activate or deactivate keys as needed for accessing deployed + machines. Simplify key distribution and effectively manage access to nodes.

@@ -45,7 +43,7 @@ " :loading="isExporting" > - Export + Export all (false); const allKeys = ref([]); const dialogType = ref(SSHCreationMethod.None); -const generatedSSHKey = ref(""); const tableLoadingMessage = ref(""); const selectedKey = ref({ @@ -141,25 +138,23 @@ const sshKeysManagement = new SSHKeysManagement(); onMounted(async () => { loading.value = true; - let profileSSH = profileManager.profile?.ssh; - - if (sshKeysManagement.notMigrated(profileSSH!)) { + console.log(profileManager.profile?.ssh); + console.log(sshKeysManagement.migrated()); + if (!sshKeysManagement.migrated()) { tableLoadingMessage.value = "Migrating your old key..."; const migrationInterval = setInterval(async () => { - profileSSH = profileManager.profile?.ssh; - const migrated = !sshKeysManagement.notMigrated(profileSSH!); + const migrated = !sshKeysManagement.migrated(); if (migrated) { clearInterval(migrationInterval); allKeys.value = sshKeysManagement.list(); - loading.value = false; tableLoadingMessage.value = ""; } }, 1000); } else { allKeys.value = sshKeysManagement.list(); tableLoadingMessage.value = ""; - loading.value = false; } + loading.value = false; }); const openDialog = (type: SSHCreationMethod) => { @@ -168,7 +163,6 @@ const openDialog = (type: SSHCreationMethod) => { const closeDialog = () => { dialogType.value = SSHCreationMethod.None; - generatedSSHKey.value = ""; }; const exportAllKeys = () => { @@ -229,8 +223,8 @@ const generateSSHKeys = async (key: SSHKeyData) => { size: 4096, }); - generatedSSHKey.value = keys.publicKey; key.fingerPrint = sshKeysManagement.calculateFingerprint(keys.publicKey); + key.publicKey = keys.publicKey; const copiedAllkeys = [...allKeys.value, key]; await sshKeysManagement.update(copiedAllkeys); diff --git a/packages/playground/src/weblets/profile_manager.vue b/packages/playground/src/weblets/profile_manager.vue index 38890ba8f5..c219c718b9 100644 --- a/packages/playground/src/weblets/profile_manager.vue +++ b/packages/playground/src/weblets/profile_manager.vue @@ -706,10 +706,9 @@ async function activate(mnemonic: string, keypairType: KeypairType) { emit("update:modelValue", false); // Migrate the ssh-key const sshKeysManagement = new SSHKeysManagement(); - const profileSSH = profileManager.profile?.ssh; - - if (sshKeysManagement.notMigrated(profileSSH!)) { - const newKeys = sshKeysManagement.migrate(profileSSH as unknown as string); + if (!sshKeysManagement.migrated()) { + console.log(sshKeysManagement.migrated()); + const newKeys = sshKeysManagement.migrate(); await sshKeysManagement.update(newKeys); } } catch (e) { diff --git a/packages/playground/src/weblets/tf_funkwhale.vue b/packages/playground/src/weblets/tf_funkwhale.vue index ef1cde52c1..503f2dd78f 100644 --- a/packages/playground/src/weblets/tf_funkwhale.vue +++ b/packages/playground/src/weblets/tf_funkwhale.vue @@ -112,6 +112,8 @@ require-domain v-model="selectionDetails" /> + +