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: disable admin access for per-request key #659

Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,8 @@ private Map<ResourceDescriptor, Set<ResourceAccessType>> lookupPermissions(
private Map<ResourceDescriptor, Set<ResourceAccessType>> getAdminAccess(
Set<ResourceDescriptor> resources, ProxyContext context) {
if (hasAdminAccess(context)) {
boolean isNotApplication = context.getApiKeyData().getPerRequestKey() == null;
Set<ResourceAccessType> permissions = isNotApplication
? ResourceAccessType.ALL
: ResourceAccessType.READ_ONLY;
return resources.stream()
.collect(Collectors.toUnmodifiableMap(Function.identity(), resource -> permissions));
.collect(Collectors.toUnmodifiableMap(Function.identity(), resource -> ResourceAccessType.ALL));
}

return Map.of();
Expand Down Expand Up @@ -255,7 +251,8 @@ private Map<ResourceDescriptor, Set<ResourceAccessType>> getDeploymentAccess(
}

public boolean hasAdminAccess(ProxyContext context) {
return RuleMatcher.match(context, adminRules);
return context.getApiKeyData().getPerRequestKey() == null // not application
&& RuleMatcher.match(context, adminRules);
}

public void filterForbidden(ProxyContext context, ResourceDescriptor descriptor, MetadataBase metadata) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1616,6 +1616,73 @@ public void testIfMatch(Vertx vertx, VertxTestContext context) {
});
}

@Test
public void testAdminRightsNotInheritedByPerRequestKey(Vertx vertx, VertxTestContext context) {
ApiKeyData adminAppKey = createAdminAppKey();
apiKeyStore.assignPerRequestApiKey(adminAppKey);

Checkpoint checkpoint = context.checkpoint(4);
WebClient client = WebClient.create(vertx);

String fileUrl = "/v1/files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/file.txt";
Future.succeededFuture().compose((mapper) -> {
// Create a file using proxy key1
Promise<Void> promise = Promise.promise();
client.put(serverPort, "localhost", fileUrl)
.putHeader("Api-key", "proxyKey1")
.as(BodyCodec.string())
.sendMultipartForm(generateMultipartForm("file.txt", TEST_FILE_CONTENT, "text/plain"),
context.succeeding(response -> {
context.verify(() -> {
assertEquals(200, response.statusCode());
checkpoint.flag();
promise.complete();
});
})
);
return promise.future();
}).compose((mapper) -> {
// Verify that admin has read access to the file
Promise<Void> promise = Promise.promise();
client.get(serverPort, "localhost", fileUrl)
.putHeader("Authorization", "admin")
.as(BodyCodec.string())
.send(context.succeeding(response -> {
context.verify(() -> {
assertEquals(200, response.statusCode());
checkpoint.flag();
promise.complete();
});
}));
return promise.future();
}).compose((mapper) -> {
// Verify that a per-request key has access to the appdata inside admin's bucket
Promise<Void> promise = Promise.promise();
client.get(serverPort, "localhost", "/v1/metadata/files/4X25dj1mja51jykqxsXnCH/appdata/testapp/")
.putHeader("Api-key", adminAppKey.getPerRequestKey())
.as(BodyCodec.string())
.send(context.succeeding(response -> {
context.verify(() -> {
assertEquals(404, response.statusCode());
checkpoint.flag();
promise.complete();
});
}));
return promise.future();
}).andThen((mapper) -> {
// Ensure that a per-request key derived from admin key does not grant access to the file
client.get(serverPort, "localhost", fileUrl)
.putHeader("Api-key", adminAppKey.getPerRequestKey())
.as(BodyCodec.string())
.send(context.succeeding(response -> {
context.verify(() -> {
assertEquals(403, response.statusCode());
checkpoint.flag();
});
}));
});
}

private static MultipartForm generateMultipartForm(String fileName, String content) {
return generateMultipartForm(fileName, content, "text/plan");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.epam.aidial.core.server;

import com.epam.aidial.core.server.data.ApiKeyData;
import com.epam.aidial.core.server.util.ProxyUtil;
import com.fasterxml.jackson.databind.JsonNode;
import io.vertx.core.http.HttpMethod;
Expand Down Expand Up @@ -152,6 +153,9 @@ void testPublicationCreation() {

@Test
void testDeleteApprovedPublicationWorkflow() {
ApiKeyData adminAppKey = createAdminAppKey();
apiKeyStore.assignPerRequestApiKey(adminAppKey);

Response response = resourceRequest(HttpMethod.PUT, "/my/folder/conversation", CONVERSATION_BODY_1);
verify(response, 200);

Expand All @@ -171,6 +175,9 @@ void testDeleteApprovedPublicationWorkflow() {
""");
verify(response, 200);

response = operationRequest("/v1/ops/publication/approve", PUBLICATION_URL, "api-key", adminAppKey.getPerRequestKey());
verify(response, 403);

response = operationRequest("/v1/ops/publication/approve", PUBLICATION_URL, "authorization", "admin");
verifyJson(response, 200, """
{
Expand Down Expand Up @@ -767,6 +774,9 @@ void testPublicationApprove() {

@Test
void testPublicationReject() {
ApiKeyData adminAppKey = createAdminAppKey();
apiKeyStore.assignPerRequestApiKey(adminAppKey);

Response response = resourceRequest(HttpMethod.PUT, "/my/folder/conversation", CONVERSATION_BODY_1);
verify(response, 200);

Expand All @@ -780,6 +790,9 @@ void testPublicationReject() {
response = operationRequest("/v1/ops/publication/reject", PUBLICATION_URL, "authorization", "user");
verify(response, 403);

response = operationRequest("/v1/ops/publication/reject", PUBLICATION_URL, "api-key", adminAppKey.getPerRequestKey());
verify(response, 403);

response = operationRequest("/v1/ops/publication/reject", PUBLICATION_URL, "authorization", "admin");
verifyJson(response, 200, """
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.epam.aidial.core.server;

import com.epam.aidial.core.server.data.ApiKeyData;
import com.epam.aidial.core.server.security.AccessTokenValidator;
import com.epam.aidial.core.server.security.ApiKeyStore;
import com.epam.aidial.core.server.security.EncryptionService;
Expand Down Expand Up @@ -175,8 +176,7 @@ void init() throws Exception {
}

if (authorization.equals("user") || authorization.equals("admin")) {
return Future.succeededFuture(new ExtractedClaims(authorization, List.of(authorization),
authorization, Map.of("title", List.of("Manager")), null, null));
return Future.succeededFuture(createClaims(authorization));
}

return Future.failedFuture("Not authorized");
Expand All @@ -203,6 +203,17 @@ void init() throws Exception {
}
}

static ExtractedClaims createClaims(String role) {
return new ExtractedClaims(role, List.of(role), role, Map.of("title", List.of("Manager")), null, null);
}

static ApiKeyData createAdminAppKey() {
ApiKeyData perRequestKey = new ApiKeyData();
perRequestKey.setExtractedClaims(createClaims("admin"));
perRequestKey.setSourceDeployment("testapp");
return perRequestKey;
}

@AfterEach
void destroy() throws Exception {
try {
Expand Down
Loading