Add password-bootstrap SSH setup for new connections
This commit is contained in:
@@ -3,6 +3,7 @@ package com.sshmanager.service;
|
||||
import com.sshmanager.dto.ConnectionCreateRequest;
|
||||
import com.sshmanager.dto.ConnectionDto;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.exception.InvalidOperationException;
|
||||
import com.sshmanager.repository.ConnectionRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -17,9 +18,13 @@ import java.util.Optional;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ConnectionServiceTest {
|
||||
@@ -30,12 +35,18 @@ class ConnectionServiceTest {
|
||||
@Mock
|
||||
private EncryptionService encryptionService;
|
||||
|
||||
@Mock
|
||||
private SshService sshService;
|
||||
|
||||
@Mock
|
||||
private SshBootstrapService sshBootstrapService;
|
||||
|
||||
@InjectMocks
|
||||
private ConnectionService connectionService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(connectionRepository.save(any(Connection.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
lenient().when(connectionRepository.save(any(Connection.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,11 +102,82 @@ class ConnectionServiceTest {
|
||||
assertNull(saved.getEncryptedPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPasswordBootstrapConnectionSavesGeneratedPrivateKey() {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName("prod");
|
||||
request.setHost("127.0.0.1");
|
||||
request.setPort(22);
|
||||
request.setUsername("root");
|
||||
request.setSetupMode(ConnectionCreateRequest.SetupMode.PASSWORD_BOOTSTRAP);
|
||||
request.setBootstrapPassword("bootstrap-secret");
|
||||
|
||||
when(sshBootstrapService.bootstrapWithPassword(request, 1L))
|
||||
.thenReturn(new SshBootstrapService.BootstrapResult("generated-private-key"));
|
||||
when(encryptionService.encrypt("generated-private-key")).thenReturn("enc-generated-private-key");
|
||||
|
||||
ConnectionDto result = connectionService.create(request, 1L);
|
||||
|
||||
assertNotNull(result);
|
||||
ArgumentCaptor<Connection> captor = ArgumentCaptor.forClass(Connection.class);
|
||||
verify(connectionRepository).save(captor.capture());
|
||||
Connection saved = captor.getValue();
|
||||
|
||||
assertEquals(Connection.AuthType.PRIVATE_KEY, saved.getAuthType());
|
||||
assertNull(saved.getEncryptedPassword());
|
||||
assertEquals("enc-generated-private-key", saved.getEncryptedPrivateKey());
|
||||
assertNull(saved.getPassphrase());
|
||||
verify(sshBootstrapService).bootstrapWithPassword(request, 1L);
|
||||
verify(encryptionService, never()).encrypt("bootstrap-secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPasswordBootstrapConnectionRequiresBootstrapPassword() {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName("prod");
|
||||
request.setHost("127.0.0.1");
|
||||
request.setUsername("root");
|
||||
request.setSetupMode(ConnectionCreateRequest.SetupMode.PASSWORD_BOOTSTRAP);
|
||||
|
||||
InvalidOperationException exception = assertThrows(
|
||||
InvalidOperationException.class,
|
||||
() -> connectionService.create(request, 1L)
|
||||
);
|
||||
|
||||
assertEquals("启用一键免密配置时必须填写初始登录密码", exception.getMessage());
|
||||
verifyNoInteractions(connectionRepository, sshBootstrapService, encryptionService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createPasswordBootstrapConnectionDoesNotSaveWhenBootstrapFails() {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName("prod");
|
||||
request.setHost("127.0.0.1");
|
||||
request.setUsername("root");
|
||||
request.setSetupMode(ConnectionCreateRequest.SetupMode.PASSWORD_BOOTSTRAP);
|
||||
request.setBootstrapPassword("bootstrap-secret");
|
||||
|
||||
when(sshBootstrapService.bootstrapWithPassword(request, 1L))
|
||||
.thenThrow(new InvalidOperationException("免密初始化失败"));
|
||||
|
||||
InvalidOperationException exception = assertThrows(
|
||||
InvalidOperationException.class,
|
||||
() -> connectionService.create(request, 1L)
|
||||
);
|
||||
|
||||
assertEquals("免密初始化失败", exception.getMessage());
|
||||
verify(connectionRepository, never()).save(any(Connection.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSwitchToPrivateKeyClearsPasswordCredential() {
|
||||
Connection existing = new Connection();
|
||||
existing.setId(10L);
|
||||
existing.setUserId(1L);
|
||||
existing.setName("prod");
|
||||
existing.setHost("127.0.0.1");
|
||||
existing.setPort(22);
|
||||
existing.setUsername("root");
|
||||
existing.setAuthType(Connection.AuthType.PASSWORD);
|
||||
existing.setEncryptedPassword("old-password");
|
||||
|
||||
@@ -126,6 +208,10 @@ class ConnectionServiceTest {
|
||||
Connection existing = new Connection();
|
||||
existing.setId(20L);
|
||||
existing.setUserId(1L);
|
||||
existing.setName("prod");
|
||||
existing.setHost("127.0.0.1");
|
||||
existing.setPort(22);
|
||||
existing.setUsername("root");
|
||||
existing.setAuthType(Connection.AuthType.PRIVATE_KEY);
|
||||
existing.setEncryptedPrivateKey("old-key");
|
||||
existing.setPassphrase("old-passphrase");
|
||||
@@ -149,4 +235,30 @@ class ConnectionServiceTest {
|
||||
assertNull(saved.getEncryptedPrivateKey());
|
||||
assertNull(saved.getPassphrase());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateRejectsPasswordBootstrapMode() {
|
||||
Connection existing = new Connection();
|
||||
existing.setId(20L);
|
||||
existing.setUserId(1L);
|
||||
existing.setName("prod");
|
||||
existing.setHost("127.0.0.1");
|
||||
existing.setPort(22);
|
||||
existing.setUsername("root");
|
||||
existing.setAuthType(Connection.AuthType.PASSWORD);
|
||||
existing.setEncryptedPassword("old-password");
|
||||
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setSetupMode(ConnectionCreateRequest.SetupMode.PASSWORD_BOOTSTRAP);
|
||||
|
||||
when(connectionRepository.findById(20L)).thenReturn(Optional.of(existing));
|
||||
|
||||
InvalidOperationException exception = assertThrows(
|
||||
InvalidOperationException.class,
|
||||
() -> connectionService.update(20L, request, 1L)
|
||||
);
|
||||
|
||||
assertEquals("编辑连接时不支持一键免密配置", exception.getMessage());
|
||||
verify(connectionRepository, never()).save(any(Connection.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.sshmanager.service;
|
||||
|
||||
import com.sshmanager.dto.ConnectionCreateRequest;
|
||||
import com.sshmanager.entity.Connection;
|
||||
import com.sshmanager.exception.InvalidOperationException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SshBootstrapServiceTest {
|
||||
|
||||
@Mock
|
||||
private SshService sshService;
|
||||
|
||||
@InjectMocks
|
||||
private SshBootstrapService sshBootstrapService;
|
||||
|
||||
@Test
|
||||
void bootstrapWithPasswordGeneratesKeyAndVerifiesPrivateKeyLogin() throws Exception {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName("prod");
|
||||
request.setHost("127.0.0.1");
|
||||
request.setPort(22);
|
||||
request.setUsername("root");
|
||||
request.setBootstrapPassword("bootstrap-secret");
|
||||
|
||||
when(sshService.executeCommandWithResult(any(Connection.class), anyString(), isNull(), isNull(), anyString()))
|
||||
.thenReturn(new SshService.CommandResult("", "", 0));
|
||||
when(sshService.executeCommandWithResult(any(Connection.class), isNull(), anyString(), isNull(), anyString()))
|
||||
.thenReturn(new SshService.CommandResult("ssh-manager bootstrap ok", "", 0));
|
||||
|
||||
SshBootstrapService.BootstrapResult result = sshBootstrapService.bootstrapWithPassword(request, 1L);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getPrivateKey().contains("PRIVATE KEY"));
|
||||
|
||||
ArgumentCaptor<Connection> connectionCaptor = ArgumentCaptor.forClass(Connection.class);
|
||||
ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> privateKeyCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> commandCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(sshService, times(2)).executeCommandWithResult(
|
||||
connectionCaptor.capture(),
|
||||
passwordCaptor.capture(),
|
||||
privateKeyCaptor.capture(),
|
||||
isNull(),
|
||||
commandCaptor.capture()
|
||||
);
|
||||
|
||||
List<Connection> connections = connectionCaptor.getAllValues();
|
||||
List<String> passwords = passwordCaptor.getAllValues();
|
||||
List<String> privateKeys = privateKeyCaptor.getAllValues();
|
||||
List<String> commands = commandCaptor.getAllValues();
|
||||
|
||||
assertEquals(Connection.AuthType.PASSWORD, connections.get(0).getAuthType());
|
||||
assertEquals("bootstrap-secret", passwords.get(0));
|
||||
assertNull(privateKeys.get(0));
|
||||
assertTrue(commands.get(0).contains("authorized_keys"));
|
||||
|
||||
assertEquals(Connection.AuthType.PRIVATE_KEY, connections.get(1).getAuthType());
|
||||
assertNull(passwords.get(1));
|
||||
assertNotNull(privateKeys.get(1));
|
||||
assertTrue(privateKeys.get(1).contains("PRIVATE KEY"));
|
||||
assertTrue(commands.get(1).contains("ssh-manager bootstrap ok"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bootstrapWithPasswordFailsWhenRemoteAuthorizationCommandFails() throws Exception {
|
||||
ConnectionCreateRequest request = new ConnectionCreateRequest();
|
||||
request.setName("prod");
|
||||
request.setHost("127.0.0.1");
|
||||
request.setPort(22);
|
||||
request.setUsername("root");
|
||||
request.setBootstrapPassword("bootstrap-secret");
|
||||
|
||||
when(sshService.executeCommandWithResult(any(Connection.class), anyString(), isNull(), isNull(), anyString()))
|
||||
.thenReturn(new SshService.CommandResult("", "permission denied", 1));
|
||||
|
||||
InvalidOperationException exception = assertThrows(
|
||||
InvalidOperationException.class,
|
||||
() -> sshBootstrapService.bootstrapWithPassword(request, 1L)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("authorized_keys"));
|
||||
assertTrue(exception.getMessage().contains("permission denied"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user