Skip to content

Commit

Permalink
CHE-2059 : Each time a workspace is created, register/associate ssh k…
Browse files Browse the repository at this point in the history
…ey (#2949)

* CHE-2059 : Each time a workspace is created, register/associate ssh key

Change-Id: Ifddfe5398cffd1af31aa93beb5d0674b29270f4f
Signed-off-by: Florent BENOIT <fbenoit@codenvy.com>
  • Loading branch information
benoitf authored Nov 4, 2016
1 parent b3ef0a2 commit 4a86b5e
Show file tree
Hide file tree
Showing 5 changed files with 409 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@

import org.eclipse.che.api.workspace.server.RecipeScriptDownloadService;
import org.eclipse.che.ide.ext.machine.server.ssh.KeysInjector;
import org.eclipse.che.ide.ext.machine.server.ssh.WorkspaceSshKeys;
import org.eclipse.che.inject.DynaModule;

@DynaModule
public class MachineModule extends AbstractModule {
protected void configure() {
bind(KeysInjector.class).asEagerSingleton();

bind(WorkspaceSshKeys.class).asEagerSingleton();
bind(RecipeScriptDownloadService.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -66,15 +67,41 @@ public void start() {
@Override
public void onEvent(MachineStatusEvent event) {
if (event.getEventType() == MachineStatusEvent.EventType.RUNNING) {
final Instance machine;
try {
final Instance machine = environmentEngine.getMachine(event.getWorkspaceId(),
event.getMachineId());
machine = environmentEngine.getMachine(event.getWorkspaceId(),
event.getMachineId());
} catch (NotFoundException e) {
LOG.error("Unable to find machine: " + e.getLocalizedMessage(), e);
return;
}

try {
// get machine keypairs
List<SshPairImpl> sshPairs = sshManager.getPairs(machine.getOwner(), "machine");
final List<String> publicKeys = sshPairs.stream()
final List<String> publicMachineKeys = sshPairs.stream()
.filter(sshPair -> sshPair.getPublicKey() != null)
.map(SshPairImpl::getPublicKey)
.collect(Collectors.toList());

// get workspace keypair (if any)
SshPairImpl sshWorkspacePair = null;
try {
sshWorkspacePair = sshManager.getPair(machine.getOwner(), "workspace", event.getWorkspaceId());
} catch (NotFoundException e) {
LOG.debug("No ssh key associated to the workspace", e);
}

// build list of all pairs.
final List<String> publicKeys;
if (sshWorkspacePair != null && sshWorkspacePair.getPublicKey() != null) {
publicKeys = new ArrayList<>(publicMachineKeys.size() + 1);
publicKeys.add(sshWorkspacePair.getPublicKey());
publicKeys.addAll(publicMachineKeys);
} else {
publicKeys = publicMachineKeys;
}

if (publicKeys.isEmpty()) {
return;
}
Expand All @@ -100,7 +127,7 @@ public void onEvent(MachineStatusEvent event) {
}
}
});
} catch (IOException | ServerException | NotFoundException e) {
} catch (IOException | ServerException e) {
LOG.error(e.getLocalizedMessage(), e);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*******************************************************************************
* Copyright (c) 2012-2016 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ext.machine.server.ssh;

import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
import org.eclipse.che.api.ssh.server.SshManager;
import org.eclipse.che.api.workspace.server.event.WorkspaceCreatedEvent;
import org.eclipse.che.api.workspace.server.event.WorkspaceRemovedEvent;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
* Creates SSH keyPair each time a workspace is created (and delete it when workspace is removed)
*
* @author Florent Benoit
*/
@Singleton // must be eager
public class WorkspaceSshKeys {

/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(WorkspaceSshKeys.class);

/**
* The event service used to subscribe on create and delete events on any workspaces.
*/
private final EventService eventService;

/**
* SSH manager handling ssh keys. Used to generate ssh keypair or remove the default keypair when workspace is removed.
*/
private final SshManager sshManager;


/**
* Default injection by using event service and ssh manager.
*
* @param eventService
* used to get CREATE/DELETE events for workspace
* @param sshManager
* used to generate/remove default ssh keys
*/
@Inject
public WorkspaceSshKeys(final EventService eventService, final SshManager sshManager) {
this.eventService = eventService;
this.sshManager = sshManager;
}

/**
* When component is initialized, subscribe to workspace events in order to generate/delete ssh keys.
*/
@PostConstruct
public void start() {
eventService.subscribe(new EventSubscriber<WorkspaceCreatedEvent>() {
@Override
public void onEvent(WorkspaceCreatedEvent workspaceCreatedEvent) {
// Register default SSH keypair for this workspace.
try {
sshManager.generatePair(EnvironmentContext.getCurrent().getSubject().getUserId(), "workspace",
workspaceCreatedEvent.getWorkspace().getId());
} catch (ServerException | ConflictException e) {
// Conflict shouldn't happen as workspace id is new each time.
LOG.error("Unable to generate a default ssh pair for the workspace with ID {}",
workspaceCreatedEvent.getWorkspace().getId(), e);
}
}
});

eventService.subscribe(new EventSubscriber<WorkspaceRemovedEvent>() {
@Override
public void onEvent(WorkspaceRemovedEvent workspaceRemovedEvent) {
// Unregister default SSH keypair for this workspace (if any)
try {
sshManager.removePair(EnvironmentContext.getCurrent().getSubject().getUserId(), "workspace",
workspaceRemovedEvent.getWorkspace().getId());
} catch (NotFoundException e) {
LOG.debug("Do not remove default keypair from workspace {} as it is not existing (workspace ID {})",
workspaceRemovedEvent.getWorkspace().getConfig().getName(),
workspaceRemovedEvent.getWorkspace().getId());
} catch (ServerException e) {
LOG.error("Error when trying to remove default ssh pair for the workspace {} (workspace ID {})",
workspaceRemovedEvent.getWorkspace().getConfig().getName(), workspaceRemovedEvent.getWorkspace().getId());
}
}
});

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*******************************************************************************/
package org.eclipse.che.ide.ext.machine.server.ssh;

import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.model.machine.MachineRuntimeInfo;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.notification.EventSubscriber;
Expand Down Expand Up @@ -128,6 +129,7 @@ public void shouldNotInjectSshKeysWhenThereAreNotAnyPair() throws Exception {

verify(environmentEngine).getMachine(eq(WORKSPACE_ID), eq(MACHINE_ID));
verify(sshManager).getPairs(eq(OWNER_ID), eq("machine"));
verify(sshManager).getPair(eq(OWNER_ID), eq("workspace"), eq(WORKSPACE_ID));
verifyZeroInteractions(docker, environmentEngine, sshManager);
}

Expand All @@ -142,6 +144,7 @@ public void shouldNotInjectSshKeysWhenThereAreNotAnyPairWithPublicKey() throws E

verify(environmentEngine).getMachine(eq(WORKSPACE_ID), eq(MACHINE_ID));
verify(sshManager).getPairs(eq(OWNER_ID), eq("machine"));
verify(sshManager).getPair(eq(OWNER_ID), eq("workspace"), eq(WORKSPACE_ID));
verifyZeroInteractions(docker, environmentEngine, sshManager);
}

Expand All @@ -157,6 +160,7 @@ public void shouldInjectSshKeysWhenThereAreAnyPairWithNotNullPublicKey() throws

verify(environmentEngine).getMachine(eq(WORKSPACE_ID), eq(MACHINE_ID));
verify(sshManager).getPairs(eq(OWNER_ID), eq("machine"));
verify(sshManager).getPair(eq(OWNER_ID), eq("workspace"), eq(WORKSPACE_ID));

ArgumentCaptor<CreateExecParams> argumentCaptor = ArgumentCaptor.forClass(CreateExecParams.class);
verify(docker).createExec(argumentCaptor.capture());
Expand All @@ -167,6 +171,97 @@ public void shouldInjectSshKeysWhenThereAreAnyPairWithNotNullPublicKey() throws
verifyZeroInteractions(docker, environmentEngine, sshManager);
}

/**
* Validate the usecase: There is a workspace sshkeypair but no machine keypair (empty list)
* Expect that the workspace public key is injected.
*/
@Test
public void shouldInjectSshKeysWhenThereIsOnlyWorkspaceKey() throws Exception {
// no machine key pairs
when(sshManager.getPairs(anyString(), eq("machine")))
.thenReturn(Collections.emptyList());

// workspace keypair
when(sshManager.getPair(anyString(), eq("workspace"), anyString()))
.thenReturn(new SshPairImpl(OWNER_ID, "workspace", WORKSPACE_ID, "publicKeyWorkspace", null));


subscriber.onEvent(newDto(MachineStatusEvent.class).withEventType(MachineStatusEvent.EventType.RUNNING)
.withMachineId(MACHINE_ID)
.withWorkspaceId(WORKSPACE_ID));

verify(environmentEngine).getMachine(eq(WORKSPACE_ID), eq(MACHINE_ID));
// check calls for machine and workspace ssh pairs
verify(sshManager).getPairs(eq(OWNER_ID), eq("machine"));
verify(sshManager).getPair(eq(OWNER_ID), eq("workspace"), eq(WORKSPACE_ID));

ArgumentCaptor<CreateExecParams> argumentCaptor = ArgumentCaptor.forClass(CreateExecParams.class);
verify(docker).createExec(argumentCaptor.capture());
assertEquals(argumentCaptor.getValue().getCmd(), new String[] {"/bin/bash", "-c", "mkdir ~/.ssh/ -p" +
"&& echo 'publicKeyWorkspace' >> ~/.ssh/authorized_keys"});
verify(docker).startExec(eq(StartExecParams.create(EXEC_ID)), anyObject());
verifyZeroInteractions(docker, environmentEngine, sshManager);
}

/**
* Validate the usecase: There is a workspace sshkeypair (without public key) but there is a machine keypair
* Expect that only the machine keypair is injected (as workspace keypair has no public key).
*/
@Test
public void shouldInjectSshKeysWhenThereIsNoPublicWorkspaceKeyButMachineKeys() throws Exception {
// no machine key pairs
when(sshManager.getPairs(anyString(), eq("machine")))
.thenReturn(Arrays.asList(new SshPairImpl(OWNER_ID, "machine", "myPair", "publicKey1", null)));

// workspace keypair without public key
when(sshManager.getPair(anyString(), eq("workspace"), anyString()))
.thenReturn(new SshPairImpl(OWNER_ID, "workspace", WORKSPACE_ID, null, null));


subscriber.onEvent(newDto(MachineStatusEvent.class).withEventType(MachineStatusEvent.EventType.RUNNING)
.withMachineId(MACHINE_ID)
.withWorkspaceId(WORKSPACE_ID));

verify(environmentEngine).getMachine(eq(WORKSPACE_ID), eq(MACHINE_ID));
// check calls for machine and workspace ssh pairs
verify(sshManager).getPairs(eq(OWNER_ID), eq("machine"));
verify(sshManager).getPair(eq(OWNER_ID), eq("workspace"), eq(WORKSPACE_ID));

ArgumentCaptor<CreateExecParams> argumentCaptor = ArgumentCaptor.forClass(CreateExecParams.class);
verify(docker).createExec(argumentCaptor.capture());
assertEquals(argumentCaptor.getValue().getCmd(), new String[] {"/bin/bash", "-c", "mkdir ~/.ssh/ -p" +
"&& echo 'publicKey1' >> ~/.ssh/authorized_keys"});
verify(docker).startExec(eq(StartExecParams.create(EXEC_ID)), anyObject());
verifyZeroInteractions(docker, environmentEngine, sshManager);
}

/**
* Validate the usecase of no workspace keypair (notfound exception) and no machine keypair
* Expect no ssh keys are injected
*/
@Test
public void shouldNotInjectSshKeysWhenThereIsNoWorkspaceKey() throws Exception {
// no machine key pairs
when(sshManager.getPairs(anyString(), eq("machine")))
.thenReturn(Collections.emptyList());

// no workspace keypair
when(sshManager.getPair(anyString(), eq("workspace"), anyString()))
.thenThrow(NotFoundException.class);


subscriber.onEvent(newDto(MachineStatusEvent.class).withEventType(MachineStatusEvent.EventType.RUNNING)
.withMachineId(MACHINE_ID)
.withWorkspaceId(WORKSPACE_ID));

verify(environmentEngine).getMachine(eq(WORKSPACE_ID), eq(MACHINE_ID));
// check calls for machine and workspace ssh pairs
verify(sshManager).getPairs(eq(OWNER_ID), eq("machine"));
verify(sshManager).getPair(eq(OWNER_ID), eq("workspace"), eq(WORKSPACE_ID));

verifyZeroInteractions(docker, environmentEngine, sshManager);
}

@Test
public void shouldSendMessageInMachineLoggerWhenSomeErrorOcursOnKeysInjection() throws Exception {
when(sshManager.getPairs(anyString(), anyString()))
Expand Down
Loading

0 comments on commit 4a86b5e

Please sign in to comment.