From 36d79f80da8976bee608bec1b0d3e5d5df08884d Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Thu, 4 Dec 2025 22:28:40 +0200 Subject: [PATCH] Apply changes for benchmark PR --- .../artifacts/base/ArtifactServiceCE.java | 11 ++ .../artifacts/base/ArtifactServiceCEImpl.java | 85 ++++++++--- .../artifacts/base/ArtifactServiceImpl.java | 9 +- .../server/aspect/GitRouteAspect.java | 49 +++++- .../server/exceptions/AppsmithError.java | 2 +- .../git/central/CentralGitServiceCEImpl.java | 144 ++++++++++++++++-- .../controllers/GitArtifactController.java | 6 +- .../controllers/GitArtifactControllerCE.java | 24 +++ 8 files changed, 296 insertions(+), 34 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCE.java index 36a5493611df..49591b1ea295 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCE.java @@ -23,5 +23,16 @@ public interface ArtifactServiceCE { */ Mono 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 saveSshKeyPair(ArtifactType artifactType, String branchedArtifactId); + Mono getSshKey(ArtifactType artifactType, String branchedArtifactId); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCEImpl.java index d08a1fddbef1..dee7463bcd8e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceCEImpl.java @@ -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; @@ -31,11 +34,18 @@ public class ArtifactServiceCEImpl implements ArtifactServiceCE { protected final ArtifactBasedService applicationService; private final AnalyticsService analyticsService; + private final GitDeployKeysRepository gitDeployKeysRepository; + private final SessionUserService sessionUserService; public ArtifactServiceCEImpl( - ArtifactBasedService applicationService, AnalyticsService analyticsService) { + ArtifactBasedService applicationService, + AnalyticsService analyticsService, + GitDeployKeysRepository gitDeployKeysRepository, + SessionUserService sessionUserService) { this.applicationService = applicationService; this.analyticsService = analyticsService; + this.gitDeployKeysRepository = gitDeployKeysRepository; + this.sessionUserService = sessionUserService; } @Override @@ -47,6 +57,49 @@ public ArtifactBasedService getArtifactBasedService(Artifact public Mono 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 saveSshKeyPair(ArtifactType artifactType, String branchedArtifactId) { + Mono 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.")); + } + return Mono.just(gitAuth); + }); + return gitAuthMono.flatMap(gitAuth -> saveSshKeyToArtifact(artifactType, branchedArtifactId, gitAuth)); + } + + private Mono saveSshKeyToArtifact( + ArtifactType artifactType, String branchedArtifactId, GitAuth gitAuth) { ArtifactBasedService artifactBasedService = getArtifactBasedService(artifactType); ArtifactPermission artifactPermission = artifactBasedService.getPermissionService(); final String artifactTypeName = artifactType.name().toLowerCase(); @@ -61,21 +114,21 @@ public Mono 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, @@ -83,24 +136,23 @@ public Mono createOrUpdateSshKeyPair( + " 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()); + .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 eventData = Map.of( - FieldName.APP_MODE, ApplicationMode.EDIT.toString(), FieldName.APPLICATION, artifact); + final Map eventData = + Map.of(FieldName.APP_MODE, ApplicationMode.EDIT.toString(), FieldName.ARTIFACT, artifact); final Map data = Map.of( - FieldName.APPLICATION_ID, + FieldName.ARTIFACT_ID, artifact.getId(), "workspaceId", artifact.getWorkspaceId(), @@ -114,8 +166,7 @@ public Mono createOrUpdateSshKeyPair( log.warn("Error sending ssh key generation data point", e); return Mono.just(artifact); }); - }) - .thenReturn(gitAuth); + }); } /** diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceImpl.java index 515b15085268..d4f978c3c6cd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/base/ArtifactServiceImpl.java @@ -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 applicationService, AnalyticsService analyticsService) { - super(applicationService, analyticsService); + ArtifactBasedService applicationService, + AnalyticsService analyticsService, + GitDeployKeysRepository gitDeployKeysRepository, + SessionUserService sessionUserService) { + super(applicationService, analyticsService, gitDeployKeysRepository, sessionUserService); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/aspect/GitRouteAspect.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/aspect/GitRouteAspect.java index 3e322890846d..4226eae06a23 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/aspect/GitRouteAspect.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/aspect/GitRouteAspect.java @@ -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())); + }); } /** @@ -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); + }); } /** @@ -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. * diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index 54867acac3c9..82981b55d7bc 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -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, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java index 274265be7411..f94e161dd3de 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/central/CentralGitServiceCEImpl.java @@ -77,6 +77,10 @@ import reactor.util.function.Tuple2; import java.io.IOException; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; @@ -135,6 +139,50 @@ public class CentralGitServiceCEImpl implements CentralGitServiceCE { protected static final String ORIGIN = "origin/"; protected static final String REMOTE_NAME_REPLACEMENT = ""; + private boolean isInvalidSshKeyError(Throwable throwable) { + // Log the original error for debugging purposes + log.debug( + "Checking if error is due to invalid SSH key. Error type: {}, Message: {}", + throwable.getClass().getName(), + throwable.getMessage(), + throwable); + + // Inspect the causal chain and messages to distinguish SSH auth errors from network/connectivity issues + Throwable t = throwable; + while (t != null) { + String msg = t.getMessage() == null ? "" : t.getMessage().toLowerCase(); + // Positive signals for invalid key/auth + 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("userauth")) { + return true; + } + + // Negative signals: clear network/offline/connectivity hints + if (t instanceof UnresolvedAddressException + || t instanceof UnknownHostException + || t instanceof ConnectException + || t instanceof NoRouteToHostException) { + return false; + } + + if (msg.contains("unresolvedaddressexception") + || msg.contains("defaultconnectfuture") + || msg.contains("network is unreachable") + || msg.contains("connection refused") + || msg.contains("connection timed out") + || msg.contains("host is down")) { + return false; + } + + t = t.getCause(); + } + return false; + } + protected Mono isRepositoryLimitReachedForWorkspace(String workspaceId, Boolean isRepositoryPrivate) { if (!isRepositoryPrivate) { return Mono.just(FALSE); @@ -1064,8 +1112,13 @@ public Mono connectArtifactToGit( if (error instanceof AppsmithException e) { appsmithException = e; } else if (error instanceof TransportException) { - appsmithException = - new AppsmithException(AppsmithError.INVALID_GIT_SSH_CONFIGURATION); + if (isInvalidSshKeyError(error)) { + appsmithException = + new AppsmithException(AppsmithError.INVALID_GIT_SSH_CONFIGURATION); + } else { + appsmithException = new AppsmithException( + AppsmithError.GIT_GENERIC_ERROR, error.getMessage()); + } } else if (error instanceof InvalidRemoteException) { appsmithException = new AppsmithException( AppsmithError.INVALID_GIT_CONFIGURATION, error.getMessage()); @@ -1204,7 +1257,8 @@ public Mono connectArtifactToGit( // If the push fails remove all the cloned files from local repo return this.detachRemote(baseArtifactId, artifactType, gitType) .flatMap(isDeleted -> { - if (error instanceof TransportException) { + if (error instanceof TransportException + && isInvalidSshKeyError(error)) { return gitAnalyticsUtils .addAnalyticsForGitOperation( AnalyticsEvents.GIT_CONNECT, @@ -1698,10 +1752,53 @@ protected Mono getStatus( fetchRemoteMono = Mono.defer(() -> gitHandlingService .fetchRemoteReferences( jsonTransformationDTO, fetchRemoteDTO, baseGitMetadata.getGitAuth()) - .onErrorResume(error -> Mono.error(new AppsmithException( - AppsmithError.GIT_ACTION_FAILED, - GitCommandConstants.FETCH_REMOTE, - error.getMessage())))); + .onErrorResume(error -> { + log.error( + "Error while fetching remote references for artifact: {}, branch: {}", + baseArtifactId, + finalBranchName, + error); + + AppsmithException appsmithException = null; + if (error instanceof AppsmithException e) { + appsmithException = e; + } else if (error instanceof TransportException) { + if (isInvalidSshKeyError(error)) { + appsmithException = + new AppsmithException(AppsmithError.INVALID_GIT_SSH_CONFIGURATION); + } else { + appsmithException = new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, + GitCommandConstants.FETCH_REMOTE, + error.getMessage()); + } + } else if (error instanceof InvalidRemoteException) { + appsmithException = new AppsmithException( + AppsmithError.INVALID_GIT_CONFIGURATION, error.getMessage()); + } else if (error instanceof TimeoutException) { + appsmithException = new AppsmithException(AppsmithError.GIT_EXECUTION_TIMEOUT); + } else if (error instanceof ClassCastException) { + // To catch TransportHttp cast error in case HTTP URL is passed + // instead of SSH URL + if (error.getMessage() != null + && error.getMessage().contains("TransportHttp")) { + appsmithException = + new AppsmithException(AppsmithError.INVALID_GIT_SSH_URL); + } else { + appsmithException = new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, + GitCommandConstants.FETCH_REMOTE, + error.getMessage()); + } + } else { + appsmithException = new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, + GitCommandConstants.FETCH_REMOTE, + error.getMessage()); + } + + return Mono.error(appsmithException); + })); } return removeDanglingChanges @@ -1975,10 +2072,39 @@ public Mono fetchRemoteChanges( gitType, throwable); + AppsmithException appsmithException = null; + if (throwable instanceof AppsmithException e) { + appsmithException = e; + } else if (throwable instanceof TransportException) { + if (isInvalidSshKeyError(throwable)) { + appsmithException = new AppsmithException(AppsmithError.INVALID_GIT_SSH_CONFIGURATION); + } else { + appsmithException = new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, "fetch", throwable.getMessage()); + } + } else if (throwable instanceof InvalidRemoteException) { + appsmithException = + new AppsmithException(AppsmithError.INVALID_GIT_CONFIGURATION, throwable.getMessage()); + } else if (throwable instanceof TimeoutException) { + appsmithException = new AppsmithException(AppsmithError.GIT_EXECUTION_TIMEOUT); + } else if (throwable instanceof ClassCastException) { + // To catch TransportHttp cast error in case HTTP URL is passed + // instead of SSH URL + if (throwable.getMessage() != null + && throwable.getMessage().contains("TransportHttp")) { + appsmithException = new AppsmithException(AppsmithError.INVALID_GIT_SSH_URL); + } else { + appsmithException = new AppsmithException( + AppsmithError.GIT_ACTION_FAILED, "fetch", throwable.getMessage()); + } + } else { + appsmithException = + new AppsmithException(AppsmithError.GIT_ACTION_FAILED, "fetch", throwable.getMessage()); + } + return gitRedisUtils .releaseFileLock(artifactType, baseArtifactId, isFileLock) - .then(Mono.error(new AppsmithException( - AppsmithError.GIT_ACTION_FAILED, "fetch", throwable.getMessage()))); + .then(Mono.error(appsmithException)); }) .elapsed() .zipWith(currUserMono) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactController.java index e607a0dd151a..06328ee6de75 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactController.java @@ -1,5 +1,6 @@ package com.appsmith.server.git.controllers; +import com.appsmith.server.artifacts.base.ArtifactService; import com.appsmith.server.constants.Url; import com.appsmith.server.git.central.CentralGitService; import com.appsmith.server.git.utils.GitProfileUtils; @@ -12,7 +13,8 @@ @RequestMapping(Url.GIT_ARTIFACT_URL) public class GitArtifactController extends GitArtifactControllerCE { - public GitArtifactController(CentralGitService centralGitService, GitProfileUtils gitProfileUtils) { - super(centralGitService, gitProfileUtils); + public GitArtifactController( + CentralGitService centralGitService, GitProfileUtils gitProfileUtils, ArtifactService artifactService) { + super(centralGitService, gitProfileUtils, artifactService); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java index 5431928b0f02..f44d582c8565 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/git/controllers/GitArtifactControllerCE.java @@ -1,7 +1,10 @@ package com.appsmith.server.git.controllers; import com.appsmith.external.views.Views; +import com.appsmith.server.artifacts.base.ArtifactService; +import com.appsmith.server.constants.ArtifactType; import com.appsmith.server.constants.Url; +import com.appsmith.server.domains.Artifact; import com.appsmith.server.domains.GitAuth; import com.appsmith.server.domains.GitProfile; import com.appsmith.server.dtos.ArtifactImportDTO; @@ -36,6 +39,7 @@ public class GitArtifactControllerCE { protected final CentralGitService centralGitService; protected final GitProfileUtils gitProfileUtils; + protected final ArtifactService artifactService; protected static final GitType GIT_TYPE = GitType.FILE_SYSTEM; @@ -109,4 +113,24 @@ public Mono>> getSupportedKeys() { public Mono> generateKeyForGitImport(@RequestParam(required = false) String keyType) { return centralGitService.generateSSHKey(keyType).map(result -> new ResponseDTO<>(HttpStatus.OK, result)); } + + /** + * Save SSH key pair for an artifact (APPLICATION or PACKAGE). + * This endpoint fetches the SSH key from the database (generated via /import/keys endpoint) + * using the current user's email and saves it to the artifact. + * This ensures the private key never travels through the client for security purposes. + * + * @param artifactId The ID of the artifact (can be base or branched artifact) + * @param artifactType The type of artifact (APPLICATION or PACKAGE) + * @return The saved artifact with updated GitAuth + */ + @JsonView(Views.Public.class) + @PostMapping("/{artifactId}/ssh-keypair") + public Mono> saveSshKeyPair( + @PathVariable String artifactId, @RequestParam ArtifactType artifactType) { + log.info("Saving SSH key pair for artifact: {}, type: {}", artifactId, artifactType); + return artifactService + .saveSshKeyPair(artifactType, artifactId) + .map(result -> new ResponseDTO<>(HttpStatus.CREATED, result)); + } }