diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java b/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java index 926a673d..9415b8b7 100644 --- a/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java +++ b/server/src/main/java/com/epam/aidial/core/server/controller/DeploymentPostController.java @@ -12,6 +12,7 @@ import com.epam.aidial.core.server.data.ApiKeyData; import com.epam.aidial.core.server.data.ErrorData; import com.epam.aidial.core.server.function.BaseRequestFunction; +import com.epam.aidial.core.server.function.CollectRequestApplicationFilesFn; import com.epam.aidial.core.server.function.CollectRequestAttachmentsFn; import com.epam.aidial.core.server.function.CollectRequestDataFn; import com.epam.aidial.core.server.function.CollectResponseAttachmentsFn; @@ -73,7 +74,8 @@ public DeploymentPostController(Proxy proxy, ProxyContext context) { new ApplyDefaultDeploymentSettingsFn(proxy, context), new EnhanceAssistantRequestFn(proxy, context), new EnhanceModelRequestFn(proxy, context), - new AppendApplicationPropertiesFn(proxy, context)); + new AppendApplicationPropertiesFn(proxy, context), + new CollectRequestApplicationFilesFn(proxy, context)); } public Future handle(String deploymentId, String deploymentApi) { @@ -267,6 +269,7 @@ void handleRequestBody(Buffer requestBody) { if (ProxyUtil.processChain(tree, enhancementFunctions)) { context.setRequestBody(Buffer.buffer(ProxyUtil.MAPPER.writeValueAsBytes(tree))); } + proxy.getApiKeyStore().assignPerRequestApiKey(context.getProxyApiKeyData()); } catch (Throwable e) { if (e instanceof HttpException httpException) { respond(httpException.getStatus(), httpException.getMessage()); diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java b/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java index c6894025..8875bfb1 100644 --- a/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java +++ b/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java @@ -12,11 +12,11 @@ import com.epam.aidial.core.server.service.ApplicationService; import com.epam.aidial.core.server.service.PermissionDeniedException; import com.epam.aidial.core.server.service.ResourceNotFoundException; -import com.epam.aidial.core.server.service.ShareService; import com.epam.aidial.core.server.util.ApplicationTypeSchemaProcessingException; import com.epam.aidial.core.server.util.ApplicationTypeSchemaUtils; import com.epam.aidial.core.server.util.ProxyUtil; import com.epam.aidial.core.server.util.ResourceDescriptorFactory; +import com.epam.aidial.core.server.validation.ApplicationTypeResourceException; import com.epam.aidial.core.server.validation.ApplicationTypeSchemaValidationException; import com.epam.aidial.core.storage.data.MetadataBase; import com.epam.aidial.core.storage.data.ResourceItemMetadata; @@ -37,6 +37,7 @@ import java.util.List; import static com.epam.aidial.core.storage.http.HttpStatus.BAD_REQUEST; +import static com.epam.aidial.core.storage.http.HttpStatus.FORBIDDEN; import static com.epam.aidial.core.storage.http.HttpStatus.INTERNAL_SERVER_ERROR; @Slf4j @@ -130,11 +131,9 @@ private Future getResource(ResourceDescriptor descriptor, boolean hasWriteAcc Future> responseFuture = (descriptor.getType() == ResourceTypes.APPLICATION) ? getApplicationData(descriptor, hasWriteAccess, etagHeader) : getResourceData(descriptor, etagHeader); - responseFuture.onSuccess(pair -> { - context.putHeader(HttpHeaders.ETAG, pair.getKey().getEtag()) - .exposeHeaders() - .respond(HttpStatus.OK, pair.getValue()); - }) + responseFuture.onSuccess(pair -> context.putHeader(HttpHeaders.ETAG, pair.getKey().getEtag()) + .exposeHeaders() + .respond(HttpStatus.OK, pair.getValue())) .onFailure(error -> handleError(descriptor, error)); return Future.succeededFuture(); @@ -177,7 +176,9 @@ private void validateCustomApplication(Application application) { throw new HttpException(BAD_REQUEST, "No read access to file: " + file.getUrl()); }); } catch (ValidationException | IllegalArgumentException | ApplicationTypeSchemaValidationException e) { - throw new HttpException(BAD_REQUEST, " Custom application validation failed", e); + throw new HttpException(BAD_REQUEST, "Custom application validation failed", e); + } catch (ApplicationTypeResourceException e) { + throw new HttpException(FORBIDDEN, "Failed to access application resource " + e.getResourceUri(), e); } catch (ApplicationTypeSchemaProcessingException e) { throw new HttpException(INTERNAL_SERVER_ERROR, "Custom application processing exception", e); } @@ -232,11 +233,9 @@ private Future putResource(ResourceDescriptor descriptor) { }); } - responseFuture.onSuccess((metadata) -> { - context.putHeader(HttpHeaders.ETAG, metadata.getEtag()) - .exposeHeaders() - .respond(HttpStatus.OK, metadata); - }) + responseFuture.onSuccess((metadata) -> context.putHeader(HttpHeaders.ETAG, metadata.getEtag()) + .exposeHeaders() + .respond(HttpStatus.OK, metadata)) .onFailure(error -> handleError(descriptor, error)); return Future.succeededFuture(); diff --git a/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestApplicationFilesFn.java b/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestApplicationFilesFn.java new file mode 100644 index 00000000..652ec272 --- /dev/null +++ b/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestApplicationFilesFn.java @@ -0,0 +1,67 @@ +package com.epam.aidial.core.server.function; + +import com.epam.aidial.core.config.Application; +import com.epam.aidial.core.config.Deployment; +import com.epam.aidial.core.server.Proxy; +import com.epam.aidial.core.server.ProxyContext; +import com.epam.aidial.core.server.data.ApiKeyData; +import com.epam.aidial.core.server.data.AutoSharedData; +import com.epam.aidial.core.server.security.AccessService; +import com.epam.aidial.core.server.util.ApplicationTypeSchemaUtils; +import com.epam.aidial.core.server.validation.ApplicationTypeResourceException; +import com.epam.aidial.core.storage.data.ResourceAccessType; +import com.epam.aidial.core.storage.http.HttpException; +import com.epam.aidial.core.storage.http.HttpStatus; +import com.epam.aidial.core.storage.resource.ResourceDescriptor; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + + +@Slf4j +public class CollectRequestApplicationFilesFn extends BaseRequestFunction { + public CollectRequestApplicationFilesFn(Proxy proxy, ProxyContext context) { + super(proxy, context); + } + + @Override + public Boolean apply(ObjectNode tree) { + try { + Deployment deployment = context.getDeployment(); + if (!(deployment instanceof Application application && application.getApplicationTypeSchemaId() != null)) { + return false; + } + if (application.getApplicationProperties() == null) { + throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, "Typed application's properties not set"); + } + List resources = ApplicationTypeSchemaUtils.getServerFiles(context.getConfig(), application, proxy.getEncryptionService(), + proxy.getResourceService()); + appendFilesToProxyApiKeyData(resources); + return false; + } catch (HttpException ex) { + throw ex; + } catch (ApplicationTypeResourceException ex) { + throw new HttpException(HttpStatus.FORBIDDEN, ex.getMessage()); + } catch (Exception e) { + throw new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } + + private void appendFilesToProxyApiKeyData(List resources) { + ApiKeyData apiKeyData = context.getProxyApiKeyData(); + for (ResourceDescriptor resource : resources) { + String resourceUrl = resource.getUrl(); + AccessService accessService = proxy.getAccessService(); + if (accessService.hasReadAccess(resource, context)) { + if (resource.isFolder()) { + apiKeyData.getAttachedFolders().put(resourceUrl, new AutoSharedData(ResourceAccessType.READ_ONLY)); + } else { + apiKeyData.getAttachedFiles().put(resourceUrl, new AutoSharedData(ResourceAccessType.READ_ONLY)); + } + } else { + throw new HttpException(HttpStatus.FORBIDDEN, "Access denied to the file %s".formatted(resourceUrl)); + } + } + } +} diff --git a/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestAttachmentsFn.java b/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestAttachmentsFn.java index 420a538f..d4981d80 100644 --- a/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestAttachmentsFn.java +++ b/server/src/main/java/com/epam/aidial/core/server/function/CollectRequestAttachmentsFn.java @@ -28,9 +28,6 @@ public CollectRequestAttachmentsFn(Proxy proxy, ProxyContext context) { @Override public Boolean apply(ObjectNode tree) { ProxyUtil.collectAttachedFilesFromRequest(tree, this::processAttachedFile); - // assign api key data after processing attachments - ApiKeyData destApiKeyData = context.getProxyApiKeyData(); - proxy.getApiKeyStore().assignPerRequestApiKey(destApiKeyData); return false; } diff --git a/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java b/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java index b488ff29..da4d78a2 100644 --- a/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java +++ b/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java @@ -4,6 +4,7 @@ import com.epam.aidial.core.config.Config; import com.epam.aidial.core.server.ProxyContext; import com.epam.aidial.core.server.security.EncryptionService; +import com.epam.aidial.core.server.validation.ApplicationTypeResourceException; import com.epam.aidial.core.server.validation.ApplicationTypeSchemaValidationException; import com.epam.aidial.core.server.validation.DialFileKeyword; import com.epam.aidial.core.server.validation.DialMetaKeyword; @@ -154,8 +155,19 @@ public static void replaceCustomAppFiles(Application application, Map getServerFiles(Config config, Application application, EncryptionService encryptionService, + ResourceService resourceService) { + return getFiles(config, application, encryptionService, resourceService, ListCollector.FileCollectorType.ONLY_SERVER_FILES); + } + + public static List getFiles(Config config, Application application, EncryptionService encryptionService, + ResourceService resourceService) { + return getFiles(config, application, encryptionService, resourceService, ListCollector.FileCollectorType.ALL_FILES); + } + @SuppressWarnings("unchecked") - public static List getFiles(Config config, Application application, EncryptionService encryptionService, ResourceService resourceService) { + private static List getFiles(Config config, Application application, EncryptionService encryptionService, + ResourceService resourceService, ListCollector.FileCollectorType collectorName) { try { String customApplicationSchema = getCustomApplicationSchemaOrThrow(config, application); if (customApplicationSchema == null) { @@ -169,7 +181,7 @@ public static List getFiles(Config config, Application appli if (!validationResult.isEmpty()) { throw new ApplicationTypeSchemaValidationException("Failed to validate custom app against the schema", validationResult); } - ListCollector propsCollector = (ListCollector) collectorContext.getCollectorMap().get("file"); + ListCollector propsCollector = (ListCollector) collectorContext.getCollectorMap().get(collectorName.getValue()); if (propsCollector == null) { return Collections.emptyList(); } @@ -178,15 +190,15 @@ public static List getFiles(Config config, Application appli try { ResourceDescriptor descriptor = ResourceDescriptorFactory.fromAnyUrl(item, encryptionService); if (!descriptor.isFolder() && !resourceService.hasResource(descriptor)) { - throw new ApplicationTypeSchemaValidationException("Resource listed as dependent to the application not found or inaccessible: " + item); + throw new ApplicationTypeResourceException("Resource listed as dependent to the application not found or inaccessible", item); } result.add(descriptor); } catch (IllegalArgumentException e) { - throw new ApplicationTypeSchemaValidationException("Failed to get resource descriptor for url: " + item, e); + throw new ApplicationTypeResourceException("Failed to get resource descriptor for url", item, e); } } return result; - } catch (ApplicationTypeSchemaValidationException e) { + } catch (ApplicationTypeSchemaValidationException | ApplicationTypeResourceException e) { throw e; } catch (Exception e) { throw new ApplicationTypeSchemaProcessingException("Failed to obtain list of files attached to the custom app", e); diff --git a/server/src/main/java/com/epam/aidial/core/server/validation/ApplicationTypeResourceException.java b/server/src/main/java/com/epam/aidial/core/server/validation/ApplicationTypeResourceException.java new file mode 100644 index 00000000..1fdd093b --- /dev/null +++ b/server/src/main/java/com/epam/aidial/core/server/validation/ApplicationTypeResourceException.java @@ -0,0 +1,26 @@ +package com.epam.aidial.core.server.validation; + +import lombok.Getter; + + +/** + * Exception thrown when there is an issue with a resource associated with an application type. + * This exception is typically used when a resource listed as dependent to the application + * is not found, inaccessible, or there is a failure in obtaining the resource descriptor. + */ +@Getter +public class ApplicationTypeResourceException extends RuntimeException { + + private final String resourceUri; + + public ApplicationTypeResourceException(String message, String resourceUri, Throwable cause) { + super(message, cause); + this.resourceUri = resourceUri; + } + + public ApplicationTypeResourceException(String message, String resourceUri) { + super(message); + this.resourceUri = resourceUri; + } + +} \ No newline at end of file diff --git a/server/src/main/java/com/epam/aidial/core/server/validation/ApplicationTypeSchemaValidationException.java b/server/src/main/java/com/epam/aidial/core/server/validation/ApplicationTypeSchemaValidationException.java index 5b895648..f1f4dab6 100644 --- a/server/src/main/java/com/epam/aidial/core/server/validation/ApplicationTypeSchemaValidationException.java +++ b/server/src/main/java/com/epam/aidial/core/server/validation/ApplicationTypeSchemaValidationException.java @@ -14,10 +14,6 @@ public ApplicationTypeSchemaValidationException(String message, Set "dial:file"; private final Boolean value; + private final Boolean isServerProp; public DialFileCollectorValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, Keyword keyword, ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ERROR_MESSAGE_TYPE, keyword, validationContext, suppressSubSchemaRetrieval); this.value = schemaNode.booleanValue(); + JsonNode metaNode = parentSchema.getSchemaNode().get("dial:meta"); + JsonNode propertyKindNode = (metaNode != null) ? metaNode.get("dial:propertyKind") : null; + this.isServerProp = (propertyKindNode != null) && propertyKindNode.asText().equalsIgnoreCase("server"); } @Override @@ -46,9 +50,14 @@ public DialFileCollectorValidator(SchemaLocation schemaLocation, JsonNodePath ev public Set validate(ExecutionContext executionContext, JsonNode jsonNode, JsonNode jsonNode1, JsonNodePath jsonNodePath) { if (value) { CollectorContext collectorContext = executionContext.getCollectorContext(); - ListCollector serverPropsCollector = (ListCollector) collectorContext.getCollectorMap() - .computeIfAbsent("file", k -> new ListCollector()); - serverPropsCollector.combine(List.of(jsonNode.asText())); + ListCollector fileCollector = (ListCollector) collectorContext.getCollectorMap() + .computeIfAbsent(ListCollector.FileCollectorType.ALL_FILES.getValue(), k -> new ListCollector()); + fileCollector.combine(List.of(jsonNode.asText())); + if (isServerProp) { + ListCollector serverFileCollector = (ListCollector) collectorContext.getCollectorMap() + .computeIfAbsent(ListCollector.FileCollectorType.ONLY_SERVER_FILES.getValue(), k -> new ListCollector()); + serverFileCollector.combine(List.of(jsonNode.asText())); + } } return Set.of(); } diff --git a/server/src/main/java/com/epam/aidial/core/server/validation/ListCollector.java b/server/src/main/java/com/epam/aidial/core/server/validation/ListCollector.java index 396c24f9..6e15930d 100644 --- a/server/src/main/java/com/epam/aidial/core/server/validation/ListCollector.java +++ b/server/src/main/java/com/epam/aidial/core/server/validation/ListCollector.java @@ -1,11 +1,25 @@ package com.epam.aidial.core.server.validation; import com.networknt.schema.Collector; +import lombok.Getter; import java.util.ArrayList; import java.util.List; public class ListCollector implements Collector> { + + @Getter + public enum FileCollectorType { + ALL_FILES("file"), + ONLY_SERVER_FILES("server_file"); + + private final String value; + + FileCollectorType(String value) { + this.value = value; + } + } + private final List references = new ArrayList<>(); @Override diff --git a/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java b/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java index ce49fed1..4921d405 100644 --- a/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java +++ b/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java @@ -403,7 +403,7 @@ void testApplicationWithTypeSchemaCreation_Failed_FailAccessFile() { "description": "My application description" } """); - Assertions.assertEquals(400, response.status()); + Assertions.assertEquals(403, response.status()); } @Test diff --git a/server/src/test/java/com/epam/aidial/core/server/function/CollectRequestApplicationFilesFnTest.java b/server/src/test/java/com/epam/aidial/core/server/function/CollectRequestApplicationFilesFnTest.java new file mode 100644 index 00000000..adeee9bb --- /dev/null +++ b/server/src/test/java/com/epam/aidial/core/server/function/CollectRequestApplicationFilesFnTest.java @@ -0,0 +1,183 @@ +package com.epam.aidial.core.server.function; + +import com.epam.aidial.core.config.Application; +import com.epam.aidial.core.config.Config; +import com.epam.aidial.core.config.Deployment; +import com.epam.aidial.core.server.Proxy; +import com.epam.aidial.core.server.ProxyContext; +import com.epam.aidial.core.server.data.ApiKeyData; +import com.epam.aidial.core.server.security.AccessService; +import com.epam.aidial.core.storage.http.HttpException; +import com.epam.aidial.core.storage.service.ResourceService; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CollectRequestApplicationFilesFnTest { + + @Mock + private Proxy proxy; + + @Mock + private ProxyContext context; + + @Mock + private Config config; + + @Mock + private AccessService accessService; + + @Mock + private ResourceService resourceService; + + + @InjectMocks + private CollectRequestApplicationFilesFn fn; + + private Application application; + private ObjectNode tree; + + private final String schema = """ + { + "$schema": "https://dial.epam.com/application_type_schemas/schema#", + "$id": "https://mydial.epam.com/custom_application_schemas/specific_application_type", + "dial:applicationTypeEditorUrl": "https://mydial.epam.com/specific_application_type_editor", + "dial:applicationTypeDisplayName": "Specific Application Type", + "dial:applicationTypeCompletionEndpoint": "http://specific_application_service/opeani/v1/completion", + "properties": { + "clientFile": { + "type": "string", + "format": "dial-file-encoded", + "dial:meta": { + "dial:propertyKind": "client", + "dial:propertyOrder": 1 + }, + "dial:file": true + }, + "serverFile": { + "type": "string", + "format": "dial-file-encoded", + "dial:meta": { + "dial:propertyKind": "server", + "dial:propertyOrder": 2 + }, + "dial:file": true + } + }, + "required": [ + "clientFile" + ] + }"""; + + @BeforeEach + void setUp() { + application = new Application(); + tree = JsonNodeFactory.instance.objectNode(); + } + + @Test + void apply_doesNotAppendsFilesToApiKeyData_whenDeploymentIsNotApplication() { + Deployment deployment = mock(Deployment.class); + when(context.getDeployment()).thenReturn(deployment); + + assertFalse(fn.apply(tree)); + verify(proxy, never()).getApiKeyStore(); + verify(context, never()).getProxyApiKeyData(); + } + + @Test + void apply_doesNotAppendsFilesToApiKeyData_whenApplicationHasNoCustomSchemaId() { + when(context.getDeployment()).thenReturn(application); + application.setApplicationTypeSchemaId(null); + + assertFalse(fn.apply(tree)); + verify(proxy, never()).getApiKeyStore(); + verify(context, never()).getProxyApiKeyData(); + } + + @Test + void apply_appendsFilesToApiKeyData_whenApplicationHasCustomSchemaId() { + when(proxy.getAccessService()).thenReturn(accessService); + when(proxy.getResourceService()).thenReturn(resourceService); + when(context.getProxyApiKeyData()).thenReturn(new ApiKeyData()); + when(context.getConfig()).thenReturn(config); + String serverFile = "files/public/valid-file-path/valid-sub-path/valid%20file%20name2.ext"; + when(context.getDeployment()).thenReturn(application); + application.setApplicationTypeSchemaId(URI.create("customSchemaId")); + Map customProps = new HashMap<>(); + customProps.put("clientFile", "files/public/valid-file-path/valid-sub-path/valid%20file%20name1.ext"); + customProps.put("serverFile", serverFile); + application.setApplicationProperties(customProps); + when(config.getCustomApplicationSchema(eq(URI.create("customSchemaId")))).thenReturn(schema); + when(accessService.hasReadAccess(any(), any())).thenReturn(true); + when(resourceService.hasResource(any())).thenReturn(true); + ApiKeyData apiKeyData = new ApiKeyData(); + when(context.getProxyApiKeyData()).thenReturn(apiKeyData); + + boolean result = fn.apply(tree); + + assertFalse(result); + assertNotNull(apiKeyData.getAttachedFiles().get(serverFile)); + assertEquals(1, apiKeyData.getAttachedFiles().size()); + } + + @Test + void apply_throws_whenResourceServiceHasNoResource() { + when(proxy.getResourceService()).thenReturn(resourceService); + when(context.getConfig()).thenReturn(config); + String serverFile = "files/public/valid-file-path/valid-sub-path/valid%20file%20name2.ext"; + when(context.getDeployment()).thenReturn(application); + application.setApplicationTypeSchemaId(URI.create("customSchemaId")); + Map customProps = new HashMap<>(); + customProps.put("clientFile", "files/public/valid-file-path/valid-sub-path/valid%20file%20name1.ext"); + customProps.put("serverFile", serverFile); + application.setApplicationProperties(customProps); + when(config.getCustomApplicationSchema(eq(URI.create("customSchemaId")))).thenReturn(schema); + when(resourceService.hasResource(any())).thenReturn(false); //Has No Resource + + Assertions.assertThrows(HttpException.class, () -> fn.apply(tree)); + } + + @Test + void apply_throws_whenAccessServiceHasNoReadAccess() { + when(proxy.getAccessService()).thenReturn(accessService); + when(proxy.getResourceService()).thenReturn(resourceService); + when(context.getProxyApiKeyData()).thenReturn(new ApiKeyData()); + when(context.getConfig()).thenReturn(config); + String serverFile = "files/public/valid-file-path/valid-sub-path/valid%20file%20name2.ext"; + when(context.getDeployment()).thenReturn(application); + application.setApplicationTypeSchemaId(URI.create("customSchemaId")); + Map customProps = new HashMap<>(); + customProps.put("clientFile", "files/public/valid-file-path/valid-sub-path/valid%20file%20name1.ext"); + customProps.put("serverFile", serverFile); + application.setApplicationProperties(customProps); + when(config.getCustomApplicationSchema(eq(URI.create("customSchemaId")))).thenReturn(schema); + when(accessService.hasReadAccess(any(), any())).thenReturn(false); //Has no Read Access + when(resourceService.hasResource(any())).thenReturn(true); + ApiKeyData apiKeyData = new ApiKeyData(); + when(context.getProxyApiKeyData()).thenReturn(apiKeyData); + + Assertions.assertThrows(HttpException.class, () -> fn.apply(tree)); + } +} \ No newline at end of file diff --git a/server/src/test/java/com/epam/aidial/core/server/function/enhancement/AppendCustomApplicationPropertiesFnTest.java b/server/src/test/java/com/epam/aidial/core/server/function/enhancement/AppendCustomApplicationPropertiesFnTest.java index 9cf05f0e..c5237b1f 100644 --- a/server/src/test/java/com/epam/aidial/core/server/function/enhancement/AppendCustomApplicationPropertiesFnTest.java +++ b/server/src/test/java/com/epam/aidial/core/server/function/enhancement/AppendCustomApplicationPropertiesFnTest.java @@ -43,34 +43,37 @@ public class AppendCustomApplicationPropertiesFnTest { private AppendApplicationPropertiesFn function; - private final String schema = "{" - + "\"$schema\": \"https://dial.epam.com/application_type_schemas/schema#\"," - + "\"$id\": \"https://mydial.epam.com/custom_application_schemas/specific_application_type\"," - + "\"dial:applicationTypeEditorUrl\": \"https://mydial.epam.com/specific_application_type_editor\"," - + "\"dial:applicationTypeDisplayName\": \"Specific Application Type\"," - + "\"dial:applicationTypeCompletionEndpoint\": \"http://specific_application_service/opeani/v1/completion\"," - + "\"properties\": {" - + " \"clientFile\": {" - + " \"type\": \"string\"," - + " \"format\": \"dial-file-encoded\"," - + " \"dial:meta\": {" - + " \"dial:propertyKind\": \"client\"," - + " \"dial:propertyOrder\": 1" - + " }," - + " \"dial:file\" : true" - + " }," - + " \"serverFile\": {" - + " \"type\": \"string\"," - + " \"format\": \"dial-file-encoded\"," - + " \"dial:meta\": {" - + " \"dial:propertyKind\": \"server\"," - + " \"dial:propertyOrder\": 2" - + " }," - + " \"dial:file\" : true" - + " }" - + "}," - + "\"required\": [\"clientFile\"]" - + "}"; + private final String schema = """ + { + "$schema": "https://dial.epam.com/application_type_schemas/schema#", + "$id": "https://mydial.epam.com/custom_application_schemas/specific_application_type", + "dial:applicationTypeEditorUrl": "https://mydial.epam.com/specific_application_type_editor", + "dial:applicationTypeDisplayName": "Specific Application Type", + "dial:applicationTypeCompletionEndpoint": "http://specific_application_service/opeani/v1/completion", + "properties": { + "clientFile": { + "type": "string", + "format": "dial-file-encoded", + "dial:meta": { + "dial:propertyKind": "client", + "dial:propertyOrder": 1 + }, + "dial:file": true + }, + "serverFile": { + "type": "string", + "format": "dial-file-encoded", + "dial:meta": { + "dial:propertyKind": "server", + "dial:propertyOrder": 2 + }, + "dial:file": true + } + }, + "required": [ + "clientFile" + ] + }"""; @BeforeEach void setUp() { diff --git a/server/src/test/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtilsTest.java b/server/src/test/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtilsTest.java index f6a9a5ac..81a06f88 100644 --- a/server/src/test/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtilsTest.java +++ b/server/src/test/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtilsTest.java @@ -6,6 +6,7 @@ import com.epam.aidial.core.server.ProxyContext; import com.epam.aidial.core.server.security.AccessService; import com.epam.aidial.core.server.security.EncryptionService; +import com.epam.aidial.core.server.validation.ApplicationTypeResourceException; import com.epam.aidial.core.server.validation.ApplicationTypeSchemaValidationException; import com.epam.aidial.core.storage.resource.ResourceDescriptor; import com.epam.aidial.core.storage.service.ResourceService; @@ -32,34 +33,35 @@ public class ApplicationTypeSchemaUtilsTest { private ResourceDescriptor resource; private AccessService accessService; - private final String schema = "{" - + "\"$schema\": \"https://dial.epam.com/application_type_schemas/schema#\"," - + "\"$id\": \"https://mydial.epam.com/custom_application_schemas/specific_application_type\"," - + "\"dial:applicationTypeEditorUrl\": \"https://mydial.epam.com/specific_application_type_editor\"," - + "\"dial:applicationTypeDisplayName\": \"Specific Application Type\"," - + "\"dial:applicationTypeCompletionEndpoint\": \"http://specific_application_service/opeani/v1/completion\"," - + "\"properties\": {" - + " \"clientFile\": {" - + " \"type\": \"string\"," - + " \"format\": \"dial-file-encoded\"," - + " \"dial:meta\": {" - + " \"dial:propertyKind\": \"client\"," - + " \"dial:propertyOrder\": 1" - + " }," - + " \"dial:file\" : true" - + " }," - + " \"serverFile\": {" - + " \"type\": \"string\"," - + " \"format\": \"dial-file-encoded\"," - + " \"dial:meta\": {" - + " \"dial:propertyKind\": \"server\"," - + " \"dial:propertyOrder\": 2" - + " }," - + " \"dial:file\" : true" - + " }" - + "}," - + "\"required\": [\"clientFile\",\"serverFile\"]" - + "}"; + private final String schema = """ + { + "$schema" : "https://dial.epam.com/application_type_schemas/schema#", + "$id" : "https://mydial.epam.com/custom_application_schemas/specific_application_type", + "dial:applicationTypeEditorUrl" : "https://mydial.epam.com/specific_application_type_editor", + "dial:applicationTypeDisplayName" : "Specific Application Type", + "dial:applicationTypeCompletionEndpoint" : "http://specific_application_service/opeani/v1/completion", + "properties" : { + "clientFile" : { + "type" : "string", + "format" : "dial-file-encoded", + "dial:meta" : { + "dial:propertyKind" : "client", + "dial:propertyOrder" : 1 + }, + "dial:file" : true + }, + "serverFile" : { + "type" : "string", + "format" : "dial-file-encoded", + "dial:meta" : { + "dial:propertyKind" : "server", + "dial:propertyOrder" : 2 + }, + "dial:file" : true + } + }, + "required" : [ "clientFile", "serverFile" ] + }"""; private final Map clientProperties = Map.of("clientFile", "files/public/valid-file-path/valid-sub-path/valid%20file%20name1.ext"); @@ -228,21 +230,22 @@ public void modifyEndpointForCustomApplication_throws_whenSchemaIsNull() { @Test public void modifyEndpointForCustomApplication_throws_whenEndpointNotFound() { - String schemaWithoutEndpoint = "{" - + "\"$schema\": \"https://dial.epam.com/application_type_schemas/schema#\"," - + "\"$id\": \"https://mydial.epam.com/custom_application_schemas/specific_application_type\"," - + "\"properties\": {" - + " \"clientFile\": {" - + " \"type\": \"string\"," - + " \"format\": \"dial-file-encoded\"," - + " \"dial:meta\": {" - + " \"dial:propertyKind\": \"client\"," - + " \"dial:propertyOrder\": 1" - + " }" - + " }" - + "}," - + "\"required\": [\"clientFile\"]" - + "}"; + String schemaWithoutEndpoint = """ + { + "$schema": "https://dial.epam.com/application_type_schemas/schema#", + "$id": "https://mydial.epam.com/custom_application_schemas/specific_application_type", + "properties": { + "clientFile": { + "type": "string", + "format": "dial-file-encoded", + "dial:meta": { + "dial:propertyKind": "client", + "dial:propertyOrder": 1 + } + } + }, + "required": ["clientFile"] + }"""; application.setApplicationTypeSchemaId(URI.create("schemaId")); when(config.getCustomApplicationSchema(any())).thenReturn(schemaWithoutEndpoint); @@ -357,7 +360,51 @@ public void getFiles_throwsException_whenResourceNotFound() { when(resourceService.hasResource(any())).thenReturn(false); - Assertions.assertThrows(ApplicationTypeSchemaValidationException.class, () -> + Assertions.assertThrows(ApplicationTypeResourceException.class, () -> ApplicationTypeSchemaUtils.getFiles(config, application, encryptionService, resourceService)); } + + @Test + public void getServerFiles_returnsListOfServerFiles_whenSchemaExists() { + application.setApplicationTypeSchemaId(URI.create("schemaId")); + application.setApplicationProperties(customProperties); + when(config.getCustomApplicationSchema(any())).thenReturn(schema); + + EncryptionService encryptionService = mock(EncryptionService.class); + ResourceService resourceService = mock(ResourceService.class); + + when(resourceService.hasResource(any())).thenReturn(true); + + List result = ApplicationTypeSchemaUtils.getServerFiles(config, application, encryptionService, resourceService); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals(result.get(0).getUrl(), serverProperties.get("serverFile")); + } + + @Test + public void getServerFiles_returnsEmptyList_whenSchemaIsNull() { + application.setApplicationTypeSchemaId(null); + + EncryptionService encryptionService = mock(EncryptionService.class); + ResourceService resourceService = mock(ResourceService.class); + + List result = ApplicationTypeSchemaUtils.getServerFiles(config, application, encryptionService, resourceService); + + Assertions.assertTrue(result.isEmpty()); + } + + @Test + public void getServerFiles_throwsException_whenResourceNotFound() { + application.setApplicationTypeSchemaId(URI.create("schemaId")); + application.setApplicationProperties(customProperties); + when(config.getCustomApplicationSchema(any())).thenReturn(schema); + + EncryptionService encryptionService = mock(EncryptionService.class); + ResourceService resourceService = mock(ResourceService.class); + + when(resourceService.hasResource(any())).thenReturn(false); + + Assertions.assertThrows(ApplicationTypeResourceException.class, () -> + ApplicationTypeSchemaUtils.getServerFiles(config, application, encryptionService, resourceService)); + } }