Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cache public rules #281

Merged
merged 2 commits into from
Mar 14, 2024
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 @@ -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)$");

Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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());
}
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/epam/aidial/core/data/Rules.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.epam.aidial.core.data;

import java.util.List;
import java.util.Map;

public record Rules(Map<String, List<Rule>> rules) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.epam.aidial.core.data.MetadataBase;
import com.epam.aidial.core.data.Publication;
import com.epam.aidial.core.data.ResourceFolderMetadata;
import com.epam.aidial.core.data.ResourceItemMetadata;
import com.epam.aidial.core.data.ResourceType;
import com.epam.aidial.core.data.ResourceUrl;
import com.epam.aidial.core.data.Rule;
Expand All @@ -17,6 +18,7 @@
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.lang3.tuple.Pair;

import java.util.Collection;
import java.util.HashMap;
Expand All @@ -25,6 +27,8 @@
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;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -53,6 +57,10 @@ public class PublicationService {

private static final Set<ResourceType> 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<Pair<Long, Map<String, List<Rule>>>> cachedRules = new AtomicReference<>();
astsiapanay marked this conversation as resolved.
Show resolved Hide resolved
private final EncryptionService encryption;
private final ResourceService resources;
private final BlobStorage files;
Expand All @@ -78,7 +86,7 @@ public boolean hasPublicAccess(ProxyContext context, ResourceDescription resourc
return false;
}

Map<String, List<Rule>> rules = decodeRules(resources.getResource(PUBLIC_RULES));
Map<String, List<Rule>> rules = getCachedRules();
Map<String, Boolean> cache = new HashMap<>();

return evaluate(context, resource, rules, cache);
Expand All @@ -89,7 +97,7 @@ public void filterForbidden(ProxyContext context, ResourceDescription folder, Re
return;
}

Map<String, List<Rule>> rules = decodeRules(resources.getResource(PUBLIC_RULES));
Map<String, List<Rule>> rules = getCachedRules();
Map<String, Boolean> cache = new HashMap<>();
cache.put(ruleUrl(folder), true);

Expand All @@ -101,6 +109,27 @@ public void filterForbidden(ProxyContext context, ResourceDescription folder, Re
metadata.setItems(filtered);
}

public Map<String, List<Rule>> listRules(ResourceDescription resource) {
if (!resource.isFolder() || !resource.isPublic()) {
throw new IllegalArgumentException("Bad rule url: " + resource.getUrl());
}

Map<String, List<Rule>> rules = getCachedRules();
Map<String, List<Rule>> result = new TreeMap<>();

while (resource != null) {
String url = ruleUrl(resource);;
List<Rule> list = rules.get(url);
resource = resource.getParent();

if (list != null) {
result.put(url, list);
}
}

return result;
}

public Collection<Publication> listPublications(ResourceDescription resource) {
if (resource.getType() != ResourceType.PUBLICATION || !resource.isRootFolder()) {
throw new IllegalArgumentException("Bad publication url: " + resource.getUrl());
Expand Down Expand Up @@ -473,6 +502,24 @@ private String encodeReviewBucket(ResourceDescription bucket, String id) {
return encryption.encrypt(path);
}

private Map<String, List<Rule>> getCachedRules() {
ResourceItemMetadata meta = resources.getResourceMetadata(PUBLIC_RULES);
long key = (meta == null) ? Long.MIN_VALUE : meta.getUpdatedAt();
Pair<Long, Map<String, List<Rule>>> current = cachedRules.get();

if (current == null || current.getKey() != key) {
Pair<ResourceItemMetadata, String> resource = resources.getResourceWithMetadata(PUBLIC_RULES);
Pair<Long, Map<String, List<Rule>>> next = (resource == null)
? Pair.of(Long.MIN_VALUE, decodeRules(null))
: Pair.of(resource.getKey().getUpdatedAt(), decodeRules(resource.getValue()));

cachedRules.compareAndSet(current, next);
astsiapanay marked this conversation as resolved.
Show resolved Hide resolved
current = next;
}

return current.getValue();
}

private static boolean evaluate(ProxyContext context,
ResourceDescription resource,
Map<String, List<Rule>> rules,
Expand Down Expand Up @@ -542,17 +589,7 @@ private static String encodePublications(Map<String, Publication> publications)

private static Map<String, List<Rule>> decodeRules(String json) {
Map<String, List<Rule>> 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<String, List<Rule>> rules) {
Expand Down
36 changes: 30 additions & 6 deletions src/main/java/com/epam/aidial/core/service/ResourceService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.blobstore.domain.BlobMetadata;
import org.jclouds.blobstore.domain.PageSet;
Expand Down Expand Up @@ -114,7 +115,7 @@ public void close() {
public MetadataBase getMetadata(ResourceDescription descriptor, String token, int limit, boolean recursive) {
return descriptor.isFolder()
? getFolderMetadata(descriptor, token, limit, recursive)
: getItemMetadata(descriptor);
: getResourceMetadata(descriptor);
}

private ResourceFolderMetadata getFolderMetadata(ResourceDescription descriptor, String token, int limit, boolean recursive) {
Expand Down Expand Up @@ -156,7 +157,11 @@ private ResourceFolderMetadata getFolderMetadata(ResourceDescription descriptor,
return new ResourceFolderMetadata(descriptor, resources).setNextToken(set.getNextMarker());
}

private ResourceItemMetadata getItemMetadata(ResourceDescription descriptor) {
public ResourceItemMetadata getResourceMetadata(ResourceDescription descriptor) {
if (descriptor.isFolder()) {
throw new IllegalArgumentException("Resource folder: " + descriptor.getUrl());
}

String redisKey = redisKey(descriptor);
String blobKey = blobKey(descriptor);
Result result = redisGet(redisKey, false);
Expand Down Expand Up @@ -187,12 +192,12 @@ public boolean hasResource(ResourceDescription descriptor) {
}

@Nullable
public String getResource(ResourceDescription descriptor) {
return getResource(descriptor, true);
public Pair<ResourceItemMetadata, String> getResourceWithMetadata(ResourceDescription descriptor) {
return getResourceWithMetadata(descriptor, true);
}

@Nullable
public String getResource(ResourceDescription descriptor, boolean lock) {
public Pair<ResourceItemMetadata, String> getResourceWithMetadata(ResourceDescription descriptor, boolean lock) {
String redisKey = redisKey(descriptor);
Result result = redisGet(redisKey, true);

Expand All @@ -208,7 +213,26 @@ public String getResource(ResourceDescription descriptor, boolean lock) {
}
}

return result.exists ? result.body : null;
if (result.exists) {
ResourceItemMetadata metadata = new ResourceItemMetadata(descriptor)
.setCreatedAt(result.createdAt)
.setUpdatedAt(result.updatedAt);

return Pair.of(metadata, result.body);
}

return null;
}

@Nullable
public String getResource(ResourceDescription descriptor) {
return getResource(descriptor, true);
}

@Nullable
public String getResource(ResourceDescription descriptor, boolean lock) {
Pair<ResourceItemMetadata, String> result = getResourceWithMetadata(descriptor, lock);
return (result == null) ? null : result.getRight();
}

public ResourceItemMetadata putResource(ResourceDescription descriptor, String body, boolean overwrite) {
Expand Down
85 changes: 85 additions & 0 deletions src/test/java/com/epam/aidial/core/PublicationApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading