Skip to content
Merged
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 @@ -3,7 +3,7 @@
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.GitAuth;
import com.appsmith.server.domains.Artifact;
import com.appsmith.server.dtos.ApplicationAccessDTO;
import com.appsmith.server.dtos.GitAuthDTO;
import com.appsmith.server.services.CrudService;
Expand All @@ -29,7 +29,7 @@ public interface ApplicationServiceCE extends CrudService<Application, String> {

Flux<Application> findByWorkspaceIdAndBaseApplicationsInRecentlyUsedOrder(String workspaceId);

Mono<Application> save(Application application);
Mono<Application> save(Artifact application);

Mono<Application> updateApplicationWithPresets(String branchedApplicationId, Application application);

Expand All @@ -53,8 +53,6 @@ Mono<Application> changeViewAccessForAllBranchesByBranchedApplicationId(

Mono<Application> setTransientFields(Application application);

Mono<GitAuth> createOrUpdateSshKeyPair(String branchedApplicationId, String keyType);

Mono<GitAuthDTO> getSshKey(String applicationId);

Mono<Application> findByBranchNameAndBaseApplicationId(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.appsmith.server.applications.base;

import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.external.models.ActionDTO;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.artifacts.base.artifactbased.ArtifactBasedServiceCE;
import com.appsmith.server.artifacts.permissions.ArtifactPermission;
import com.appsmith.server.constants.ApplicationConstants;
import com.appsmith.server.constants.Assets;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationDetail;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Artifact;
import com.appsmith.server.domains.Asset;
import com.appsmith.server.domains.GitArtifactMetadata;
import com.appsmith.server.domains.GitAuth;
Expand Down Expand Up @@ -74,7 +76,7 @@
@Slf4j
@Service
public class ApplicationServiceCEImpl extends BaseService<ApplicationRepository, Application, String>
implements ApplicationServiceCE {
implements ApplicationServiceCE, ArtifactBasedServiceCE<Application> {

private final PolicySolution policySolution;
private final PermissionGroupService permissionGroupService;
Expand Down Expand Up @@ -165,8 +167,9 @@ public Flux<Application> findByWorkspaceId(String workspaceId, AclPermission per
* This method is used to fetch all the applications for a given workspaceId. It also sorts the applications based
* on recently used order.
* For git connected applications only default branched application is returned.
* @param workspaceId workspaceId for which applications are to be fetched
* @return Flux of applications
*
* @param workspaceId workspaceId for which applications are to be fetched
* @return Flux of applications
*/
@Override
public Flux<Application> findByWorkspaceIdAndBaseApplicationsInRecentlyUsedOrder(String workspaceId) {
Expand Down Expand Up @@ -212,7 +215,8 @@ public Flux<Application> findByWorkspaceIdAndBaseApplicationsInRecentlyUsedOrder
}

@Override
public Mono<Application> save(Application application) {
public Mono<Application> save(Artifact artifact) {
Application application = (Application) artifact;
if (!StringUtils.isEmpty(application.getName())) {
application.setSlug(TextUtils.makeSlug(application.getName()));
}
Expand All @@ -227,6 +231,11 @@ public Mono<Application> save(Application application) {
return repository.save(application).flatMap(this::setTransientFields);
}

@Override
public ArtifactPermission getPermissionService() {
return applicationPermission;
}

@Override
public Mono<Application> create(Application object) {
throw new UnsupportedOperationException(
Expand Down Expand Up @@ -649,84 +658,6 @@ private Flux<Application> setTransientFields(Flux<Application> applicationsFlux)
});
}

/**
* Generate SSH private and public keys required to communicate with remote. Keys will be stored only in the
* default/root application only and not the child branched application. This decision is taken because the combined
* size of keys is close to 4kB
*
* @param branchedApplicationId application for which the SSH key needs to be generated
* @return public key which will be used by user to copy to relevant platform
*/
@Override
public Mono<GitAuth> createOrUpdateSshKeyPair(String branchedApplicationId, String keyType) {
GitAuth gitAuth = GitDeployKeyGenerator.generateSSHKey(keyType);
return repository
.findById(branchedApplicationId, applicationPermission.getEditPermission())
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, "application", branchedApplicationId)))
.flatMap(application -> {
GitArtifactMetadata gitData = application.getGitApplicationMetadata();
// Check if the current application is the root application

if (gitData != null
&& !StringUtils.isEmpty(gitData.getDefaultArtifactId())
&& branchedApplicationId.equals(gitData.getDefaultArtifactId())) {
// This is the root application with update SSH key request
gitAuth.setRegeneratedKey(true);
gitData.setGitAuth(gitAuth);
return save(application);
} else if (gitData == null) {
// This is a root application with generate SSH key request
GitArtifactMetadata gitArtifactMetadata = new GitArtifactMetadata();
gitArtifactMetadata.setDefaultApplicationId(branchedApplicationId);
gitArtifactMetadata.setGitAuth(gitAuth);
application.setGitApplicationMetadata(gitArtifactMetadata);
return save(application);
}
// 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
if (StringUtils.isEmpty(gitData.getDefaultArtifactId())) {
throw new AppsmithException(
AppsmithError.INVALID_GIT_CONFIGURATION,
"Unable to find root application, please connect your application to remote repo to resolve this issue.");
}
gitAuth.setRegeneratedKey(true);

return repository
.findById(gitData.getDefaultArtifactId(), applicationPermission.getEditPermission())
.flatMap(baseApplication -> {
GitArtifactMetadata gitArtifactMetadata = baseApplication.getGitApplicationMetadata();
gitArtifactMetadata.setDefaultApplicationId(baseApplication.getId());
gitArtifactMetadata.setGitAuth(gitAuth);
baseApplication.setGitApplicationMetadata(gitArtifactMetadata);
return save(baseApplication);
});
})
.flatMap(application -> {
// Send generate SSH key analytics event
assert application.getId() != null;
final Map<String, Object> eventData = Map.of(
FieldName.APP_MODE, ApplicationMode.EDIT.toString(), FieldName.APPLICATION, application);
final Map<String, Object> data = Map.of(
FieldName.APPLICATION_ID,
application.getId(),
"organizationId",
application.getWorkspaceId(),
"isRegeneratedKey",
gitAuth.isRegeneratedKey(),
FieldName.EVENT_DATA,
eventData);
return analyticsService
.sendObjectEvent(AnalyticsEvents.GENERATE_SSH_KEY, application, data)
.onErrorResume(e -> {
log.warn("Error sending ssh key generation data point", e);
return Mono.just(application);
});
})
.thenReturn(gitAuth);
}

/**
* Method to get the SSH public key
*
Expand Down Expand Up @@ -1025,9 +956,10 @@ public Flux<String> findBranchedApplicationIdsByBaseApplicationId(String baseApp

/**
* Gets branched application with the right permission set based on mode of application
*
* @param defaultApplicationId : default app id
* @param branchName : branch name of the application
* @param mode : is it edit mode or view mode
* @param branchName : branch name of the application
* @param mode : is it edit mode or view mode
* @return : returns a publisher of branched application
*/
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.appsmith.server.applications.base;

import com.appsmith.server.artifacts.base.artifactbased.ArtifactBasedService;
import com.appsmith.server.domains.Application;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.services.AnalyticsService;
Expand All @@ -20,7 +22,8 @@

@Slf4j
@Service
public class ApplicationServiceImpl extends ApplicationServiceCECompatibleImpl implements ApplicationService {
public class ApplicationServiceImpl extends ApplicationServiceCECompatibleImpl
implements ApplicationService, ArtifactBasedService<Application> {

public ApplicationServiceImpl(
Validator validator,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.appsmith.server.artifacts.base;

public interface ArtifactService extends ArtifactServiceCE {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.appsmith.server.artifacts.base;

import com.appsmith.server.artifacts.base.artifactbased.ArtifactBasedService;
import com.appsmith.server.constants.ArtifactType;
import com.appsmith.server.domains.Artifact;
import com.appsmith.server.domains.GitAuth;
import reactor.core.publisher.Mono;

public interface ArtifactServiceCE {

/**
* This method returns the appropriate ArtifactBasedService based on the type of artifact.
*/
ArtifactBasedService<? extends Artifact> getArtifactBasedService(ArtifactType artifactType);

/**
* Generate SSH private and public keys required to communicate with remote. Keys will be stored only in the
* default/root application only and not the child branched application. This decision is taken because the combined
* size of keys is close to 4kB
*
* @return public key which will be used by user to copy to relevant platform
*/
Mono<GitAuth> createOrUpdateSshKeyPair(ArtifactType artifactType, String branchedArtifactId, String keyType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.appsmith.server.artifacts.base;

import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.server.artifacts.base.artifactbased.ArtifactBasedService;
import com.appsmith.server.artifacts.permissions.ArtifactPermission;
import com.appsmith.server.constants.ArtifactType;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Artifact;
import com.appsmith.server.domains.GitArtifactMetadata;
import com.appsmith.server.domains.GitAuth;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.GitDeployKeyGenerator;
import com.appsmith.server.services.AnalyticsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

import java.util.Map;

@Slf4j
@Service
public class ArtifactServiceCEImpl implements ArtifactServiceCE {

protected final ArtifactBasedService<Application> applicationService;
private final AnalyticsService analyticsService;

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

@Override
public ArtifactBasedService<? extends Artifact> getArtifactBasedService(ArtifactType artifactType) {
return applicationService;
}

@Override
public Mono<GitAuth> createOrUpdateSshKeyPair(
ArtifactType artifactType, String branchedArtifactId, String keyType) {
GitAuth gitAuth = GitDeployKeyGenerator.generateSSHKey(keyType);
ArtifactBasedService<? extends Artifact> artifactBasedService = getArtifactBasedService(artifactType);
ArtifactPermission artifactPermission = artifactBasedService.getPermissionService();
final String artifactTypeName = artifactType.name().toLowerCase();
return artifactBasedService
.findById(branchedArtifactId, artifactPermission.getEditPermission())
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, artifactTypeName, branchedArtifactId)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: ACL_NO_RESOURCE_FOUND is a preferred one

Edit: It's fine to leave as is, since it's not aligned with the intent of the PR

.flatMap(artifact -> {
GitArtifactMetadata gitData = artifact.getGitArtifactMetadata();
// Check if the current artifact is the root artifact

if (gitData != null
&& StringUtils.hasLength(gitData.getDefaultArtifactId())
&& branchedArtifactId.equals(gitData.getDefaultArtifactId())) {
// This is the root application 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
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
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());
gitArtifactMetadata.setGitAuth(gitAuth);
baseApplication.setGitArtifactMetadata(gitArtifactMetadata);
return artifactBasedService.save(baseApplication);
});
})
Comment on lines +73 to +92
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Based on the code analysis, I can now provide a final response:

Add optimistic locking to prevent concurrent SSH key updates
Add @Version field to GitAuth entity and use Spring's optimistic locking to handle concurrent modifications. This will ensure that if two processes try to update the SSH key simultaneously, one will fail with an OptimisticLockingFailureException rather than silently overwriting each other's changes.

Example locations that need attention:

  • GitAuth.java - Add version field
  • ArtifactServiceCEImpl.createOrUpdateSshKeyPair() - Handle the optimistic locking exception and retry if needed
🔗 Analysis chain

Flag potential concurrency scenario
If multiple clients simultaneously call this function for the same artifact, two processes might generate different SSH keys in rapid succession. Consider adding steps (e.g., optimistic locking or more robust concurrency control) to avoid inconsistent or lost updates.


Let me search for relevant code to verify the concurrency concerns.

Let me also check for any transaction or version control mechanisms in the artifact service:

This will help us understand if there are any existing concurrency controls and the full context of the SSH key generation flow.


Let me try a different search pattern to find the relevant code:


Let me check the underlying service implementation to understand if there are any concurrency controls:


Let me try one more search to find the Application entity and service implementation:

🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
# Search for the class and method definition to understand the context
ast-grep --pattern 'class $_ {
  $$$
  generateSSHKeyPair($$$) {
    $$$
  }
  $$$
}'

# Search for any existing concurrency control mechanisms
rg -i "lock|synchronized|atomic|concurrent" app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/

Length of output: 205


Script:

# Search for transaction annotations or version fields
rg -i "@transaction|@version|optimisticlock" app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/

# Check the entity/model class for version fields
ast-grep --pattern 'class $_ {
  $$$
  @Version
  private $_ version;
  $$$
}'

Length of output: 199


Script:

#!/bin/bash
# Search for the artifact service implementation
rg -l "class.*ArtifactService.*Impl" app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/ | xargs cat

# Search for the entity class that might contain version field
fd "Artifact.*\.java" app/server/appsmith-server/src/main/java/com/appsmith/server/ -x cat {}

# Look for any save operations in the service
rg -A 5 "save\(" app/server/appsmith-server/src/main/java/com/appsmith/server/artifacts/

Length of output: 65961


Script:

# Search for the ArtifactBasedService implementation
fd "ArtifactBasedService.*Impl\.java" app/server/appsmith-server/src/main/java/ -x cat {}

# Search for any MongoDB optimistic locking annotations
rg -i "@version|@optimisticlock" app/server/appsmith-server/src/main/java/com/appsmith/server/domains/

Length of output: 198


Script:

# Search for Application entity class
rg -l "class Application" app/server/appsmith-server/src/main/java/com/appsmith/server/domains/ | xargs cat

# Search for ApplicationService implementation
fd "ApplicationService.*Impl\.java" app/server/appsmith-server/src/main/java/ -x cat {}

# Look for any transaction or locking mechanisms in the service layer
rg -i "@transactional|@lock|synchronized" app/server/appsmith-server/src/main/java/com/appsmith/server/services/

Length of output: 77806

.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> data = Map.of(
FieldName.APPLICATION_ID,
artifact.getId(),
"organizationId",
artifact.getWorkspaceId(),
"isRegeneratedKey",
gitAuth.isRegeneratedKey(),
FieldName.EVENT_DATA,
eventData);
return analyticsService
.sendObjectEvent(AnalyticsEvents.GENERATE_SSH_KEY, artifact, data)
.onErrorResume(e -> {
log.warn("Error sending ssh key generation data point", e);
return Mono.just(artifact);
});
})
.thenReturn(gitAuth);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.appsmith.server.artifacts.base;

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

@Service
public class ArtifactServiceImpl extends ArtifactServiceCEImpl implements ArtifactService {

public ArtifactServiceImpl(
ArtifactBasedService<Application> applicationService, AnalyticsService analyticsService) {
super(applicationService, analyticsService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.appsmith.server.artifacts.base.artifactbased;

import com.appsmith.server.domains.Artifact;

public interface ArtifactBasedService<T extends Artifact> extends ArtifactBasedServiceCE<T> {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.appsmith.server.artifacts.base.artifactbased;

import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.artifacts.permissions.ArtifactPermission;
import com.appsmith.server.domains.Artifact;
import reactor.core.publisher.Mono;

public interface ArtifactBasedServiceCE<T extends Artifact> {

Mono<T> findById(String id, AclPermission aclPermission);

Mono<T> save(Artifact artifact);

ArtifactPermission getPermissionService();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.appsmith.server.artifacts.permissions;

public interface ArtifactPermission extends ArtifactPermissionCE {}
Loading