fix(auth): add AI gRPC resource parser and enable auth for MCP/Agent requests (#13827)

Resolves #13824

This commit adds support for authentication of AI-related gRPC requests
(AbstractMcpRequest and AbstractAgentRequest) in the Nacos auth module.

Key changes:
- Implement AiGrpcResourceParser to extract namespace, group and resource name
  from AI protocol requests.
- Register AiGrpcResourceParser under SignType.AI in GrpcProtocolAuthService.
- Add comprehensive unit tests using parameterized testing to cover both
  MCP and Agent request types, including edge cases (null/empty fields).
- Fix missing security token refresh in AiGrpcClient by initializing
  SecurityProxy with scheduled login task.

Ensures that all incoming AI gRPC requests are properly authenticated
when security is enabled, closing a previous authorization gap.
This commit is contained in:
hongye 2025-09-18 10:14:34 +08:00 committed by GitHub
parent f1af11a50f
commit 006ffca559
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 232 additions and 12 deletions

View File

@ -16,33 +16,39 @@
package com.alibaba.nacos.auth.parser.grpc; package com.alibaba.nacos.auth.parser.grpc;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest; import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest; import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest; import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
import com.alibaba.nacos.api.ai.remote.request.ReleaseMcpServerRequest; import com.alibaba.nacos.api.ai.remote.request.ReleaseMcpServerRequest;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.remote.request.Request; import com.alibaba.nacos.api.remote.request.Request;
import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.common.utils.StringUtils;
/** /**
* Ai Grpc resource parser. * AI Grpc resource parser.
* *
* @author xiweng.yy * @author hongye.nhy xiweng.yy
*/ */
public class AiGrpcResourceParser extends AbstractGrpcResourceParser { public class AiGrpcResourceParser extends AbstractGrpcResourceParser {
@Override @Override
protected String getNamespaceId(Request request) { protected String getNamespaceId(Request request) {
if (request instanceof AbstractMcpRequest) { String namespaceId = null;
return ((AbstractMcpRequest) request).getNamespaceId(); if (request instanceof AbstractMcpRequest) {
namespaceId = ((AbstractMcpRequest) request).getNamespaceId();
} else if (request instanceof AbstractAgentRequest) { } else if (request instanceof AbstractAgentRequest) {
return ((AbstractAgentRequest) request).getNamespaceId(); namespaceId = ((AbstractAgentRequest) request).getNamespaceId();
} }
return StringUtils.EMPTY; if (StringUtils.isBlank(namespaceId)) {
namespaceId = AiConstants.Mcp.MCP_DEFAULT_NAMESPACE;
}
return namespaceId;
} }
@Override @Override
protected String getGroup(Request request) { protected String getGroup(Request request) {
return StringUtils.EMPTY; return Constants.DEFAULT_GROUP;
} }
@Override @Override

View File

@ -16,6 +16,9 @@
package com.alibaba.nacos.auth; package com.alibaba.nacos.auth;
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.config.remote.request.ConfigPublishRequest; import com.alibaba.nacos.api.config.remote.request.ConfigPublishRequest;
import com.alibaba.nacos.api.naming.remote.request.AbstractNamingRequest; import com.alibaba.nacos.api.naming.remote.request.AbstractNamingRequest;
import com.alibaba.nacos.auth.annotation.Secured; import com.alibaba.nacos.auth.annotation.Secured;
@ -35,9 +38,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -55,14 +56,20 @@ class GrpcProtocolAuthServiceTest {
private AbstractNamingRequest namingRequest; private AbstractNamingRequest namingRequest;
private AbstractMcpRequest mcpRequest;
private AbstractAgentRequest agentRequest;
private GrpcProtocolAuthService protocolAuthService; private GrpcProtocolAuthService protocolAuthService;
@BeforeEach @BeforeEach
void setUp() throws Exception { void setUp() throws Exception {
protocolAuthService = new GrpcProtocolAuthService(authConfig); protocolAuthService = new GrpcProtocolAuthService(authConfig);
protocolAuthService.initialize(); protocolAuthService.initialize();
mockConfigRequest(); mockConfigRequest();
mockNamingRequest(); mockNamingRequest();
mockMcpRequest();
mockAgentRequest();
} }
private void mockConfigRequest() { private void mockConfigRequest() {
@ -79,6 +86,20 @@ class GrpcProtocolAuthServiceTest {
namingRequest.setGroupName("testNG"); namingRequest.setGroupName("testNG");
namingRequest.setServiceName("testS"); namingRequest.setServiceName("testS");
} }
private void mockMcpRequest() {
mcpRequest = new AbstractMcpRequest() {
};
mcpRequest.setNamespaceId("testNNs");
mcpRequest.setMcpName("testS");
}
private void mockAgentRequest() {
agentRequest = new AbstractAgentRequest() {
};
agentRequest.setNamespaceId("testNNs");
agentRequest.setAgentName("testS");
}
@Test @Test
@Secured(resource = "testResource") @Secured(resource = "testResource")
@ -132,6 +153,30 @@ class GrpcProtocolAuthServiceTest {
assertEquals("testCG", actual.getGroup()); assertEquals("testCG", actual.getGroup());
assertNotNull(actual.getProperties()); assertNotNull(actual.getProperties());
} }
@Test
@Secured(signType = SignType.AI)
void testParseResourceWithMcpType() throws NoSuchMethodException {
Secured secured = getMethodSecure("testParseResourceWithMcpType");
Resource actual = protocolAuthService.parseResource(mcpRequest, secured);
assertEquals(SignType.AI, actual.getType());
assertEquals(mcpRequest.getMcpName(), actual.getName());
assertEquals(mcpRequest.getNamespaceId(), actual.getNamespaceId());
assertEquals(Constants.DEFAULT_GROUP, actual.getGroup());
assertNotNull(actual.getProperties());
}
@Test
@Secured(signType = SignType.AI)
void testParseResourceWithAgentType() throws NoSuchMethodException {
Secured secured = getMethodSecure("testParseResourceWithAgentType");
Resource actual = protocolAuthService.parseResource(agentRequest, secured);
assertEquals(SignType.AI, actual.getType());
assertEquals(agentRequest.getAgentName(), actual.getName());
assertEquals(agentRequest.getNamespaceId(), actual.getNamespaceId());
assertEquals(Constants.DEFAULT_GROUP, actual.getGroup());
assertNotNull(actual.getProperties());
}
@Test @Test
void testParseIdentity() { void testParseIdentity() {

View File

@ -0,0 +1,144 @@
/*
* Copyright 1999-2021 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.auth.parser.grpc;
import com.alibaba.nacos.api.ai.constant.AiConstants;
import com.alibaba.nacos.api.ai.model.a2a.AgentCard;
import com.alibaba.nacos.api.ai.remote.request.AbstractAgentRequest;
import com.alibaba.nacos.api.ai.remote.request.AbstractMcpRequest;
import com.alibaba.nacos.api.ai.remote.request.ReleaseAgentCardRequest;
import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.naming.remote.request.NotifySubscriberRequest;
import com.alibaba.nacos.api.remote.request.Request;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.plugin.auth.api.Resource;
import com.alibaba.nacos.plugin.auth.constant.SignType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.lang.reflect.Method;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class AiGrpcResourceParserTest {
private AiGrpcResourceParser resourceParser;
private static Stream<Arguments> fulContextRequests() {
Arguments case1 = Arguments.of(mockMcpRequest("testNs", "testName"), "testNs", "testName",
MockMcpRequest.class.getSimpleName());
Arguments case2 = Arguments.of(mockAgentRequest("testNs", "testName"), "testNs", "testName",
MockAgentRequest.class.getSimpleName());
Arguments case3 = Arguments.of(makeReleaseAgentCardRequest("testNs", "testName", "testCardName"), "testNs", "testCardName",
ReleaseAgentCardRequest.class.getSimpleName());
Arguments case4 = Arguments.of(mockOtherRequest("testNs", "testName"),
AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, "", NotifySubscriberRequest.class.getSimpleName());
return Stream.of(case1, case2, case3);
}
private static Stream<Arguments> withoutNamespaceRequests() {
Arguments case1 = Arguments.of(mockMcpRequest("", "testName"),
AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, "testName", MockMcpRequest.class.getSimpleName());
Arguments case2 = Arguments.of(mockAgentRequest(null, "testName"),
AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, "testName", MockAgentRequest.class.getSimpleName());
Arguments case3 = Arguments.of(mockOtherRequest(null, "testName"),
AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, "", NotifySubscriberRequest.class.getSimpleName());
return Stream.of(case1, case2, case3);
}
private static Stream<Arguments> withoutNameRequests() {
Arguments case1 = Arguments.of(mockMcpRequest("testNs", ""), "testNs", "",
MockMcpRequest.class.getSimpleName());
Arguments case2 = Arguments.of(mockAgentRequest("testNs", null), "testNs", "",
MockAgentRequest.class.getSimpleName());
Arguments case3 = Arguments.of(mockOtherRequest("testNs", ""),
AiConstants.Mcp.MCP_DEFAULT_NAMESPACE, "", NotifySubscriberRequest.class.getSimpleName());
return Stream.of(case1, case2, case3);
}
@BeforeEach
void setUp() throws Exception {
resourceParser = new AiGrpcResourceParser();
}
@ParameterizedTest
@MethodSource({"fulContextRequests", "withoutNamespaceRequests", "withoutNameRequests"})
@Secured(signType = SignType.AI)
void testParse(Request request, String expectedNamespaceId, String expectedName, String expectedRequestClassName) throws NoSuchMethodException {
Secured secured = getMethodSecure();
Resource actual = resourceParser.parse(request, secured);
assertEquals(expectedNamespaceId, actual.getNamespaceId());
assertEquals(Constants.DEFAULT_GROUP, actual.getGroup());
assertEquals(expectedName, actual.getName());
assertEquals(SignType.AI, actual.getType());
assertEquals(expectedRequestClassName, actual.getProperties()
.getProperty(com.alibaba.nacos.plugin.auth.constant.Constants.Resource.REQUEST_CLASS));
}
private static AbstractMcpRequest mockMcpRequest(String testNs, String testS) {
MockMcpRequest result = new MockMcpRequest();
result.setNamespaceId(testNs);
result.setMcpName(testS);
return result;
}
private static MockAgentRequest mockAgentRequest(String testNs, String testS) {
MockAgentRequest result = new MockAgentRequest();
result.setNamespaceId(testNs);
result.setAgentName(testS);
return result;
}
private static ReleaseAgentCardRequest makeReleaseAgentCardRequest(String testNs, String agentName, String cardName) {
ReleaseAgentCardRequest result = new ReleaseAgentCardRequest();
result.setNamespaceId(testNs);
result.setAgentName(agentName);
AgentCard agentCard = new AgentCard();
agentCard.setName(cardName);
result.setAgentCard(agentCard);
return result;
}
private static Request mockOtherRequest(String testNs, String testS) {
NotifySubscriberRequest result = new NotifySubscriberRequest();
result.setNamespace(testNs);
result.setGroupName("");
result.setServiceName(testS);
return result;
}
@Secured(signType = SignType.AI)
void forSecureAnnotationMethod() {
}
private Secured getMethodSecure() throws NoSuchMethodException {
Method method = AiGrpcResourceParserTest.class.getDeclaredMethod("forSecureAnnotationMethod");
return method.getAnnotation(Secured.class);
}
private static class MockMcpRequest extends AbstractMcpRequest {
}
private static class MockAgentRequest extends AbstractAgentRequest {
}
}

View File

@ -57,6 +57,7 @@ import com.alibaba.nacos.client.naming.core.NamingServerListManager;
import com.alibaba.nacos.client.naming.remote.http.NamingHttpClientManager; import com.alibaba.nacos.client.naming.remote.http.NamingHttpClientManager;
import com.alibaba.nacos.client.security.SecurityProxy; import com.alibaba.nacos.client.security.SecurityProxy;
import com.alibaba.nacos.client.utils.AppNameUtils; import com.alibaba.nacos.client.utils.AppNameUtils;
import com.alibaba.nacos.common.executor.NameThreadFactory;
import com.alibaba.nacos.common.lifecycle.Closeable; import com.alibaba.nacos.common.lifecycle.Closeable;
import com.alibaba.nacos.common.remote.ConnectionType; import com.alibaba.nacos.common.remote.ConnectionType;
import com.alibaba.nacos.common.remote.client.RpcClient; import com.alibaba.nacos.common.remote.client.RpcClient;
@ -64,13 +65,19 @@ import com.alibaba.nacos.common.remote.client.RpcClientConfigFactory;
import com.alibaba.nacos.common.remote.client.RpcClientFactory; import com.alibaba.nacos.common.remote.client.RpcClientFactory;
import com.alibaba.nacos.common.remote.client.grpc.GrpcClientConfig; import com.alibaba.nacos.common.remote.client.grpc.GrpcClientConfig;
import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.common.utils.ThreadUtils;
import com.alibaba.nacos.plugin.auth.api.RequestResource; import com.alibaba.nacos.plugin.auth.api.RequestResource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import static com.alibaba.nacos.client.constant.Constants.Security.SECURITY_INFO_REFRESH_INTERVAL_MILLS;
/** /**
* Nacos AI GRPC protocol client. * Nacos AI GRPC protocol client.
@ -92,13 +99,17 @@ public class AiGrpcClient implements Closeable {
private final AbstractServerListManager serverListManager; private final AbstractServerListManager serverListManager;
private final AiGrpcRedoService redoService; private final AiGrpcRedoService redoService;
private final NacosClientProperties properties;
private SecurityProxy securityProxy; private SecurityProxy securityProxy;
private NacosMcpServerCacheHolder mcpServerCacheHolder; private NacosMcpServerCacheHolder mcpServerCacheHolder;
private NacosAgentCardCacheHolder agentCardCacheHolder; private NacosAgentCardCacheHolder agentCardCacheHolder;
private ScheduledThreadPoolExecutor executorService;
public AiGrpcClient(String namespaceId, NacosClientProperties properties) { public AiGrpcClient(String namespaceId, NacosClientProperties properties) {
this.namespaceId = namespaceId; this.namespaceId = namespaceId;
this.uuid = UUID.randomUUID().toString(); this.uuid = UUID.randomUUID().toString();
@ -106,6 +117,7 @@ public class AiGrpcClient implements Closeable {
this.rpcClient = buildRpcClient(properties); this.rpcClient = buildRpcClient(properties);
this.serverListManager = new NamingServerListManager(properties, namespaceId); this.serverListManager = new NamingServerListManager(properties, namespaceId);
this.redoService = new AiGrpcRedoService(properties, this); this.redoService = new AiGrpcRedoService(properties, this);
this.properties = properties;
} }
private RpcClient buildRpcClient(NacosClientProperties properties) { private RpcClient buildRpcClient(NacosClientProperties properties) {
@ -133,6 +145,16 @@ public class AiGrpcClient implements Closeable {
this.rpcClient.start(); this.rpcClient.start();
this.securityProxy = new SecurityProxy(this.serverListManager, this.securityProxy = new SecurityProxy(this.serverListManager,
NamingHttpClientManager.getInstance().getNacosRestTemplate()); NamingHttpClientManager.getInstance().getNacosRestTemplate());
initSecurityProxy(properties);
}
private void initSecurityProxy(NacosClientProperties properties) {
this.executorService = new ScheduledThreadPoolExecutor(1,
new NameThreadFactory("com.alibaba.nacos.client.ai.security"));
final Properties nacosClientPropertiesView = properties.asProperties();
this.securityProxy.login(nacosClientPropertiesView);
this.executorService.scheduleWithFixedDelay(() -> securityProxy.login(nacosClientPropertiesView), 0,
SECURITY_INFO_REFRESH_INTERVAL_MILLS, TimeUnit.MILLISECONDS);
} }
/** /**
@ -542,5 +564,8 @@ public class AiGrpcClient implements Closeable {
if (null != securityProxy) { if (null != securityProxy) {
securityProxy.shutdown(); securityProxy.shutdown();
} }
if (null != executorService) {
ThreadUtils.shutdownThreadPool(executorService, LOGGER);
}
} }
} }