diff --git a/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java b/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java index 74cba54c6..c04700034 100644 --- a/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java +++ b/src/main/java/com/epam/aidial/core/controller/ControllerSelector.java @@ -49,6 +49,7 @@ public class ControllerSelector { private static final Pattern INVITATIONS = Pattern.compile("^/v1/invitations$"); private static final Pattern INVITATION = Pattern.compile("^/v1/invitations/([a-zA-Z0-9]+)$"); private static final Pattern PUBLICATIONS = Pattern.compile("^/v1/ops/publications/(list|get|create|delete|approve|reject)$"); + private static final Pattern PUBLICATION_RULES = Pattern.compile("^/v1/ops/publications/rules/list$"); private static final Pattern RESOURCE_OPERATIONS = Pattern.compile("^/v1/ops/resources/(move)$"); @@ -261,11 +262,17 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p case "create" -> controller::createPublication; case "delete" -> controller::deletePublication; case "approve" -> controller::approvePublication; - case "reject" -> controller:: rejectPublication; + case "reject" -> controller::rejectPublication; default -> null; }; } + match = match(PUBLICATION_RULES, path); + if (match != null) { + PublicationController controller = new PublicationController(proxy, context); + return controller::listRules; + } + match = match(RESOURCE_OPERATIONS, path); if (match != null) { ResourceOperationController controller = new ResourceOperationController(proxy, context); diff --git a/src/main/java/com/epam/aidial/core/controller/PublicationController.java b/src/main/java/com/epam/aidial/core/controller/PublicationController.java index 7907e19b2..73a7c5550 100644 --- a/src/main/java/com/epam/aidial/core/controller/PublicationController.java +++ b/src/main/java/com/epam/aidial/core/controller/PublicationController.java @@ -6,6 +6,7 @@ import com.epam.aidial.core.data.Publications; import com.epam.aidial.core.data.ResourceLink; import com.epam.aidial.core.data.ResourceType; +import com.epam.aidial.core.data.Rules; import com.epam.aidial.core.security.AccessService; import com.epam.aidial.core.security.EncryptionService; import com.epam.aidial.core.service.LockService; @@ -133,6 +134,21 @@ public Future rejectPublication() { return Future.succeededFuture(); } + public Future listRules() { + context.getRequest() + .body() + .compose(body -> { + String url = ProxyUtil.convertToObject(body, ResourceLink.class).url(); + ResourceDescription rule = decodeRule(url); + checkRuleAccess(rule); + return vertx.executeBlocking(() -> publicationService.listRules(rule)); + }) + .onSuccess(rules -> context.respond(HttpStatus.OK, new Rules(rules))) + .onFailure(error -> respondError("Can't list rules", error)); + + return Future.succeededFuture(); + } + private void respondError(String message, Throwable error) { HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; String body = null; @@ -172,6 +188,25 @@ private ResourceDescription decodePublication(String path, boolean allowPublic) return resource; } + private ResourceDescription decodeRule(String path) { + try { + if (!path.startsWith(BlobStorageUtil.PUBLIC_LOCATION)) { + throw new IllegalArgumentException(); + } + + String folder = path.substring(BlobStorageUtil.PUBLIC_LOCATION.length()); + ResourceDescription resource = ResourceDescription.fromEncoded(ResourceType.RULES, BlobStorageUtil.PUBLIC_BUCKET, BlobStorageUtil.PUBLIC_LOCATION, folder); + + if (!resource.isFolder()) { + throw new IllegalArgumentException(); + } + + return resource; + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid resource: " + path, e); + } + } + private void checkAccess(ResourceDescription resource, boolean allowUser) { boolean hasAccess = accessService.hasAdminAccess(context); @@ -184,4 +219,10 @@ private void checkAccess(ResourceDescription resource, boolean allowUser) { throw new HttpException(HttpStatus.FORBIDDEN, "Forbidden resource: " + resource.getUrl()); } } + + private void checkRuleAccess(ResourceDescription rule) { + if (!accessService.hasReadAccess(rule, context)) { + throw new HttpException(HttpStatus.FORBIDDEN, "Forbidden resource: " + rule.getUrl()); + } + } } \ No newline at end of file diff --git a/src/main/java/com/epam/aidial/core/data/Rules.java b/src/main/java/com/epam/aidial/core/data/Rules.java new file mode 100644 index 000000000..2ff6a9698 --- /dev/null +++ b/src/main/java/com/epam/aidial/core/data/Rules.java @@ -0,0 +1,7 @@ +package com.epam.aidial.core.data; + +import java.util.List; +import java.util.Map; + +public record Rules(Map> rules) { +} \ No newline at end of file diff --git a/src/main/java/com/epam/aidial/core/service/PublicationService.java b/src/main/java/com/epam/aidial/core/service/PublicationService.java index f1b78ee54..2c7a8e628 100644 --- a/src/main/java/com/epam/aidial/core/service/PublicationService.java +++ b/src/main/java/com/epam/aidial/core/service/PublicationService.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; import java.util.function.LongSupplier; import java.util.function.Supplier; @@ -56,6 +57,9 @@ public class PublicationService { private static final Set ALLOWED_RESOURCES = Set.of(ResourceType.FILE, ResourceType.CONVERSATION, ResourceType.PROMPT); + /** + * Key is updated time from the metadata. Value is decoded map (folder path, list of rules). + */ private final AtomicReference>>> cachedRules = new AtomicReference<>(); private final EncryptionService encryption; private final ResourceService resources; @@ -105,6 +109,27 @@ public void filterForbidden(ProxyContext context, ResourceDescription folder, Re metadata.setItems(filtered); } + public Map> listRules(ResourceDescription resource) { + if (!resource.isFolder() || !resource.isPublic()) { + throw new IllegalArgumentException("Bad rule url: " + resource.getUrl()); + } + + Map> rules = getCachedRules(); + Map> result = new TreeMap<>(); + + while (resource != null) { + String url = ruleUrl(resource);; + List list = rules.get(url); + resource = resource.getParent(); + + if (list != null) { + result.put(url, list); + } + } + + return result; + } + public Collection listPublications(ResourceDescription resource) { if (resource.getType() != ResourceType.PUBLICATION || !resource.isRootFolder()) { throw new IllegalArgumentException("Bad publication url: " + resource.getUrl()); @@ -564,17 +589,7 @@ private static String encodePublications(Map publications) private static Map> decodeRules(String json) { Map> rules = ProxyUtil.convertToObject(json, RULES_TYPE); - - if (rules == null) { - Rule rule = new Rule(); - rule.setSource("roles"); - rule.setFunction(Rule.Function.TRUE); - rule.setTargets(List.of()); - rules = new LinkedHashMap<>(); - rules.put(PUBLIC_LOCATION, List.of(rule)); - } - - return rules; + return (rules == null) ? new LinkedHashMap<>() : rules; } private static String encodeRules(Map> rules) { diff --git a/src/test/java/com/epam/aidial/core/PublicationApiTest.java b/src/test/java/com/epam/aidial/core/PublicationApiTest.java index b6692c545..0ecc26e8c 100644 --- a/src/test/java/com/epam/aidial/core/PublicationApiTest.java +++ b/src/test/java/com/epam/aidial/core/PublicationApiTest.java @@ -518,4 +518,89 @@ void testPublicationList() { } """); } + + @Test + void listRules() { + Response response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": ""} + """); + verify(response, 400); + + response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": "public"} + """); + verify(response, 400); + + response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": "public/"} + """); + verifyJson(response, 200, """ + { + "rules" : { } + } + """); + + response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": "public/"} + """, "authorization", "user"); + verifyJson(response, 200, """ + { + "rules" : { } + } + """); + + response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": "public/"} + """, "authorization", "admin"); + verifyJson(response, 200, """ + { + "rules" : { } + } + """); + + response = resourceRequest(HttpMethod.PUT, "/my/folder/conversation", CONVERSATION_BODY_1); + verify(response, 200); + + response = operationRequest("/v1/ops/publications/create", PUBLICATION_REQUEST.formatted(bucket, bucket)); + verify(response, 200); + + response = operationRequest("/v1/ops/publications/approve", PUBLICATION_URL, "authorization", "admin"); + verify(response, 200); + + + response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": "public/folder/"} + """, "authorization", "user"); + verifyJson(response, 200, """ + { + "rules" : { + "public/folder/" : [ { + "function" : "EQUAL", + "source" : "roles", + "targets" : [ "user" ] + } ] + } + } + """); + + response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": "public/folder/"} + """, "authorization", "admin"); + verifyJson(response, 200, """ + { + "rules" : { + "public/folder/" : [ { + "function" : "EQUAL", + "source" : "roles", + "targets" : [ "user" ] + } ] + } + } + """); + + response = operationRequest("/v1/ops/publications/rules/list", """ + {"url": "public/folder/"} + """); + verify(response, 403); + } } \ No newline at end of file