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

fix: route Files API by encoded path, url parameter in metadata response is URL encoded #96

Merged
merged 6 commits into from
Dec 19, 2023
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 @@ -6,29 +6,38 @@
import com.epam.aidial.core.storage.ResourceDescription;
import com.epam.aidial.core.storage.ResourceType;
import com.epam.aidial.core.util.HttpStatus;
import com.epam.aidial.core.util.UrlUtil;
import io.vertx.core.Future;
import lombok.AllArgsConstructor;

@AllArgsConstructor
public abstract class AccessControlBaseController {

private static final String DEFAULT_RESOURCE_ERROR_MESSAGE = "Invalid file url provided %s";

final Proxy proxy;
final ProxyContext context;


/**
* @param bucket url encoded bucket name
* @param filePath url encoded file path
*/
public Future<?> handle(String bucket, String filePath) {
String urlDecodedBucket = UrlUtil.decodePath(bucket);
String expectedUserBucket = BlobStorageUtil.buildUserBucket(context);
String decryptedBucket = proxy.getEncryptionService().decrypt(bucket);
String decryptedBucket = proxy.getEncryptionService().decrypt(urlDecodedBucket);

if (!expectedUserBucket.equals(decryptedBucket)) {
return context.respond(HttpStatus.FORBIDDEN, "You don't have an access to the bucket " + bucket);
}

ResourceDescription resource;
try {
resource = ResourceDescription.from(ResourceType.FILE, bucket, decryptedBucket, filePath);
resource = ResourceDescription.fromEncoded(ResourceType.FILE, urlDecodedBucket, decryptedBucket, filePath);
} catch (Exception ex) {
return context.respond(HttpStatus.BAD_REQUEST, "Invalid file url provided");
String errorMessage = ex.getMessage() != null ? ex.getMessage() : DEFAULT_RESOURCE_ERROR_MESSAGE.formatted(filePath);
return context.respond(HttpStatus.BAD_REQUEST, errorMessage);
}

return handle(resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ public class ControllerSelector {
private static final Pattern PATTERN_TRUNCATE_PROMPT = Pattern.compile("/+v1/deployments/([-.@a-zA-Z0-9]+)/truncate_prompt");

public Controller select(Proxy proxy, ProxyContext context) {
String path = URLDecoder.decode(context.getRequest().path(), StandardCharsets.UTF_8);
String path = context.getRequest().path();
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
HttpMethod method = context.getRequest().method();
Controller controller = null;

if (method == HttpMethod.GET) {
controller = selectGet(proxy, context, path);
controller = selectGet(proxy, context, path, decodedPath);
} else if (method == HttpMethod.POST) {
controller = selectPost(proxy, context, path);
controller = selectPost(proxy, context, decodedPath);
} else if (method == HttpMethod.DELETE) {
controller = selectDelete(proxy, context, path);
} else if (method == HttpMethod.PUT) {
Expand All @@ -61,69 +62,69 @@ public Controller select(Proxy proxy, ProxyContext context) {
return (controller == null) ? new RouteController(proxy, context) : controller;
}

private static Controller selectGet(Proxy proxy, ProxyContext context, String path) {
private static Controller selectGet(Proxy proxy, ProxyContext context, String path, String decodedPath) {
Matcher match;

match = match(PATTERN_DEPLOYMENT, path);
match = match(PATTERN_DEPLOYMENT, decodedPath);
if (match != null) {
DeploymentController controller = new DeploymentController(context);
String deploymentId = match.group(1);
return () -> controller.getDeployment(deploymentId);
}

match = match(PATTERN_DEPLOYMENTS, path);
match = match(PATTERN_DEPLOYMENTS, decodedPath);
if (match != null) {
DeploymentController controller = new DeploymentController(context);
return controller::getDeployments;
}

match = match(PATTERN_MODEL, path);
match = match(PATTERN_MODEL, decodedPath);
if (match != null) {
ModelController controller = new ModelController(context);
String modelId = match.group(1);
return () -> controller.getModel(modelId);
}

match = match(PATTERN_MODELS, path);
match = match(PATTERN_MODELS, decodedPath);
if (match != null) {
ModelController controller = new ModelController(context);
return controller::getModels;
}

match = match(PATTERN_ADDON, path);
match = match(PATTERN_ADDON, decodedPath);
if (match != null) {
AddonController controller = new AddonController(context);
String addonId = match.group(1);
return () -> controller.getAddon(addonId);
}

match = match(PATTERN_ADDONS, path);
match = match(PATTERN_ADDONS, decodedPath);
if (match != null) {
AddonController controller = new AddonController(context);
return controller::getAddons;
}

match = match(PATTERN_ASSISTANT, path);
match = match(PATTERN_ASSISTANT, decodedPath);
if (match != null) {
AssistantController controller = new AssistantController(context);
String assistantId = match.group(1);
return () -> controller.getAssistant(assistantId);
}

match = match(PATTERN_ASSISTANTS, path);
match = match(PATTERN_ASSISTANTS, decodedPath);
if (match != null) {
AssistantController controller = new AssistantController(context);
return controller::getAssistants;
}

match = match(PATTERN_APPLICATION, path);
match = match(PATTERN_APPLICATION, decodedPath);
if (match != null) {
ApplicationController controller = new ApplicationController(context);
String application = match.group(1);
return () -> controller.getApplication(application);
}

match = match(PATTERN_APPLICATIONS, path);
match = match(PATTERN_APPLICATIONS, decodedPath);
if (match != null) {
ApplicationController controller = new ApplicationController(context);
return controller::getApplications;
Expand All @@ -145,7 +146,7 @@ private static Controller selectGet(Proxy proxy, ProxyContext context, String pa
return () -> controller.handle(bucket, filePath);
}

match = match(PATTERN_BUCKET, path);
match = match(PATTERN_BUCKET, decodedPath);
if (match != null) {
BucketController controller = new BucketController(proxy, context);
return controller::getBucket;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protected Future<?> handle(ResourceDescription resource) {
storage.delete(absoluteFilePath);
return null;
} catch (Exception ex) {
log.error("Failed to delete file " + absoluteFilePath, ex);
log.error("Failed to delete file %s/%s".formatted(resource.getBucketName(), resource.getOriginalPath()), ex);
throw new RuntimeException(ex);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected Future<?> handle(ResourceDescription resource) {
result.fail(e);
}
}).onFailure(error -> context.respond(HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to fetch file with path %s/%s".formatted(resource.getBucketName(), resource.getRelativePath())));
"Failed to fetch file with path %s/%s".formatted(resource.getBucketName(), resource.getOriginalPath())));

return result.future();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ protected Future<?> handle(ResourceDescription resource) {
} catch (Exception ex) {
log.error("Failed to list files", ex);
context.respond(HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to list files by path %s/%s".formatted(resource.getBucketName(), resource.getRelativePath()));
"Failed to list files by path %s/%s".formatted(resource.getBucketName(), resource.getOriginalPath()));
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protected Future<?> handle(ResourceDescription resource) {
.onFailure(error -> {
writeStream.abortUpload(error);
context.respond(HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to upload file by path %s/%s".formatted(resource.getBucketName(), resource.getRelativePath()));
"Failed to upload file by path %s/%s".formatted(resource.getBucketName(), resource.getOriginalPath()));
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ private static FileMetadataBase buildFileMetadata(ResourceDescription resource,

private static ResourceDescription getResourceDescription(ResourceType resourceType, String bucketName, String bucketLocation, String absoluteFilePath) {
String relativeFilePath = absoluteFilePath.substring(bucketLocation.length() + resourceType.getFolder().length() + 1);
return ResourceDescription.from(resourceType, bucketName, bucketLocation, relativeFilePath);
return ResourceDescription.fromDecoded(resourceType, bucketName, bucketLocation, relativeFilePath);
}

private static BlobMetadata buildBlobMetadata(String absoluteFilePath, String contentType, String bucketName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ public String buildUserBucket(ProxyContext context) {
throw new IllegalArgumentException("Can't find user bucket. Either user sub or api-key project must be provided");
}

public String buildAbsoluteFilePath(ResourceType resource, String bucket, String path) {
return bucket + resource.getFolder() + PATH_SEPARATOR + path;
}

public boolean isFolder(String path) {
return path.endsWith(PATH_SEPARATOR);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,121 @@
package com.epam.aidial.core.storage;

import com.epam.aidial.core.util.UrlUtil;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ResourceDescription {
ResourceType type;
String name;
List<String> parentFolders;
String relativePath;
String originalPath;
String bucketName;
String bucketLocation;
boolean isFolder;

public String getUrl() {
StringBuilder builder = new StringBuilder();
builder.append(bucketName)
builder.append(UrlUtil.encodePath(bucketName))
.append(BlobStorageUtil.PATH_SEPARATOR);
if (parentFolders != null) {
builder.append(String.join(BlobStorageUtil.PATH_SEPARATOR, parentFolders))

if (!parentFolders.isEmpty()) {
String parentPath = parentFolders.stream()
.map(UrlUtil::encodePath)
.collect(Collectors.joining(BlobStorageUtil.PATH_SEPARATOR));
builder.append(parentPath)
.append(BlobStorageUtil.PATH_SEPARATOR);
}
if (name != null && !isHomeFolder(name)) {
builder.append(name);

if (name != null) {
builder.append(UrlUtil.encodePath(name));

if (isFolder) {
builder.append(BlobStorageUtil.PATH_SEPARATOR);
}
}

return URLEncoder.encode(builder.toString(), StandardCharsets.UTF_8);
return builder.toString();
}

public String getAbsoluteFilePath() {
StringBuilder builder = new StringBuilder();
if (parentFolders != null) {
builder.append(bucketLocation)
.append(type.getFolder())
.append(BlobStorageUtil.PATH_SEPARATOR);

if (!parentFolders.isEmpty()) {
builder.append(getParentPath())
.append(BlobStorageUtil.PATH_SEPARATOR);
}
if (name != null && !isHomeFolder(name)) {

if (name != null) {
builder.append(name);

if (isFolder) {
builder.append(BlobStorageUtil.PATH_SEPARATOR);
}
}

return BlobStorageUtil.buildAbsoluteFilePath(type, bucketLocation, builder.toString());
return builder.toString();
}

public String getParentPath() {
return parentFolders == null ? null : String.join(BlobStorageUtil.PATH_SEPARATOR, parentFolders);
return parentFolders.isEmpty() ? null : String.join(BlobStorageUtil.PATH_SEPARATOR, parentFolders);
}

public static ResourceDescription from(ResourceType type, String bucketName, String bucketLocation, String relativeFilePath) {
/**
* @param type resource type
* @param bucketName bucket name (encrypted)
* @param bucketLocation bucket location on blob storage; bucket location must end with /
* @param path url encoded relative path; if url path is null or empty we treat it as user home
*/
public static ResourceDescription fromEncoded(ResourceType type, String bucketName, String bucketLocation, String path) {
// in case empty path - treat it as a home folder
relativeFilePath = StringUtils.isBlank(relativeFilePath) ? BlobStorageUtil.PATH_SEPARATOR : relativeFilePath;
String urlEncodedRelativePath = StringUtils.isBlank(path) ? BlobStorageUtil.PATH_SEPARATOR : path;
verify(bucketLocation.endsWith(BlobStorageUtil.PATH_SEPARATOR), "Bucket location must end with /");

String[] elements = relativeFilePath.split(BlobStorageUtil.PATH_SEPARATOR);
List<String> parentFolders = null;
String name = "/";
if (elements.length > 0) {
name = elements[elements.length - 1];
}
if (elements.length > 1) {
String parentPath = relativeFilePath.substring(0, relativeFilePath.length() - name.length() - 1);
if (!parentPath.isEmpty() && !parentPath.equals(BlobStorageUtil.PATH_SEPARATOR)) {
parentFolders = List.of(parentPath.split(BlobStorageUtil.PATH_SEPARATOR));
}
}
String[] encodedElements = urlEncodedRelativePath.split(BlobStorageUtil.PATH_SEPARATOR);
List<String> elements = Arrays.stream(encodedElements).map(UrlUtil::decodePath).toList();
elements.forEach(element ->
verify(isValidFilename(element), "Invalid path provided " + urlEncodedRelativePath)
);

return from(type, bucketName, bucketLocation, urlEncodedRelativePath, elements, BlobStorageUtil.isFolder(urlEncodedRelativePath));
}

/**
* @param type resource type
* @param bucketName bucket name (encrypted)
* @param bucketLocation bucket location on blob storage; bucket location must end with /
* @param path url decoded relative path; if url path is null or empty we treat it as user home
*/
public static ResourceDescription fromDecoded(ResourceType type, String bucketName, String bucketLocation, String path) {
// in case empty path - treat it as a home folder
path = StringUtils.isBlank(path) ? BlobStorageUtil.PATH_SEPARATOR : path;
verify(bucketLocation.endsWith(BlobStorageUtil.PATH_SEPARATOR), "Bucket location must end with /");

List<String> elements = Arrays.asList(path.split(BlobStorageUtil.PATH_SEPARATOR));
return from(type, bucketName, bucketLocation, path, elements, BlobStorageUtil.isFolder(path));
}

return new ResourceDescription(type, name, parentFolders, relativeFilePath, bucketName, bucketLocation, BlobStorageUtil.isFolder(relativeFilePath));
private static ResourceDescription from(ResourceType type, String bucketName, String bucketLocation,
String originalPath, List<String> paths, boolean isFolder) {
boolean isEmptyElements = paths.isEmpty();
String name = isEmptyElements ? null : paths.get(paths.size() - 1);
List<String> parentFolders = isEmptyElements ? List.of() : paths.subList(0, paths.size() - 1);
return new ResourceDescription(type, name, parentFolders, originalPath, bucketName, bucketLocation, isFolder);
}

private static boolean isHomeFolder(String path) {
return path.equals(BlobStorageUtil.PATH_SEPARATOR);
private static boolean isValidFilename(String value) {
return !value.contains(BlobStorageUtil.PATH_SEPARATOR);
}

private static void verify(boolean condition, String message) {
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/com/epam/aidial/core/util/UrlUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.epam.aidial.core.util;

import lombok.experimental.UtilityClass;

import java.net.URI;
import java.net.URISyntaxException;

@UtilityClass
public class UrlUtil {

public String encodePath(String path) {
try {
URI uri = new URI(null, null, path, null);
return uri.toASCIIString();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

public String decodePath(String path) {
try {
URI uri = new URI(path);
if (uri.getRawFragment() != null || uri.getRawQuery() != null) {
throw new IllegalArgumentException("Wrong path provided " + path);
}
return uri.getPath();
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
Loading