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: draft share API implementation #190

Merged
merged 16 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 5 additions & 1 deletion src/main/java/com/epam/aidial/core/AiDial.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import com.epam.aidial.core.security.AccessTokenValidator;
import com.epam.aidial.core.security.ApiKeyStore;
import com.epam.aidial.core.security.EncryptionService;
import com.epam.aidial.core.service.InvitationService;
import com.epam.aidial.core.service.LockService;
import com.epam.aidial.core.service.ResourceService;
import com.epam.aidial.core.service.ShareService;
import com.epam.aidial.core.storage.BlobStorage;
import com.epam.aidial.core.token.TokenStatsTracker;
import com.epam.aidial.core.upstream.UpstreamBalancer;
Expand Down Expand Up @@ -98,11 +100,13 @@ void start() throws Exception {
resourceService = new ResourceService(vertx, redis, storage, lockService, settings("resources"));
}

InvitationService invitationService = new InvitationService(redis);
ShareService shareService = new ShareService(resourceService, invitationService, encryptionService);
RateLimiter rateLimiter = new RateLimiter(vertx, resourceService);

proxy = new Proxy(vertx, client, configStore, logStore,
rateLimiter, upstreamBalancer, accessTokenValidator,
storage, encryptionService, apiKeyStore, tokenStatsTracker, resourceService);
storage, encryptionService, apiKeyStore, tokenStatsTracker, resourceService, invitationService, shareService);

server = vertx.createHttpServer(new HttpServerOptions(settings("server"))).requestHandler(proxy);
open(server, HttpServer::listen);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/epam/aidial/core/Proxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import com.epam.aidial.core.security.ApiKeyStore;
import com.epam.aidial.core.security.EncryptionService;
import com.epam.aidial.core.security.ExtractedClaims;
import com.epam.aidial.core.service.InvitationService;
import com.epam.aidial.core.service.ResourceService;
import com.epam.aidial.core.service.ShareService;
import com.epam.aidial.core.storage.BlobStorage;
import com.epam.aidial.core.token.TokenStatsTracker;
import com.epam.aidial.core.upstream.UpstreamBalancer;
Expand Down Expand Up @@ -69,6 +71,8 @@ public class Proxy implements Handler<HttpServerRequest> {
private final ApiKeyStore apiKeyStore;
private final TokenStatsTracker tokenStatsTracker;
private final ResourceService resourceService;
private final InvitationService invitationService;
private final ShareService shareService;

@Override
public void handle(HttpServerRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,16 @@ public abstract class AccessControlBaseController {

/**
* @param bucket url encoded bucket name
* @param path url encoded resource path
* @param path url encoded resource path
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
*/
public Future<?> handle(String resourceType, String bucket, String path) {
ResourceType type = ResourceType.of(resourceType);
String urlDecodedBucket = UrlUtil.decodePath(bucket);
String decryptedBucket = proxy.getEncryptionService().decrypt(urlDecodedBucket);
boolean hasReadAccess = isSharedWithMe(type, bucket, path);
boolean hasWriteAccess = hasWriteAccess(path, decryptedBucket);
boolean hasAccess = checkFullAccess ? hasWriteAccess : hasReadAccess || hasWriteAccess;

if (!hasAccess) {
return context.respond(HttpStatus.FORBIDDEN, "You don't have an access to the bucket " + bucket);
}
// we should take a real user bucket not provided from resource
String actualUserLocation = BlobStorageUtil.buildInitiatorBucket(context);
String actualUserBucket = proxy.getEncryptionService().encrypt(actualUserLocation);

ResourceDescription resource;
try {
Expand All @@ -43,14 +40,23 @@ public Future<?> handle(String resourceType, String bucket, String path) {
return context.respond(HttpStatus.BAD_REQUEST, errorMessage);
}

boolean hasReadAccess = isSharedWithMe(resource, type, bucket, path, actualUserBucket, actualUserLocation);
boolean hasWriteAccess = hasWriteAccess(path, decryptedBucket);
boolean hasAccess = checkFullAccess ? hasWriteAccess : hasReadAccess || hasWriteAccess;

if (!hasAccess) {
return context.respond(HttpStatus.FORBIDDEN, "You don't have an access to the bucket " + bucket);
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
}

return handle(resource);
}

protected abstract Future<?> handle(ResourceDescription resource);

protected boolean isSharedWithMe(ResourceType type, String bucket, String filePath) {
String url = type.getGroup() + BlobStorageUtil.PATH_SEPARATOR + bucket + BlobStorageUtil.PATH_SEPARATOR + filePath;
return context.getApiKeyData().getAttachedFiles().contains(url);
protected boolean isSharedWithMe(ResourceDescription resource, ResourceType type, String providedBucket, String filePath, String userBucket, String userLocation) {
String url = type.getGroup() + BlobStorageUtil.PATH_SEPARATOR + providedBucket + BlobStorageUtil.PATH_SEPARATOR + filePath;
return context.getApiKeyData().getAttachedFiles().contains(url)
|| (proxy.getResourceService() != null && proxy.getShareService().hasReadAccess(userBucket, userLocation, resource));
}

protected boolean hasWriteAccess(String filePath, String decryptedBucket) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public class ControllerSelector {
private static final Pattern PATTERN_TOKENIZE = Pattern.compile("/+v1/deployments/([-.@a-zA-Z0-9]+)/tokenize");
private static final Pattern PATTERN_TRUNCATE_PROMPT = Pattern.compile("/+v1/deployments/([-.@a-zA-Z0-9]+)/truncate_prompt");

private static final Pattern SHARE_RESOURCE_OPERATIONS = Pattern.compile("/v1/ops/resource/share/(create|list|discard|revoke)");
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
private static final Pattern INVITATIONS = Pattern.compile("/v1/invitations");
private static final Pattern INVITATION = Pattern.compile("/v1/invitations/([a-zA-Z0-9]+)");

public Controller select(Proxy proxy, ProxyContext context) {
String path = context.getRequest().path();
String decodedPath = URLDecoder.decode(path, StandardCharsets.UTF_8);
Expand Down Expand Up @@ -174,6 +178,19 @@ private static Controller selectGet(Proxy proxy, ProxyContext context, String pa
return controller::getBucket;
}

match = match(INVITATION, path);
if (match != null) {
String invitationId = match.group(1);
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
InvitationController controller = new InvitationController(proxy, context);
return () -> controller.getOrAcceptInvitation(invitationId);
}

match = match(INVITATIONS, path);
if (match != null) {
InvitationController controller = new InvitationController(proxy, context);
return controller::getInvitations;
}

return null;
}

Expand Down Expand Up @@ -231,6 +248,15 @@ private static Controller selectPost(Proxy proxy, ProxyContext context, String p
return () -> controller.handle(deploymentId, getter, true);
}

match = match(SHARE_RESOURCE_OPERATIONS, path);
if (match != null) {
String operation = match.group(1);
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
ShareController.Operation op = ShareController.Operation.valueOf(operation.toUpperCase());

ShareController controller = new ShareController(proxy, context);
return () -> controller.handle(op);
}

return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.epam.aidial.core.controller;

import com.epam.aidial.core.Proxy;
import com.epam.aidial.core.ProxyContext;
import com.epam.aidial.core.data.Invitation;
import com.epam.aidial.core.service.InvitationService;
import com.epam.aidial.core.service.ShareService;
import com.epam.aidial.core.storage.BlobStorageUtil;
import com.epam.aidial.core.util.HttpStatus;
import io.vertx.core.Future;
import lombok.AllArgsConstructor;

import java.util.List;

@AllArgsConstructor
public class InvitationController {

final Proxy proxy;
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
final ProxyContext context;

public Future<?> getInvitations() {
return proxy.getVertx().executeBlocking(() -> {
InvitationService invitationService = proxy.getInvitationService();
String userId = context.getUserSub() == null ? context.getProject() : context.getUserSub();
List<Invitation> invitations = invitationService.getMyInvitations(userId);

return context.respond(HttpStatus.OK, invitations);
});
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
}

public Future<?> getOrAcceptInvitation(String invitationId) {
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
String accept = context.getRequest().getParam("accept");
if (accept != null) {
return proxy.getVertx().executeBlocking(() -> {
try {
ShareService shareService = proxy.getShareService();
String bucketLocation = BlobStorageUtil.buildInitiatorBucket(context);
String bucket = proxy.getEncryptionService().encrypt(bucketLocation);
shareService.acceptShare(bucket, bucketLocation, invitationId);
return context.respond(HttpStatus.OK);
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
} catch (Exception e) {
return context.respond(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
});
} else {
return proxy.getVertx().executeBlocking(() -> {
InvitationService invitationService = proxy.getInvitationService();
Invitation invitation = invitationService.getInvitation(invitationId);
if (invitation == null) {
return context.respond(HttpStatus.NOT_FOUND);
}

return context.respond(HttpStatus.OK, invitation);
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
}
151 changes: 151 additions & 0 deletions src/main/java/com/epam/aidial/core/controller/ShareController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.epam.aidial.core.controller;

import com.epam.aidial.core.Proxy;
import com.epam.aidial.core.ProxyContext;
import com.epam.aidial.core.data.ListSharedResourcesRequest;
import com.epam.aidial.core.data.ResourceLinkCollection;
import com.epam.aidial.core.data.ShareResourcesRequest;
import com.epam.aidial.core.storage.BlobStorageUtil;
import com.epam.aidial.core.util.HttpException;
import com.epam.aidial.core.util.HttpStatus;
import com.epam.aidial.core.util.ProxyUtil;
import io.vertx.core.Future;
import lombok.AllArgsConstructor;

import java.nio.charset.StandardCharsets;

@AllArgsConstructor
public class ShareController {

private static final String LIST_SHARED_BY_ME_RESOURCES = "others";

final Proxy proxy;
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
final ProxyContext context;

public Future<?> handle(Operation operation) {
return switch (operation) {
case LIST -> listSharedResources();
case CREATE -> createSharedResources();
case REVOKE -> revokeSharedResources();
case DISCARD -> discardSharedResources();
};
}

public Future<?> listSharedResources() {
return context.getRequest().body().compose(buffer -> {
ListSharedResourcesRequest request;
try {
String body = buffer.toString(StandardCharsets.UTF_8);
request = ProxyUtil.convertToObject(body, ListSharedResourcesRequest.class);
} catch (Exception e) {
throw new HttpException(HttpStatus.BAD_REQUEST, e.getMessage());
}

String bucketLocation = BlobStorageUtil.buildInitiatorBucket(context);
String bucket = proxy.getEncryptionService().encrypt(bucketLocation);
String with = request.getWith();

return proxy.getVertx().executeBlocking(() -> {
if (LIST_SHARED_BY_ME_RESOURCES.equals(with)) {
return proxy.getShareService().listSharedByMe(bucket, bucketLocation, request);
} else {
return proxy.getShareService().listSharedWithMe(bucket, bucketLocation, request);
}
});
})
.onSuccess(response -> context.respond(HttpStatus.OK, response))
.onFailure(error -> {
if (error instanceof HttpException httpException) {
context.respond(httpException.getStatus(), httpException.getMessage());
} else {
context.respond(HttpStatus.INTERNAL_SERVER_ERROR, error.getMessage());
}
});
}

public Future<?> createSharedResources() {
return context.getRequest().body().compose(buffer -> {
ShareResourcesRequest request;
try {
String body = buffer.toString(StandardCharsets.UTF_8);
request = ProxyUtil.convertToObject(body, ShareResourcesRequest.class);
} catch (Exception e) {
throw new HttpException(HttpStatus.BAD_REQUEST, e.getMessage());
}

String bucketLocation = BlobStorageUtil.buildInitiatorBucket(context);
String bucket = proxy.getEncryptionService().encrypt(bucketLocation);
return proxy.getVertx()
.executeBlocking(() -> proxy.getShareService().initializeShare(bucket, bucketLocation, request))
.onSuccess(response -> context.respond(HttpStatus.OK, response))
.onFailure(error -> {
if (error instanceof HttpException httpException) {
context.respond(httpException.getStatus(), httpException.getMessage());
} else {
context.respond(HttpStatus.INTERNAL_SERVER_ERROR, error.getMessage());
}
});

});
}

public Future<?> discardSharedResources() {
return context.getRequest().body().compose(buffer -> {
ResourceLinkCollection request;
try {
String body = buffer.toString(StandardCharsets.UTF_8);
request = ProxyUtil.convertToObject(body, ResourceLinkCollection.class);
} catch (Exception e) {
throw new HttpException(HttpStatus.BAD_REQUEST, e.getMessage());
}

String bucketLocation = BlobStorageUtil.buildInitiatorBucket(context);
String bucket = proxy.getEncryptionService().encrypt(bucketLocation);
return proxy.getVertx()
.executeBlocking(() -> {
proxy.getShareService().discardSharedAccess(bucket, bucketLocation, request);
return null;
})
.onSuccess(response -> context.respond(HttpStatus.OK))
.onFailure(error -> {
if (error instanceof HttpException httpException) {
context.respond(httpException.getStatus(), httpException.getMessage());
} else {
context.respond(HttpStatus.INTERNAL_SERVER_ERROR, error.getMessage());
}
});
});
}

public Future<?> revokeSharedResources() {
return context.getRequest().body().compose(buffer -> {
ResourceLinkCollection request;
try {
String body = buffer.toString(StandardCharsets.UTF_8);
request = ProxyUtil.convertToObject(body, ResourceLinkCollection.class);
} catch (Exception e) {
throw new HttpException(HttpStatus.BAD_REQUEST, e.getMessage());
}

String bucketLocation = BlobStorageUtil.buildInitiatorBucket(context);
String bucket = proxy.getEncryptionService().encrypt(bucketLocation);
return proxy.getVertx()
.executeBlocking(() -> {
proxy.getShareService().revokeSharedAccess(bucket, bucketLocation, request);
return null;
})
.onSuccess(response -> context.respond(HttpStatus.OK))
.onFailure(error -> {
if (error instanceof HttpException httpException) {
context.respond(httpException.getStatus(), httpException.getMessage());
} else {
context.respond(HttpStatus.INTERNAL_SERVER_ERROR, error.getMessage());
}
});
});
}

public enum Operation {
CREATE, LIST, DISCARD, REVOKE
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/epam/aidial/core/data/Invitation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.epam.aidial.core.data;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Set;

@Data
@AllArgsConstructor
public class Invitation {
String id;
String owner;
Set<ResourceLink> resources;
long createdAt;
long expireAt;
}
4 changes: 4 additions & 0 deletions src/main/java/com/epam/aidial/core/data/InvitationLink.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.epam.aidial.core.data;

public record InvitationLink(String url) {
}
5 changes: 5 additions & 0 deletions src/main/java/com/epam/aidial/core/data/InvitationType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.epam.aidial.core.data;

public enum InvitationType {
LINK, EMAIL
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.epam.aidial.core.data;

import lombok.Data;

import java.util.Set;

@Data
public class ListSharedResourcesRequest {
artsiomkorzun marked this conversation as resolved.
Show resolved Hide resolved
Set<ResourceType> resourceTypes;
String order;
String with;
}
Loading
Loading