Skip to content

Commit

Permalink
feat: support custom applications (#350)(#305)
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxim-Gadalov authored Jun 27, 2024
1 parent 2bec8d1 commit 86c088c
Show file tree
Hide file tree
Showing 32 changed files with 1,273 additions and 128 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Priority order:
| redis.provider.serverless | - | Yes | The flag indicates if the cache is serverless. **Note**. It's applied to `aws-elasti-cache`
| invitations.ttlInSeconds | 259200 | No |Invitation time to live in seconds.
| access.admin.rules | - | No |Matches claims from identity providers with the rules to figure out whether a user is allowed to perform admin actions, like deleting any resource or approving a publication. Example: [{"source": "roles", "function": "EQUAL", "targets": ["admin"]}]. If roles contain "admin, the actions are allowed.
| applications.includeCustomApps | false | No |The flag indicates whether custom applications should be included into openai listing

### Storage requirements

Expand Down
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 @@ -12,6 +12,7 @@
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.CustomApplicationService;
import com.epam.aidial.core.service.InvitationService;
import com.epam.aidial.core.service.LockService;
import com.epam.aidial.core.service.NotificationService;
Expand Down Expand Up @@ -124,11 +125,14 @@ void start() throws Exception {
ApiKeyStore apiKeyStore = new ApiKeyStore(resourceService, vertx);
ConfigStore configStore = new FileConfigStore(vertx, settings("config"), apiKeyStore);

CustomApplicationService customApplicationService = new CustomApplicationService(encryptionService,
resourceService, shareService, accessService, settings("applications"));

proxy = new Proxy(vertx, client, configStore, logStore,
rateLimiter, upstreamBalancer, accessTokenValidator,
storage, encryptionService, apiKeyStore, tokenStatsTracker, resourceService, invitationService,
shareService, publicationService, accessService, lockService, resourceOperationService, ruleService,
notificationService, version());
notificationService, customApplicationService, version());

server = vertx.createHttpServer(new HttpServerOptions(settings("server"))).requestHandler(proxy);
open(server, HttpServer::listen);
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/epam/aidial/core/Proxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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.CustomApplicationService;
import com.epam.aidial.core.service.InvitationService;
import com.epam.aidial.core.service.LockService;
import com.epam.aidial.core.service.NotificationService;
Expand Down Expand Up @@ -87,6 +88,7 @@ public class Proxy implements Handler<HttpServerRequest> {
private final ResourceOperationService resourceOperationService;
private final RuleService ruleService;
private final NotificationService notificationService;
private final CustomApplicationService customApplicationService;
private final String version;

@Override
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/epam/aidial/core/config/Application.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.epam.aidial.core.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Application extends Deployment {
}
2 changes: 2 additions & 0 deletions src/main/java/com/epam/aidial/core/config/Features.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.epam.aidial.core.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Features {
private String rateEndpoint;
private String tokenizeEndpoint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.epam.aidial.core.util.HttpStatus;
import io.vertx.core.Future;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;

@AllArgsConstructor
public abstract class AccessControlBaseController {
Expand All @@ -29,18 +30,26 @@ public Future<?> handle(String resourceUrl) {
return proxy.getVertx()
.executeBlocking(() -> {
AccessService service = proxy.getAccessService();
return isWriteAccess ? service.hasWriteAccess(resource, context) : service.hasReadAccess(resource, context);
boolean hasWriteAccess = service.hasWriteAccess(resource, context);
if (hasWriteAccess) {
// pair of writeAccess, readAccess
return Pair.of(true, true);
} else {
// pair of writeAccess, readAccess
return Pair.of(false, service.hasReadAccess(resource, context));
}
}, false)
.map(hasAccess -> {
.map(pair -> {
boolean hasAccess = isWriteAccess ? pair.getLeft() : pair.getRight();
if (hasAccess) {
handle(resource);
handle(resource, pair.getLeft());
} else {
context.respond(HttpStatus.FORBIDDEN, "You don't have an access to: " + resourceUrl);
}
return null;
});
}

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

}
Original file line number Diff line number Diff line change
@@ -1,36 +1,61 @@
package com.epam.aidial.core.controller;

import com.epam.aidial.core.Proxy;
import com.epam.aidial.core.ProxyContext;
import com.epam.aidial.core.config.Application;
import com.epam.aidial.core.config.Config;
import com.epam.aidial.core.data.ApplicationData;
import com.epam.aidial.core.data.ListData;
import com.epam.aidial.core.service.CustomApplicationService;
import com.epam.aidial.core.service.PermissionDeniedException;
import com.epam.aidial.core.service.ResourceNotFoundException;
import com.epam.aidial.core.util.HttpStatus;
import io.vertx.core.Future;
import lombok.RequiredArgsConstructor;
import io.vertx.core.Vertx;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

@RequiredArgsConstructor
@Slf4j
public class ApplicationController {

private final ProxyContext context;
private final Vertx vertx;
private final CustomApplicationService customApplicationService;
private final boolean includeCustomApplications;

public ApplicationController(ProxyContext context, Proxy proxy) {
this.context = context;
this.vertx = proxy.getVertx();
this.customApplicationService = proxy.getCustomApplicationService();
this.includeCustomApplications = customApplicationService.includeCustomApplications();
}

public Future<?> getApplication(String applicationId) {
Config config = context.getConfig();
Application application = config.getApplications().get(applicationId);

if (application == null) {
return context.respond(HttpStatus.NOT_FOUND);
Future<Application> applicationFuture;
if (application != null) {
if (!DeploymentController.hasAccess(context, application)) {
return context.respond(HttpStatus.FORBIDDEN);
}
applicationFuture = Future.succeededFuture(application);
} else {
applicationFuture = vertx.executeBlocking(() -> customApplicationService.getCustomApplication(applicationId, context), false);
}

if (!DeploymentController.hasAccess(context, application)) {
return context.respond(HttpStatus.FORBIDDEN);
}
applicationFuture.map(app -> {
if (app == null) {
throw new ResourceNotFoundException(applicationId);
}

ApplicationData data = ApplicationUtil.mapApplication(app);
context.respond(HttpStatus.OK, data);
return null;
}).onFailure(error -> handleRequestError(applicationId, error));

ApplicationData data = createApplication(application);
context.respond(HttpStatus.OK, data);
return Future.succeededFuture();
}

Expand All @@ -40,30 +65,55 @@ public Future<?> getApplications() {

for (Application application : config.getApplications().values()) {
if (DeploymentController.hasAccess(context, application)) {
ApplicationData data = createApplication(application);
ApplicationData data = ApplicationUtil.mapApplication(application);
applications.add(data);
}
}

ListData<ApplicationData> list = new ListData<>();
list.setData(applications);

context.respond(HttpStatus.OK, list);
if (includeCustomApplications) {
vertx.executeBlocking(() -> {
List<Application> ownCustomApplications = customApplicationService.getOwnCustomApplications(context);
for (Application application : ownCustomApplications) {
ApplicationData data = ApplicationUtil.mapApplication(application);
applications.add(data);
}
List<Application> sharedApplications = customApplicationService.getSharedApplications(context);
for (Application application : sharedApplications) {
ApplicationData data = ApplicationUtil.mapApplication(application);
applications.add(data);
}
List<Application> publicApplications = customApplicationService.getPublicApplications(context);
for (Application application : publicApplications) {
ApplicationData data = ApplicationUtil.mapApplication(application);
applications.add(data);
}
return null;
}, false)
.onSuccess(ignore -> context.respond(HttpStatus.OK, list)
.onFailure(error -> {
log.error("Can't fetch custom applications", error);
context.respond(HttpStatus.INTERNAL_SERVER_ERROR, error.getMessage());
}));
} else {
context.respond(HttpStatus.OK, list);
}

return Future.succeededFuture();
}

private static ApplicationData createApplication(Application application) {
ApplicationData data = new ApplicationData();
data.setId(application.getName());
data.setApplication(application.getName());
data.setDisplayName(application.getDisplayName());
data.setDisplayVersion(application.getDisplayVersion());
data.setIconUrl(application.getIconUrl());
data.setDescription(application.getDescription());
data.setFeatures(DeploymentController.createFeatures(application.getFeatures()));
data.setInputAttachmentTypes(application.getInputAttachmentTypes());
data.setMaxInputAttachments(application.getMaxInputAttachments());
data.setDefaults(application.getDefaults());
return data;
private void handleRequestError(String applicationId, Throwable error) {
if (error instanceof PermissionDeniedException) {
log.error("Forbidden application {}. Key: {}. User sub: {}", applicationId, context.getProject(), context.getUserSub());
context.respond(HttpStatus.FORBIDDEN, error.getMessage());
} else if (error instanceof ResourceNotFoundException) {
log.error("Application not found {}", applicationId, error);
context.respond(HttpStatus.NOT_FOUND, error.getMessage());
} else {
log.error("Failed to load application {}", applicationId, error);
context.respond(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to load application: " + applicationId);
}
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/epam/aidial/core/controller/ApplicationUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.epam.aidial.core.controller;

import com.epam.aidial.core.config.Application;
import com.epam.aidial.core.data.ApplicationData;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;

@UtilityClass
@Slf4j
public class ApplicationUtil {

public ApplicationData mapApplication(Application application) {
ApplicationData data = new ApplicationData();
data.setId(application.getName());
data.setApplication(application.getName());
data.setDisplayName(application.getDisplayName());
data.setDisplayVersion(application.getDisplayVersion());
data.setIconUrl(application.getIconUrl());
data.setDescription(application.getDescription());
data.setFeatures(DeploymentController.createFeatures(application.getFeatures()));
data.setInputAttachmentTypes(application.getInputAttachmentTypes());
data.setMaxInputAttachments(application.getMaxInputAttachments());
data.setDefaults(application.getDefaults());

return data;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
@UtilityClass
public class ControllerSelector {

private static final Pattern PATTERN_POST_DEPLOYMENT = Pattern.compile("^/+openai/deployments/([^/]+)/(completions|chat/completions|embeddings)$");
private static final Pattern PATTERN_POST_DEPLOYMENT = Pattern.compile("^/+openai/deployments/(.+?)/(completions|chat/completions|embeddings)$");
private static final Pattern PATTERN_DEPLOYMENT = Pattern.compile("^/+openai/deployments/([^/]+)$");
private static final Pattern PATTERN_DEPLOYMENTS = Pattern.compile("^/+openai/deployments$");

Expand All @@ -29,7 +29,7 @@ public class ControllerSelector {
private static final Pattern PATTERN_ASSISTANT = Pattern.compile("^/+openai/assistants/([^/]+)$");
private static final Pattern PATTERN_ASSISTANTS = Pattern.compile("^/+openai/assistants$");

private static final Pattern PATTERN_APPLICATION = Pattern.compile("^/+openai/applications/([^/]+)$");
private static final Pattern PATTERN_APPLICATION = Pattern.compile("^/+openai/applications/(.+?)$");
private static final Pattern PATTERN_APPLICATIONS = Pattern.compile("^/+openai/applications$");


Expand All @@ -38,8 +38,8 @@ public class ControllerSelector {
private static final Pattern PATTERN_FILES = Pattern.compile("^/v1/files/[a-zA-Z0-9]+/.*");
private static final Pattern PATTERN_FILES_METADATA = Pattern.compile("^/v1/metadata/files/[a-zA-Z0-9]+/.*");

private static final Pattern PATTERN_RESOURCE = Pattern.compile("^/v1/(conversations|prompts)/[a-zA-Z0-9]+/.*");
private static final Pattern PATTERN_RESOURCE_METADATA = Pattern.compile("^/v1/metadata/(conversations|prompts)/[a-zA-Z0-9]+/.*");
private static final Pattern PATTERN_RESOURCE = Pattern.compile("^/v1/(conversations|prompts|applications)/[a-zA-Z0-9]+/.*");
private static final Pattern PATTERN_RESOURCE_METADATA = Pattern.compile("^/v1/metadata/(conversations|prompts|applications)/[a-zA-Z0-9]+/.*");

private static final Pattern PATTERN_RATE_RESPONSE = Pattern.compile("^/+v1/([^/]+)/rate$");
private static final Pattern PATTERN_TOKENIZE = Pattern.compile("^/+v1/deployments/([^/]+)/tokenize$");
Expand Down Expand Up @@ -135,14 +135,14 @@ private static Controller selectGet(Proxy proxy, ProxyContext context, String pa

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

match = match(PATTERN_APPLICATIONS, path);
if (match != null) {
ApplicationController controller = new ApplicationController(context);
ApplicationController controller = new ApplicationController(context, proxy);
return controller::getApplications;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public DeleteFileController(Proxy proxy, ProxyContext context) {
}

@Override
protected Future<?> handle(ResourceDescription resource) {
protected Future<?> handle(ResourceDescription resource, boolean hasWriteAccess) {
if (resource.isFolder()) {
return context.respond(HttpStatus.BAD_REQUEST, "Can't delete a folder");
}
Expand Down
Loading

0 comments on commit 86c088c

Please sign in to comment.