From 0e41c52cbb45df7b52e942886174cd2ee48abe98 Mon Sep 17 00:00:00 2001
From: Lansana DIOMANDE <ldiomande@takima.fr>
Date: Mon, 16 May 2022 10:53:56 +0200
Subject: [PATCH] refactor: store gitea ssh key in homedir special folder

---
 .../src/core/callApi.service.ts               |  5 ++++-
 .../deadlock-extension/src/core/config.ts     |  3 +++
 .../deadlock-extension/src/core/controller.ts | 14 +++++++------
 .../src/core/extensionStore.ts                | 18 ++++++++--------
 .../deadlock-extension/src/core/mission.ts    | 15 +++++++------
 .../src/core/sshKeyManager.ts                 | 21 +++++++++++++++----
 6 files changed, 48 insertions(+), 28 deletions(-)

diff --git a/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts b/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts
index afbd314b..3bfdad63 100644
--- a/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/callApi.service.ts
@@ -52,11 +52,14 @@ export default class CallApiService {
               originalConfig._retry = true;
               try {
                 const storedRefreshToken = await this.extensionStore.getRefreshToken();
-                const { accessToken } = await this.keycloackConnection.getToken({
+                const { accessToken, refreshToken } = await this.keycloackConnection.getToken({
                   refreshToken: storedRefreshToken,
                   openLink: Controller.openBrowserWithUrl,
                 });
 
+                await this.extensionStore.setAccessToken(accessToken);
+                await this.extensionStore.setRefreshToken(refreshToken);
+
                 this.callApi.defaults.headers.common['authorization'] = `BEARER ${accessToken}`;
                 return this.callApi(originalConfig);
               } catch (_error: any) {
diff --git a/deadlock-plugins/deadlock-extension/src/core/config.ts b/deadlock-plugins/deadlock-extension/src/core/config.ts
index 9cb8eb83..c0849c9d 100644
--- a/deadlock-plugins/deadlock-extension/src/core/config.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/config.ts
@@ -8,6 +8,9 @@ const onContainer = isDocker();
 
 const deadlockExtensionPath = path.join(homeDir, 'deadlock-extension');
 
+const deadlockConfigPath = path.join(homeDir, '.deadlock');
+export const userSshKeyFolderPath = path.join(deadlockConfigPath, '.ssh');
+
 export const PROJECT_SRC_PATH = onContainer ? '/project' : path.join(homeDir, 'deadlock-extension', '/project');
 
 export const PROJECT_DEADLOCK_DESKTOP_PATH = onContainer
diff --git a/deadlock-plugins/deadlock-extension/src/core/controller.ts b/deadlock-plugins/deadlock-extension/src/core/controller.ts
index fa128393..2f4ea948 100644
--- a/deadlock-plugins/deadlock-extension/src/core/controller.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/controller.ts
@@ -7,6 +7,7 @@ import {
   KEYCLOAK_USER_INFO_URL,
   REGISTRY_MISSION_URL,
 } from '../config';
+
 import { log } from '../recorder/utils';
 import { OPEN_QUICK_SETUP_COMMAND } from '../theia/command';
 import BriefingView from '../view/briefingView';
@@ -81,16 +82,10 @@ export default class Controller {
       }
       this.chooseMissionWorkdir();
     } else {
-      await this.insertSshKeyInMissionWorkdir(folderUri[0].path);
       this.extensionStore.setMissionWorkdir(folderUri[0].path);
     }
   }
 
-  public async insertSshKeyInMissionWorkdir(sshKeyFolderPath) {
-    const { publicKey, privateKey } = await this.callApiService.getUserSshKey();
-    this.sshKeyManager.createSshKeyFiles(publicKey, privateKey, `${sshKeyFolderPath}/.ssh`);
-  }
-
   public async createMissionUserChallengeJson(missionId: string) {}
 
   public async clear() {
@@ -99,6 +94,12 @@ export default class Controller {
     this.quickSetupView.isAlreadyConnected = false;
   }
 
+  public async createSshKeyPairIfNotExist() {
+    if (this.sshKeyManager.isSshKeyPairExist()) return;
+    const { publicKey, privateKey } = await this.callApiService.getUserSshKey();
+    this.sshKeyManager.createSshKeyFiles(publicKey, privateKey);
+  }
+
   public async authenticate() {
     // WARN generate a new device code every time student clicks on log in button. Should I keep it ?\
     // The answer might be 'yes' because when the student is already authenticated, the log in button should be disabled.
@@ -106,6 +107,7 @@ export default class Controller {
     const tokens = await this.connection.getToken({ openLink: Controller.openBrowserWithUrl });
     await this.extensionStore.setAccessToken(tokens.accessToken);
     await this.extensionStore.setRefreshToken(tokens.refreshToken);
+    await this.createSshKeyPairIfNotExist();
     this.quickSetupView.isAlreadyConnected = true;
   }
   public static openBrowserWithUrl(url: string) {
diff --git a/deadlock-plugins/deadlock-extension/src/core/extensionStore.ts b/deadlock-plugins/deadlock-extension/src/core/extensionStore.ts
index 5d4497c9..42e5c724 100644
--- a/deadlock-plugins/deadlock-extension/src/core/extensionStore.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/extensionStore.ts
@@ -42,35 +42,35 @@ export default class ExtensionStore {
     window.showInformationMessage(`Nouveau dossier de stockage des missions: ${path}`);
   }
 
-  public async getAccessToken() {
+  public getAccessToken(): Thenable<string | undefined> {
     return this.readSecret(StoreKey.AccessTokenKey);
   }
 
-  public async getRefreshToken() {
+  public getRefreshToken(): Thenable<string | undefined> {
     return this.readSecret(StoreKey.RefreshTokenKey);
   }
 
-  public async setAccessToken(accessToken: string) {
+  public setAccessToken(accessToken: string): Thenable<void> {
     if (!accessToken) {
       log('Attempt to store undefined access token');
-      return;
+      return Promise.resolve();
     }
     return this.storeSecret(StoreKey.AccessTokenKey, accessToken);
   }
 
-  public async setRefreshToken(refreshToken: string) {
+  public setRefreshToken(refreshToken: string): Thenable<void> {
     if (!refreshToken) {
       log('Attempt to store undefined refresh token');
-      return;
+      return Promise.resolve();
     }
     return this.storeSecret(StoreKey.RefreshTokenKey, refreshToken);
   }
 
-  private async storeSecret(key: StoreKey, value: string) {
-    this.secretStorage.store(key, value);
+  private storeSecret(key: StoreKey, value: string): Thenable<void> {
+    return this.secretStorage.store(key, value);
   }
 
-  private async readSecret(key: StoreKey) {
+  private readSecret(key: StoreKey): Thenable<string | undefined> {
     return this.secretStorage.get(key);
   }
 }
diff --git a/deadlock-plugins/deadlock-extension/src/core/mission.ts b/deadlock-plugins/deadlock-extension/src/core/mission.ts
index d159354d..fc4c4b93 100644
--- a/deadlock-plugins/deadlock-extension/src/core/mission.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/mission.ts
@@ -6,6 +6,7 @@ import * as vscode from 'vscode';
 import { error as err, log } from '../recorder/utils';
 import ExtensionStore from './extensionStore';
 import { User, UserChallengeJson } from '../customTypings/user.model';
+import { userSshKeyFolderPath } from './config';
 
 /**
  * {@link https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback}
@@ -22,8 +23,7 @@ export default class Mission {
   private readonly remoteUserHomeDir: string;
   private readonly remoteMissionDir: any;
   private readonly remoteGiteaWorkDir: string;
-  private readonly userConfigPath: string;
-  private readonly userSshPath: string;
+  private readonly userMissionConfigPath: string;
   private readonly user: User;
   private readonly giteaPublicProperties: GiteaPublicProperties;
 
@@ -35,8 +35,7 @@ export default class Mission {
     const { registryBaseURL, missionId, missionVersion } = params;
     this.hostBaseWorkDir = ExtensionStore.getInstance().getMissionWorkdir() ?? '';
     this.hostMissionDir = `${this.hostBaseWorkDir}/${missionId}`;
-    this.userConfigPath = `${this.hostMissionDir}/.config`;
-    this.userSshPath = `${this.hostBaseWorkDir}/.ssh`;
+    this.userMissionConfigPath = `${this.hostMissionDir}/.config`;
     this.hostMissionDevcontainerDir = `${this.hostMissionDir}/.devcontainer`;
     this.hostMissionDevcontainerFileDir = `${this.hostMissionDevcontainerDir}/devcontainer.json`;
     this.hostMissionMountDir = `${this.hostMissionDir}/mounted`;
@@ -57,7 +56,7 @@ export default class Mission {
   public async setup(options?: Partial<DockerfileSpecific & Base & VSCodespecific & LifecycleScripts>) {
     await fs.promises.mkdir(this.hostMissionDevcontainerDir, { recursive: true });
     await fs.promises.mkdir(this.hostMissionMountDir, { recursive: true });
-    await fs.promises.mkdir(this.userConfigPath, { recursive: true });
+    await fs.promises.mkdir(this.userMissionConfigPath, { recursive: true });
     await this.createDevContainerFile(options);
     await this.createUserChallengeJsonFile();
   }
@@ -72,8 +71,8 @@ export default class Mission {
           extensions: ['Deadlock.deadlock-coding'],
           remoteUser: 'deadlock',
           mounts: [
-            `source=${this.userSshPath},target=/tmp/.ssh,type=bind,consistency=cached`,
-            `source=${this.userConfigPath},target=/home/config/,type=bind,consistency=cached`,
+            `source=${userSshKeyFolderPath},target=/tmp/.ssh,type=bind,consistency=cached`,
+            `source=${this.userMissionConfigPath},target=/home/config/,type=bind,consistency=cached`,
           ],
           userEnvProbe: 'interactiveShell',
           settings: {
@@ -105,7 +104,7 @@ export default class Mission {
 
   private createUserChallengeJsonFile() {
     return fs.promises.writeFile(
-      `${this.userConfigPath}/user-challenge.json`,
+      `${this.userMissionConfigPath}/user-challenge.json`,
       (() => {
         const userChallengeJson: UserChallengeJson = {
           giteaHost: this.giteaPublicProperties.sshHost,
diff --git a/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts b/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts
index 3ebf3a59..38579ba7 100644
--- a/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts
+++ b/deadlock-plugins/deadlock-extension/src/core/sshKeyManager.ts
@@ -1,14 +1,27 @@
 import ExtensionStore from './extensionStore';
 import * as fs from 'fs';
+import { userSshKeyFolderPath } from './config';
 
 export default class SshKeyManager {
   constructor() {}
 
-  public async createSshKeyFiles(publicKey: string, privateKey: string, sshKeyFolderPath: string) {
-    await this.createSshKeyFolderIfNotExist(sshKeyFolderPath);
-    await fs.promises.writeFile(`${sshKeyFolderPath}/id_rsa.pub`, publicKey);
+  public isSshKeyPairExist(): boolean {
+    return this.isPrivateKeyExist() && this.isPublicKeyExist();
+  }
+
+  private isPublicKeyExist(): boolean {
+    return fs.existsSync(`${userSshKeyFolderPath}/id_rsa.pub`);
+  }
+
+  private isPrivateKeyExist(): boolean {
+    return fs.existsSync(`${userSshKeyFolderPath}/id_rsa`);
+  }
+
+  public async createSshKeyFiles(publicKey: string, privateKey: string) {
+    await this.createSshKeyFolderIfNotExist(userSshKeyFolderPath);
+    await fs.promises.writeFile(`${userSshKeyFolderPath}/id_rsa.pub`, publicKey);
 
-    await fs.promises.writeFile(`${sshKeyFolderPath}/id_rsa`, privateKey, { mode: 0o600 });
+    await fs.promises.writeFile(`${userSshKeyFolderPath}/id_rsa`, privateKey, { mode: 0o600 });
   }
   private async createSshKeyFolderIfNotExist(sshKeyFolderPath) {
     if (!this.isSshKeyFolderExist(sshKeyFolderPath)) {
-- 
GitLab