Skip to content
Open
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 @@ -12,6 +12,7 @@
import com.epam.aidial.core.server.Proxy;
import com.epam.aidial.core.server.ProxyContext;
import com.epam.aidial.core.server.function.BaseRequestFunction;
import com.epam.aidial.core.server.function.ResolveResourceDependenciesFn;
import com.epam.aidial.core.server.function.enhancement.InjectApplicationPropsToMcpRequest;
import com.epam.aidial.core.server.service.ApplicationSchemaService;
import com.epam.aidial.core.server.util.McpUpstreamAuthInjector;
Expand All @@ -37,7 +38,9 @@ public class ApplicationMcpProxyController extends McpProxyController {
public ApplicationMcpProxyController(Proxy proxy, ProxyContext context, String toolSetId) {
super(proxy, context, toolSetId);
this.applicationSchemaService = proxy.getApplicationSchemaService();
this.enhancementFunctions = List.of(new InjectApplicationPropsToMcpRequest(proxy, context));
this.enhancementFunctions = List.of(
new InjectApplicationPropsToMcpRequest(proxy, context),
new ResolveResourceDependenciesFn<>(proxy, context));
this.authInjector = new McpUpstreamAuthInjector(proxy);
}

Expand Down Expand Up @@ -91,6 +94,11 @@ protected String getUpstreamEndpoint(Deployment deployment) {
return application.getMcp().getEndpoint();
}

@Override
protected boolean preAssignsPerRequestKey() {
return application.getMcp().isForwardPerRequestKey();
}

@Override
protected void injectProxyRequestHeaders(HttpClientRequest proxyRequest, MultiMap excludeHeaders) {
excludeHeaders.add(HEADER_APPLICATION_PROPERTIES, "whatever");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private List<BaseRequestFunction<RequestObject>> buildEnhancementFunctions() {
new CollectRequestApplicationFilesFn(proxy, context),
new BuildUpstreamCacheFn(proxy, context, InterfaceType.OPENAI_CHAT_COMPLETIONS),
new CollectDeploymentsFn(proxy, context),
new ResolveResourceDependenciesFn(proxy, context));
new ResolveResourceDependenciesFn<>(proxy, context));
}

@ApiOperation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ private void sendRequest(String absoluteUri) {

private void handleRequestBody(Buffer requestBody) {
context.setRequestBody(requestBody);
boolean preAssign = preAssignsPerRequestKey();
if (preAssign) {
// The enhancement chain writes into this key (ResolveResourceDependenciesFn bakes
// dependency grants into it), so it must exist before the chain runs and be persisted
// only after — grants written after assignPerRequestApiKey would never reach Redis.
createProxyApiKeyData();
}
String contentType = context.getRequest().getHeader(HttpHeaders.CONTENT_TYPE);
if (Strings.CI.contains(contentType, HEADER_CONTENT_TYPE_APPLICATION_JSON)) {

Expand All @@ -190,6 +197,9 @@ private void handleRequestBody(Buffer requestBody) {
return;
}
}
if (preAssign) {
apiKeyStore.assignPerRequestApiKey(context.getProxyApiKeyData());
}
sendRequest();
}

Expand Down Expand Up @@ -228,12 +238,20 @@ private void handleProxyRequest(HttpClientRequest proxyRequest) {
protected void injectProxyRequestHeaders(HttpClientRequest proxyRequest, MultiMap excludeHeaders) {
}

protected String assignPerRequestKey() {
/**
* Whether this controller mints the upstream's per-request key <em>before</em> the request
* enhancement chain runs, so chain functions can write into it (D-25). Default {@code false}:
* the base MCP path leaves the mint to {@link com.epam.aidial.core.server.util.McpUpstreamAuthInjector}
* at header-injection time, which is where it has always happened.
*/
protected boolean preAssignsPerRequestKey() {
return false;
}

private void createProxyApiKeyData() {
ApiKeyData proxyApiKeyData = new ApiKeyData();
context.setProxyApiKeyData(proxyApiKeyData);
ApiKeyData.initFromContext(proxyApiKeyData, context);
apiKeyStore.assignPerRequestApiKey(proxyApiKeyData);
return proxyApiKeyData.getPerRequestKey();
}

/**
Expand Down Expand Up @@ -446,7 +464,10 @@ private Future<?> handleRateLimitSuccess(Deployment deployment) {
context.setTraceOperation("Send request to %s deployment".formatted(deployment.getName()));
context.getRequest().body()
.onFailure(this::handleRequestBodyError)
.onSuccess(this::handleRequestBody);
.onSuccess(body -> taskExecutor.submit(() -> {
handleRequestBody(body);
return null;
}).onFailure(this::handleError));
return null;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public ResponsesController(Proxy proxy, ProxyContext context) {
new CollectRequestApplicationFilesFn(proxy, context),
new BuildUpstreamCacheFn(proxy, context, InterfaceType.OPENAI_RESPONSES),
new CollectDeploymentsFn(proxy, context),
new ResolveResourceDependenciesFn(proxy, context));
new ResolveResourceDependenciesFn<>(proxy, context));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ protected List<BaseRequestFunction<RequestObject>> buildEnhancementFunctions() {
new EnhanceDeploymentRequestFn(proxy, context),
new CollectRequestApplicationFilesFn(proxy, context),
new CollectDeploymentsFn(proxy, context),
new ResolveResourceDependenciesFn(proxy, context));
new ResolveResourceDependenciesFn<>(proxy, context));
}

public Future<?> handle() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.epam.aidial.core.server.data.AuthBucket;
import com.epam.aidial.core.server.data.consent.Consent;
import com.epam.aidial.core.server.data.permission.PerRequestSharedData;
import com.epam.aidial.core.server.function.request.RequestObject;
import com.epam.aidial.core.server.log.ResourceDependencyAuditLog;
import com.epam.aidial.core.server.security.AccessService;
import com.epam.aidial.core.server.service.ResourceDependencyValidator;
Expand Down Expand Up @@ -49,14 +48,20 @@
* {@code AccessService.lookupOriginatingUserPermissions} — own bucket as the human, shared,
* public — and never through rules that read the calling key's own grants (D-24).
*/
public class ResolveResourceDependenciesFn extends BaseRequestFunction<RequestObject> {
public class ResolveResourceDependenciesFn<T> extends BaseRequestFunction<T> {

public ResolveResourceDependenciesFn(Proxy proxy, ProxyContext context) {
super(proxy, context);
}

/**
* The request body is not an input here — resolution reads only {@code context}. The type
* parameter exists solely so this function can join chains of either element type
* ({@code RequestObject} on the conversation mint sites, {@code ObjectNode} on the MCP
* proxy); see D-25a.
*/
@Override
public Boolean apply(RequestObject request) {
public Boolean apply(T ignored) {
if (!(context.getDeployment() instanceof Application application)) {
// Interceptor hop or a non-application deployment — dependencies resolve for the
// application being called, nothing else.
Expand All @@ -72,9 +77,15 @@ public Boolean apply(RequestObject request) {

private void resolve(Application application, List<ResourceDependency> declaration) {
String applicationId = application.getName();
// Nowhere to bake a grant into — e.g. an MCP application with forwardPerRequestKey
// disabled never gets a per-request key at all, so it can never receive one regardless of
// consent or reach. Treated exactly like "not consented": every declared dependency is
// unresolved, so a required one still hard-fails the call instead of silently succeeding
// for an app that was promised access it can never actually receive.
boolean hasKeyToBakeInto = context.getProxyApiKeyData() != null;
// The consent record is content-bound to the whole declaration: any change since the grant
// re-requires it, and until then nothing resolves. Checked before any resolution work.
boolean consented = proxy.getConsentService().isAdminConsented(applicationId, declaration);
boolean consented = hasKeyToBakeInto && proxy.getConsentService().isAdminConsented(applicationId, declaration);
AuthBucket userBucket = BucketBuilder.buildBucket(context);

List<Resolved> resolvedTargets = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void inject(BiConsumer<String, String> headers, ToolSet toolSet,
headers.accept(authHeader.getHeaderName(), authHeader.getHeaderValue());
}
if (toolSet.isForwardPerRequestKey()) {
headers.accept(Proxy.HEADER_API_KEY, assignPerRequestKey(context));
headers.accept(Proxy.HEADER_API_KEY, perRequestKey(context));
}
}

Expand All @@ -61,7 +61,7 @@ public void inject(BiConsumer<String, String> headers, Application app, ProxyCon
headers.accept(HEADER_APPLICATION_ID, app.getName());
Application.Mcp mcp = app.getMcp();
if (mcp.isForwardPerRequestKey()) {
headers.accept(Proxy.HEADER_API_KEY, assignPerRequestKey(context));
headers.accept(Proxy.HEADER_API_KEY, perRequestKey(context));
}
if (mcp.getConfigDelivery() == Application.McpConfigDelivery.HEADER) {
applicationSchemaService.consumeMetadataProperties(app, (properties, appendHeader) -> {
Expand All @@ -73,7 +73,19 @@ public void inject(BiConsumer<String, String> headers, Application app, ProxyCon
}
}

private String assignPerRequestKey(ProxyContext context) {
/**
* Returns the per-request key the upstream should receive, minting one only if this request
* does not already have one. Re-entry is normal on this path: handleProxyRequest runs again
* on every 429 retry, connection/send failure, and same-origin 307/308 redirect. Minting
* afresh each time would orphan the previous Redis-backed key — only the current
* proxyApiKeyData is invalidated on completion — and would hand the upstream a key that does
* not carry the grants baked into the one built before the enhancement chain.
*/
private String perRequestKey(ProxyContext context) {
ApiKeyData assigned = context.getProxyApiKeyData();
if (assigned != null && assigned.getPerRequestKey() != null) {
return assigned.getPerRequestKey();
}
ApiKeyData keyData = new ApiKeyData();
context.setProxyApiKeyData(keyData);
ApiKeyData.initFromContext(keyData, context);
Expand Down
Loading
Loading