This is an automated email from the ASF dual-hosted git repository. yasith pushed a commit to branch feat/airavata-service-layer in repository https://gitbox.apache.org/repos/asf/airavata.git
commit cde0907f9828e5cc1c8716391efc979cc15c0b3a Author: yasithdev <[email protected]> AuthorDate: Thu Mar 26 10:14:34 2026 -0500 feat: add CredentialService Extract credential business logic from AiravataServerHandler into CredentialService, using RequestContext instead of AuthzToken claims and direct sharingHandler.userHasAccess() for permission checks. --- .../service/credential/CredentialService.java | 209 +++++++++++++++++++++ .../service/credential/CredentialServiceTest.java | 151 +++++++++++++++ 2 files changed, 360 insertions(+) diff --git a/airavata-api/src/main/java/org/apache/airavata/service/credential/CredentialService.java b/airavata-api/src/main/java/org/apache/airavata/service/credential/CredentialService.java new file mode 100644 index 0000000000..2c2420253b --- /dev/null +++ b/airavata-api/src/main/java/org/apache/airavata/service/credential/CredentialService.java @@ -0,0 +1,209 @@ +package org.apache.airavata.service.credential; + +import org.apache.airavata.common.utils.ServerSettings; +import org.apache.airavata.credential.store.server.CredentialStoreServerHandler; +import org.apache.airavata.model.credential.store.CredentialSummary; +import org.apache.airavata.model.credential.store.PasswordCredential; +import org.apache.airavata.model.credential.store.SSHCredential; +import org.apache.airavata.model.credential.store.SummaryType; +import org.apache.airavata.model.group.ResourcePermissionType; +import org.apache.airavata.model.group.ResourceType; +import org.apache.airavata.service.context.RequestContext; +import org.apache.airavata.service.exception.ServiceAuthorizationException; +import org.apache.airavata.service.exception.ServiceException; +import org.apache.airavata.sharing.registry.models.Entity; +import org.apache.airavata.sharing.registry.models.EntitySearchField; +import org.apache.airavata.sharing.registry.models.SearchCondition; +import org.apache.airavata.sharing.registry.models.SearchCriteria; +import org.apache.airavata.sharing.registry.server.SharingRegistryServerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class CredentialService { + + private static final Logger logger = LoggerFactory.getLogger(CredentialService.class); + + private final CredentialStoreServerHandler credentialHandler; + private final SharingRegistryServerHandler sharingHandler; + + public CredentialService(CredentialStoreServerHandler credentialHandler, SharingRegistryServerHandler sharingHandler) { + this.credentialHandler = credentialHandler; + this.sharingHandler = sharingHandler; + } + + public String generateAndRegisterSSHKeys(RequestContext ctx, String description) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userName = ctx.getUserId(); + try { + SSHCredential sshCredential = new SSHCredential(); + sshCredential.setUsername(userName); + sshCredential.setGatewayId(gatewayId); + sshCredential.setDescription(description); + String key = credentialHandler.addSSHCredential(sshCredential); + try { + Entity entity = new Entity(); + entity.setEntityId(key); + entity.setDomainId(gatewayId); + entity.setEntityTypeId(gatewayId + ":" + ResourceType.CREDENTIAL_TOKEN); + entity.setOwnerId(userName + "@" + gatewayId); + entity.setName(key); + entity.setDescription(description); + sharingHandler.createEntity(entity); + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + logger.error("Rolling back ssh key creation for user " + userName + " and description [" + description + "]"); + credentialHandler.deleteSSHCredential(key, gatewayId); + throw new ServiceException("Failed to create sharing registry record"); + } + logger.debug("Airavata generated SSH keys for gateway : " + gatewayId + " and for user : " + userName); + return key; + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error occurred while registering SSH Credential. More info : " + e.getMessage(), e); + } + } + + public String registerPwdCredential(RequestContext ctx, String loginUserName, String password, String description) + throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userName = ctx.getUserId(); + try { + PasswordCredential pwdCredential = new PasswordCredential(); + pwdCredential.setPortalUserName(userName); + pwdCredential.setLoginUserName(loginUserName); + pwdCredential.setPassword(password); + pwdCredential.setDescription(description); + pwdCredential.setGatewayId(gatewayId); + String key = credentialHandler.addPasswordCredential(pwdCredential); + try { + Entity entity = new Entity(); + entity.setEntityId(key); + entity.setDomainId(gatewayId); + entity.setEntityTypeId(gatewayId + ":" + ResourceType.CREDENTIAL_TOKEN); + entity.setOwnerId(userName + "@" + gatewayId); + entity.setName(key); + entity.setDescription(description); + sharingHandler.createEntity(entity); + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + logger.error("Rolling back password registration for user " + userName + " and description [" + description + "]"); + credentialHandler.deletePWDCredential(key, gatewayId); + throw new ServiceException("Failed to create sharing registry record"); + } + logger.debug("Airavata generated PWD credential for gateway : " + gatewayId + " and for user : " + loginUserName); + return key; + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error occurred while registering PWD Credential. More info : " + e.getMessage(), e); + } + } + + public CredentialSummary getCredentialSummary(RequestContext ctx, String tokenId) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userName = ctx.getUserId(); + try { + if (!userHasAccess(gatewayId, userName, tokenId, ResourcePermissionType.READ)) { + logger.info("User " + userName + " not allowed to access credential store token " + tokenId); + throw new ServiceAuthorizationException("User does not have permission to access this resource"); + } + CredentialSummary credentialSummary = credentialHandler.getCredentialSummary(tokenId, gatewayId); + logger.debug("Airavata retrieved the credential summary for token " + tokenId + " GatewayId: " + gatewayId); + return credentialSummary; + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error retrieving credential summary for token " + tokenId + ". GatewayId: " + gatewayId + " More info : " + e.getMessage(), e); + } + } + + public List<CredentialSummary> getAllCredentialSummaries(RequestContext ctx, SummaryType type) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userName = ctx.getUserId(); + try { + List<SearchCriteria> filters = new ArrayList<>(); + SearchCriteria searchCriteria = new SearchCriteria(); + searchCriteria.setSearchField(EntitySearchField.ENTITY_TYPE_ID); + searchCriteria.setSearchCondition(SearchCondition.EQUAL); + searchCriteria.setValue(gatewayId + ":" + ResourceType.CREDENTIAL_TOKEN.name()); + filters.add(searchCriteria); + List<String> accessibleTokenIds = + sharingHandler.searchEntities(gatewayId, userName + "@" + gatewayId, filters, 0, -1).stream() + .map(p -> p.getEntityId()) + .collect(Collectors.toList()); + List<CredentialSummary> credentialSummaries = + credentialHandler.getAllCredentialSummaries(type, accessibleTokenIds, gatewayId); + logger.debug("Airavata successfully retrieved credential summaries of type " + type + " GatewayId: " + gatewayId); + return credentialSummaries; + } catch (Exception e) { + throw new ServiceException("Error retrieving credential summaries of type " + type + ". GatewayId: " + gatewayId + " More info : " + e.getMessage(), e); + } + } + + public boolean deleteSSHPubKey(RequestContext ctx, String airavataCredStoreToken) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userName = ctx.getUserId(); + try { + if (!userHasAccess(gatewayId, userName, airavataCredStoreToken, ResourcePermissionType.WRITE)) { + logger.info("User " + userName + " not allowed to delete (no WRITE permission) credential store token " + airavataCredStoreToken); + throw new ServiceAuthorizationException("User does not have permission to delete this resource."); + } + logger.debug("Airavata deleted SSH pub key for gateway Id : " + gatewayId + " and with token id : " + airavataCredStoreToken); + return credentialHandler.deleteSSHCredential(airavataCredStoreToken, gatewayId); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error occurred while deleting SSH credential. More info : " + e.getMessage(), e); + } + } + + public boolean deletePWDCredential(RequestContext ctx, String airavataCredStoreToken) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userName = ctx.getUserId(); + try { + if (!userHasAccess(gatewayId, userName, airavataCredStoreToken, ResourcePermissionType.WRITE)) { + logger.info("User " + userName + " not allowed to delete (no WRITE permission) credential store token " + airavataCredStoreToken); + throw new ServiceAuthorizationException("User does not have permission to delete this resource."); + } + logger.debug("Airavata deleted PWD credential for gateway Id : " + gatewayId + " and with token id : " + airavataCredStoreToken); + return credentialHandler.deletePWDCredential(airavataCredStoreToken, gatewayId); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error occurred while deleting PWD credential. More info : " + e.getMessage(), e); + } + } + + private boolean userHasAccess(String gatewayId, String userName, String entityId, ResourcePermissionType permissionType) { + String userId = userName + "@" + gatewayId; + try { + boolean hasOwnerAccess = sharingHandler.userHasAccess( + gatewayId, userId, entityId, gatewayId + ":" + ResourcePermissionType.OWNER); + if (permissionType.equals(ResourcePermissionType.WRITE)) { + return hasOwnerAccess + || sharingHandler.userHasAccess(gatewayId, userId, entityId, gatewayId + ":" + ResourcePermissionType.WRITE); + } else if (permissionType.equals(ResourcePermissionType.READ)) { + return hasOwnerAccess + || sharingHandler.userHasAccess(gatewayId, userId, entityId, gatewayId + ":" + ResourcePermissionType.READ); + } else if (permissionType.equals(ResourcePermissionType.OWNER)) { + return hasOwnerAccess; + } + return false; + } catch (Exception e) { + throw new RuntimeException("Unable to check if user has access", e); + } + } + + private boolean isSharingEnabled() { + try { + return ServerSettings.isEnableSharing(); + } catch (Exception e) { + return false; + } + } +} diff --git a/airavata-api/src/test/java/org/apache/airavata/service/credential/CredentialServiceTest.java b/airavata-api/src/test/java/org/apache/airavata/service/credential/CredentialServiceTest.java new file mode 100644 index 0000000000..25424bdd92 --- /dev/null +++ b/airavata-api/src/test/java/org/apache/airavata/service/credential/CredentialServiceTest.java @@ -0,0 +1,151 @@ +package org.apache.airavata.service.credential; + +import org.apache.airavata.credential.store.server.CredentialStoreServerHandler; +import org.apache.airavata.model.credential.store.CredentialSummary; +import org.apache.airavata.model.credential.store.SummaryType; +import org.apache.airavata.model.group.ResourcePermissionType; +import org.apache.airavata.service.context.RequestContext; +import org.apache.airavata.service.exception.ServiceAuthorizationException; +import org.apache.airavata.sharing.registry.models.Entity; +import org.apache.airavata.sharing.registry.server.SharingRegistryServerHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CredentialServiceTest { + + @Mock CredentialStoreServerHandler credentialHandler; + @Mock SharingRegistryServerHandler sharingHandler; + + CredentialService credentialService; + RequestContext ctx; + + @BeforeEach + void setUp() { + credentialService = new CredentialService(credentialHandler, sharingHandler); + ctx = new RequestContext("testUser", "testGateway", "token123", + Map.of("userName", "testUser", "gatewayId", "testGateway")); + } + + @Test + void generateAndRegisterSSHKeys_returnsToken() throws Exception { + when(credentialHandler.addSSHCredential(any())).thenReturn("ssh-token-1"); + + String result = credentialService.generateAndRegisterSSHKeys(ctx, "my ssh key"); + + assertEquals("ssh-token-1", result); + verify(credentialHandler).addSSHCredential(any()); + verify(sharingHandler).createEntity(any()); + } + + @Test + void generateAndRegisterSSHKeys_rollsBackOnSharingFailure() throws Exception { + when(credentialHandler.addSSHCredential(any())).thenReturn("ssh-token-1"); + doThrow(new RuntimeException("sharing error")).when(sharingHandler).createEntity(any()); + + assertThrows(Exception.class, () -> credentialService.generateAndRegisterSSHKeys(ctx, "my ssh key")); + verify(credentialHandler).deleteSSHCredential("ssh-token-1", "testGateway"); + } + + @Test + void registerPwdCredential_returnsToken() throws Exception { + when(credentialHandler.addPasswordCredential(any())).thenReturn("pwd-token-1"); + + String result = credentialService.registerPwdCredential(ctx, "loginUser", "secret", "my pwd"); + + assertEquals("pwd-token-1", result); + verify(credentialHandler).addPasswordCredential(any()); + verify(sharingHandler).createEntity(any()); + } + + @Test + void registerPwdCredential_rollsBackOnSharingFailure() throws Exception { + when(credentialHandler.addPasswordCredential(any())).thenReturn("pwd-token-1"); + doThrow(new RuntimeException("sharing error")).when(sharingHandler).createEntity(any()); + + assertThrows(Exception.class, () -> credentialService.registerPwdCredential(ctx, "loginUser", "secret", "my pwd")); + verify(credentialHandler).deletePWDCredential("pwd-token-1", "testGateway"); + } + + @Test + void getCredentialSummary_delegatesToCredentialHandler() throws Exception { + when(sharingHandler.userHasAccess(eq("testGateway"), eq("testUser@testGateway"), eq("tok-1"), + eq("testGateway:" + ResourcePermissionType.OWNER))).thenReturn(true); + CredentialSummary summary = new CredentialSummary(); + summary.setToken("tok-1"); + when(credentialHandler.getCredentialSummary("tok-1", "testGateway")).thenReturn(summary); + + CredentialSummary result = credentialService.getCredentialSummary(ctx, "tok-1"); + + assertNotNull(result); + assertEquals("tok-1", result.getToken()); + verify(credentialHandler).getCredentialSummary("tok-1", "testGateway"); + } + + @Test + void getCredentialSummary_throwsAuthorizationExceptionWhenNoAccess() throws Exception { + when(sharingHandler.userHasAccess(any(), any(), any(), any())).thenReturn(false); + + assertThrows(ServiceAuthorizationException.class, () -> credentialService.getCredentialSummary(ctx, "tok-1")); + verify(credentialHandler, never()).getCredentialSummary(any(), any()); + } + + @Test + void deleteSSHPubKey_delegatesToCredentialHandler() throws Exception { + when(sharingHandler.userHasAccess(eq("testGateway"), eq("testUser@testGateway"), eq("tok-1"), + eq("testGateway:" + ResourcePermissionType.OWNER))).thenReturn(true); + when(credentialHandler.deleteSSHCredential("tok-1", "testGateway")).thenReturn(true); + + boolean result = credentialService.deleteSSHPubKey(ctx, "tok-1"); + + assertTrue(result); + verify(credentialHandler).deleteSSHCredential("tok-1", "testGateway"); + } + + @Test + void deleteSSHPubKey_throwsAuthorizationExceptionWhenNoAccess() throws Exception { + when(sharingHandler.userHasAccess(any(), any(), any(), any())).thenReturn(false); + + assertThrows(ServiceAuthorizationException.class, () -> credentialService.deleteSSHPubKey(ctx, "tok-1")); + verify(credentialHandler, never()).deleteSSHCredential(any(), any()); + } + + @Test + void deletePWDCredential_delegatesToCredentialHandler() throws Exception { + when(sharingHandler.userHasAccess(eq("testGateway"), eq("testUser@testGateway"), eq("tok-1"), + eq("testGateway:" + ResourcePermissionType.OWNER))).thenReturn(true); + when(credentialHandler.deletePWDCredential("tok-1", "testGateway")).thenReturn(true); + + boolean result = credentialService.deletePWDCredential(ctx, "tok-1"); + + assertTrue(result); + verify(credentialHandler).deletePWDCredential("tok-1", "testGateway"); + } + + @Test + void getAllCredentialSummaries_delegatesToCredentialHandler() throws Exception { + Entity entity = new Entity(); + entity.setEntityId("tok-1"); + when(sharingHandler.searchEntities(eq("testGateway"), eq("testUser@testGateway"), any(), eq(0), eq(-1))) + .thenReturn(List.of(entity)); + CredentialSummary summary = new CredentialSummary(); + summary.setToken("tok-1"); + when(credentialHandler.getAllCredentialSummaries(eq(SummaryType.SSH), any(), eq("testGateway"))) + .thenReturn(List.of(summary)); + + List<CredentialSummary> result = credentialService.getAllCredentialSummaries(ctx, SummaryType.SSH); + + assertEquals(1, result.size()); + verify(credentialHandler).getAllCredentialSummaries(eq(SummaryType.SSH), any(), eq("testGateway")); + } +}
