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
@@ -0,0 +1,54 @@
/*
* Copyright 1999-2026 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.ai.service.visibility;

import com.alibaba.nacos.ai.model.AiResource;
import com.alibaba.nacos.ai.service.repository.AiResourcePersistService;
import com.alibaba.nacos.common.utils.StringUtils;
import com.alibaba.nacos.plugin.visibility.model.VisibilityResource;
import com.alibaba.nacos.plugin.visibility.spi.VisibilityResourceLocator;
import org.springframework.stereotype.Service;

import java.util.Optional;

import static com.alibaba.nacos.api.common.Constants.DEFAULT_NAMESPACE_ID;

/**
* AI implementation of {@link VisibilityResourceLocator}.
*
* @author Zhengcy05
*/
@Service
public class AiVisibilityResourceLocator implements VisibilityResourceLocator {
Comment thread
Zhengcy05 marked this conversation as resolved.

private final AiResourcePersistService aiResourcePersistService;

public AiVisibilityResourceLocator(AiResourcePersistService aiResourcePersistService) {
this.aiResourcePersistService = aiResourcePersistService;
}

@Override
public Optional<VisibilityResource> findResource(String namespaceId, String resourceType,
String resourceName) {
String resolvedNamespaceId =
StringUtils.isBlank(namespaceId) ? DEFAULT_NAMESPACE_ID : namespaceId;
// The auth plugin resolves resources by the same identity tuple used by AI persistence.
AiResource resource =
aiResourcePersistService.find(resolvedNamespaceId, resourceName, resourceType);
return Optional.ofNullable(resource).map(each -> (VisibilityResource) each);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public QueryCondition convert(QueryCondition condition, String identity, QueryAd
applyPublicAndOwner(result, identity);
break;
}
// TODO: stage-2 authorized resources integration.
// Explicitly authorized names are added as an OR branch so storage can page and count
// the complete visible set instead of filtering a fetched page in memory.
List<String> authorized =
advisor.getAuthorizedPredicate() == null ? null : advisor.getAuthorizedPredicate()
.getResources();
Expand Down
4 changes: 4 additions & 0 deletions persistence/src/test/resources/META-INF/derby-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,17 @@ CREATE TABLE roles (
constraint uk_username_role UNIQUE (username,role)
);

CREATE INDEX idx_role_user ON roles(role,username);

CREATE TABLE permissions (
role varchar(50) NOT NULL,
resource varchar(512) NOT NULL,
action varchar(8) NOT NULL,
constraint uk_role_permission UNIQUE (role,resource,action)
);

CREATE INDEX idx_permission_resource ON permissions(resource,action,role);



/******************************************/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
import com.alibaba.nacos.plugin.auth.api.Resource;
import com.alibaba.nacos.plugin.auth.constant.SignType;
import com.alibaba.nacos.plugin.auth.impl.constant.AuthConstants;
import com.alibaba.nacos.plugin.auth.impl.users.NacosUser;
import com.alibaba.nacos.plugin.auth.impl.utils.AuthIdentityUtils;
import com.alibaba.nacos.plugin.auth.impl.visibility.VisibilityGrantService;
import com.alibaba.nacos.plugin.auth.spi.server.AuthPluginManager;
import com.alibaba.nacos.plugin.auth.spi.server.AuthPluginService;
import com.alibaba.nacos.plugin.visibility.constant.VisibilityConstants;
Expand All @@ -37,21 +38,24 @@
import com.alibaba.nacos.plugin.visibility.spi.QueryAdvisor;
import com.alibaba.nacos.plugin.visibility.spi.ValidationResult;
import com.alibaba.nacos.plugin.visibility.spi.VisibilityService;
import com.alibaba.nacos.sys.utils.ApplicationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;

/**
* Default AI visibility service implementation for nacos auth plugin.
* Default visibility service implementation for Nacos auth plugin.
*
* @author xiweng.yy
*/
public class DefaultAiVisibilityService implements VisibilityService {
public class DefaultVisibilityService implements VisibilityService {

private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAiVisibilityService.class);
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultVisibilityService.class);

private static final String NAME = AuthConstants.AUTH_PLUGIN_TYPE;

Expand Down Expand Up @@ -86,15 +90,12 @@ public QueryAdvisor adviseQuery(String identity, String action, String apiType,
}
if (!VisibilityConstants.ACTION_READ.equals(action)) {
advisor.setBasePredicate(BaseVisibilityPredicate.OWNER);
advisor.setAuthorizedPredicate(buildAuthorizedResources(identity, action, context));
return advisor;
}
advisor.setBasePredicate(isAnonymousIdentity(identity) ? BaseVisibilityPredicate.PUBLIC
: BaseVisibilityPredicate.PUBLIC_AND_OWNER);
AuthorizedResources authorized = new AuthorizedResources();
authorized.setResourceType(context == null ? null : context.getResourceType());
// TODO: populate explicit authorized resources from auth plugin once query advisor integration is complete.
authorized.setResources(new ArrayList<>());
advisor.setAuthorizedPredicate(authorized);
advisor.setAuthorizedPredicate(buildAuthorizedResources(identity, action, context));
return advisor;
}

Expand Down Expand Up @@ -139,7 +140,7 @@ private boolean checkResourcePermission(VisibilityResource res, String action) {
return false;
} catch (Exception e) {
LOGGER.debug(
"[DefaultAiVisibilityService] Permission check failed for resource '{}': {}",
"[DefaultVisibilityService] Permission check failed for resource '{}': {}",
resourceId,
e.getMessage());
return false;
Expand Down Expand Up @@ -171,19 +172,31 @@ private boolean isAnonymousIdentity(String identity) {
}

private boolean isCurrentIdentityGlobalAdmin(String identity) {
if (StringUtils.isBlank(identity)) {
return false;
return AuthIdentityUtils.isCurrentIdentityGlobalAdmin(identity);
}

private AuthorizedResources buildAuthorizedResources(String identity, String action,
VisibilityQueryContext context) {
AuthorizedResources authorized = new AuthorizedResources();
authorized.setResourceType(context == null ? null : context.getResourceType());
authorized.setResources(new ArrayList<>());
if (context == null || StringUtils.isBlank(identity)) {
return authorized;
}
try {
IdentityContext identityContext =
RequestContextHolder.getContext().getAuthContext().getIdentityContext();
Object nacosUser = identityContext.getParameter(AuthConstants.NACOS_USER_KEY);
if (!(nacosUser instanceof NacosUser user)) {
return false;
}
return identity.equals(user.getUserName()) && user.isGlobalAdmin();
} catch (Exception e) {
return false;
if (ApplicationUtils.getApplicationContext() == null) {
// Unit tests and lightweight runtimes may call the advisor before Spring context is ready.
return authorized;
}
AtomicReference<VisibilityGrantService> serviceRef = new AtomicReference<>();
ApplicationUtils.getBeanIfExist(VisibilityGrantService.class, serviceRef::set);
VisibilityGrantService grantService = serviceRef.get();
if (grantService == null) {
return authorized;
}
// The domain adapter combines these names with the base predicate before count and paging.
List<String> resources = grantService.findAuthorizedResourceNames(identity,
context.getNamespaceId(), context.getResourceType(), action);
authorized.setResources(new ArrayList<>(resources));
return authorized;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

package com.alibaba.nacos.plugin.auth.impl.configuration.web;

import com.alibaba.nacos.plugin.auth.impl.controller.v3.VisibilityGrantControllerV3;
import com.alibaba.nacos.plugin.auth.impl.authenticate.IAuthenticationManager;
import com.alibaba.nacos.plugin.auth.impl.controller.v3.PermissionControllerV3;
import com.alibaba.nacos.plugin.auth.impl.controller.v3.RoleControllerV3;
import com.alibaba.nacos.plugin.auth.impl.controller.v3.UserControllerV3;
import com.alibaba.nacos.plugin.auth.impl.roles.NacosRoleService;
import com.alibaba.nacos.plugin.auth.impl.token.TokenManagerDelegate;
import com.alibaba.nacos.plugin.auth.impl.users.NacosUserService;
import com.alibaba.nacos.plugin.auth.impl.visibility.VisibilityGrantService;
import com.alibaba.nacos.plugin.auth.impl.visibility.DefaultVisibilityGrantService;
import org.springframework.context.annotation.Bean;

/**
Expand Down Expand Up @@ -50,4 +53,16 @@ public RoleControllerV3 roleControllerV3(NacosRoleService roleService) {
public PermissionControllerV3 permissionControllerV3(NacosRoleService roleService) {
return new PermissionControllerV3(roleService);
}

@Bean
public VisibilityGrantService visibilityGrantService(NacosRoleService roleService,
NacosUserService userService) {
return new DefaultVisibilityGrantService(roleService, userService);
}

@Bean
public VisibilityGrantControllerV3 visibilityGrantControllerV3(
VisibilityGrantService visibilityGrantService) {
return new VisibilityGrantControllerV3(visibilityGrantService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ public class AuthConstants {

public static final String PERMISSION_PATH = "/v3/auth/permission";

public static final String VISIBILITY_PATH = "/v3/auth/visibility";

public static final String VISIBILITY_RESOURCE = "auth/visibility";

/**
* Prefix for system-reserved per-user roles used by explicit visibility grants.
*/
public static final String VISIBILITY_GRANT_ROLE_PREFIX = "__nacos_vis__.";

/**
* System-reserved anonymous user name. Used for unauthenticated access to PUBLIC AI resources.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public Result<String> deletePermission(@RequestParam String role, @RequestParam
* Query permissions of a role.
*
* @param role the role
* @param resource the resource
* @param pageNo page index
* @param pageSize page size
* @param search the type of search (accurate or blur)
Expand All @@ -109,9 +110,13 @@ public Result<String> deletePermission(@RequestParam String role, @RequestParam
public Result<Page<PermissionInfo>> getPermissionList(@RequestParam int pageNo,
@RequestParam int pageSize,
@RequestParam(name = "role", defaultValue = StringUtils.EMPTY) String role,
@RequestParam(name = "resource", defaultValue = StringUtils.EMPTY) String resource,
@RequestParam(name = "search", defaultValue = "accurate") String search) {
Page<PermissionInfo> permissionPage;
if (SEARCH_TYPE_BLUR.equalsIgnoreCase(search)) {
if (StringUtils.isNotBlank(resource)) {
permissionPage = nacosRoleService.getPermissionsByResource(resource, pageNo,
pageSize);
} else if (SEARCH_TYPE_BLUR.equalsIgnoreCase(search)) {
permissionPage = nacosRoleService.findPermissions(role, pageNo, pageSize);
} else {
permissionPage = nacosRoleService.getPermissions(role, pageNo, pageSize);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 1999-2026 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.plugin.auth.impl.controller.v3;

import com.alibaba.nacos.api.annotation.Since;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.model.v2.Result;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.plugin.auth.constant.ActionTypes;
import com.alibaba.nacos.plugin.auth.constant.Constants;
import com.alibaba.nacos.plugin.auth.impl.constant.AuthConstants;
import com.alibaba.nacos.plugin.auth.impl.visibility.VisibilityGrantInfo;
import com.alibaba.nacos.plugin.auth.impl.visibility.VisibilityGrantService;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* Plugin-owned visibility grant API.
*
* @author Zhengcy05
*/
@RestController
@RequestMapping(AuthConstants.VISIBILITY_PATH)
public class VisibilityGrantControllerV3 {

private final VisibilityGrantService visibilityGrantService;

public VisibilityGrantControllerV3(VisibilityGrantService visibilityGrantService) {
this.visibilityGrantService = visibilityGrantService;
}

/**
* Grant one visibility action to a user for a resource.
*
* @param namespaceId namespace ID, blank for the default namespace
* @param resourceType resource type
* @param resourceName resource name
* @param username grantee username
* @param action grant action
* @return success result
* @throws NacosException if validation or grant management fails
*/
@Since("3.3.0")
@PostMapping
@Secured(resource = AuthConstants.VISIBILITY_RESOURCE, action = ActionTypes.WRITE,
tags = Constants.Tag.ONLY_IDENTITY)
public Result<String> grant(@RequestParam(required = false) String namespaceId,
@RequestParam String resourceType, @RequestParam String resourceName,
@RequestParam String username, @RequestParam String action) throws NacosException {
visibilityGrantService.grant(namespaceId, resourceType, resourceName, username, action);
return Result.success("grant visibility permission ok!");
}

/**
* Revoke one visibility action from a user for a resource.
*
* @param namespaceId namespace ID, blank for the default namespace
* @param resourceType resource type
* @param resourceName resource name
* @param username grantee username
* @param action grant action
* @return success result
* @throws NacosException if validation or grant management fails
*/
@Since("3.3.0")
@DeleteMapping
@Secured(resource = AuthConstants.VISIBILITY_RESOURCE, action = ActionTypes.WRITE,
tags = Constants.Tag.ONLY_IDENTITY)
public Result<String> revoke(@RequestParam(required = false) String namespaceId,
@RequestParam String resourceType, @RequestParam String resourceName,
@RequestParam String username, @RequestParam String action) throws NacosException {
visibilityGrantService.revoke(namespaceId, resourceType, resourceName, username, action);
return Result.success("revoke visibility permission ok!");
}

/**
* List grants attached to one resource.
*
* @param namespaceId namespace ID, blank for the default namespace
* @param resourceType resource type
* @param resourceName resource name
* @return current grants
* @throws NacosException if validation or grant management fails
*/
@Since("3.3.0")
@GetMapping("/list")
@Secured(resource = AuthConstants.VISIBILITY_RESOURCE, action = ActionTypes.READ,
tags = Constants.Tag.ONLY_IDENTITY)
public Result<List<VisibilityGrantInfo>> list(
@RequestParam(required = false) String namespaceId,
@RequestParam String resourceType, @RequestParam String resourceName)
throws NacosException {
return Result.success(visibilityGrantService.list(namespaceId, resourceType, resourceName));
}
}
Loading
Loading