Skip to content

[Refactor] Pack namespace permission api to one #5304

@spaceluke

Description

@spaceluke

Is your feature request related to a problem? Please describe.

According to #5301

In order to better expand the permission management capabilities of Apollo Portal in the future, the existing Namespace-related permission management has been sorted out and integrated.

Describe the solution you'd like

Namespace权限模型梳理

appId env cluster namespace Model
☑️ AppId → *
☑️ ☑️ AppId → Namespace
☑️ ☑️ AppId + Env → *
☑️ ☑️ ☑️ AppId + Env → Namespace
☑️ ☑️ ☑️ AppId + Env + Cluster → *
☑️ ☑️ ☑️ ☑️ AppId + Env + Cluster → Namespace

前三个字段是限定了一个权限scope,范围从App到Env再到Cluster三级维度。

因为是针对Namespace的权限,所以Namespace字段是比较不同的,为空时代表指向当前scope下所有Namespace,否则指向所有名为传入值的Namespace。

Model Target PermissionType (e.g. Modify) TargetId
AppId → * AppId的所有namespace
AppId → Namespace AppId下的所有指定名字的namespace ModifyNamespace AppId+Namespace
AppId + Env → * AppId的env下所有namespace
AppId + Env → Namespace AppId的env下所有指定名字的namespace ModifyNamespace AppId+Namespace+Env
AppId + Env + Cluster → * AppId的env中cluster的所有namespace *ModifyNamespaceInCluster AppId+Env+ClusterName
AppId + Env + Cluster → Namespace AppId的env中cluster下指定名字的namespace

考虑到向前兼容,兼容原有的 PermissionType ,原有的 AppId → NamespaceAppId + Env → Namespace 两种权限模型的 PermissionType 都为 ModifyNamespace/ReleaseNamespace ,包括原有的TargetId格式,都是不能修改的。

Apollo的权限校验方式是这样的:

image

原有的两种权限模型的PermissionType是一样的,而在匹配需要的Permission的时候是要用Type+TargetId的,TargetId在Apollo中是用字符串拼接的方式生成的,中间用“+”号隔开,举个例子:

  • AppId → Namespace: ModifyNamespace - test20251228+application
  • AppId + Env → Namespace: ModifyNamespace - test20251228+application+LOCAL

因为参数数量分别是两个和三个,当Type相同,TargetId不会起歧义,指向错误的目标。

但是后续的权限模型的Type不能再继续沿用这个TypeName了,因为还有其他三个参数的权限模型存在,比如说 AppId + Env + Cluster → * 这个模型。

TargetId可能是test20251228+LOCAL+PRO ,也就是**“LOCAL环境下的PRO集群”,这种情况下就会和“PRO环境下所有名为LOCAL的Namespace”**产生二义性,这在权限系统中是危险的,可能会出现越权行为。

因此除了现有的两种存量权限模型,后续新增的Namespace权限模型,都需要有自己独特的PermissionType,以避免TargetId出现二义性问题。

e.g. 本次新增的AppId + Env + Cluster → *的PermissionType为 ModifyNamespaceInCluster / ReleaseNamespaceInCluster

整体架构

image 1

改动点

1. Annotation 入口

全部改为四个入参的方法

2. Api 入口

同步Namespace能力接口

Before:

  @PutMapping(value = "/apps/{appId}/namespaces/{namespaceName}/items", consumes = {"application/json"})
  public ResponseEntity<Void> update(@PathVariable String appId, @PathVariable String namespaceName,
                                     @RequestBody NamespaceSyncModel model) {
    checkModel(!model.isInvalid() && model.syncToNamespacesValid(appId, namespaceName));
    boolean hasPermission = permissionValidator.hasModifyNamespacePermission(appId, namespaceName);
    Env envNoPermission = null;
    // if uses has ModifyNamespace permission then he has permission
    if (!hasPermission) {
      // else check if user has every env's ModifyNamespace permission
      hasPermission = true;
      for (NamespaceIdentifier namespaceIdentifier : model.getSyncToNamespaces()) {
        // once user has not one of the env's ModifyNamespace permission, then break the loop
        hasPermission &= permissionValidator.hasModifyNamespacePermission(namespaceIdentifier.getAppId(), namespaceIdentifier.getNamespaceName(), namespaceIdentifier.getEnv().toString())
        || permissionValidator.hasModifyClusterPermission(namespaceIdentifier.getAppId(), namespaceIdentifier.getEnv().toString(), namespaceIdentifier.getClusterName());
        if (!hasPermission) {
          envNoPermission = namespaceIdentifier.getEnv();
          break;
        }
      }
    if (hasPermission) {
      configService.syncItems(model.getSyncToNamespaces(), model.getSyncItems());
      return ResponseEntity.status(HttpStatus.OK).build();
    }
    throw new AccessDeniedException(String.format("You don't have the permission to modify namespace: %s", noPermissionNamespace));
  }

原本逻辑:

  1. 校验是否有 AppId → Namespace 权限,如有则直接放行
  2. if 没有,则分别校验对每个Namespace的 AppId + Env → Namespace 权限
  3. if 其中一个Namespace没有权限,则抛出无权限异常

改动逻辑:

  • 取消掉第一步校验,直接对每个Namespace调用统一的接口进行权限校验

After:

  @PutMapping(value = "/apps/{appId}/namespaces/{namespaceName}/items", consumes = {"application/json"})
  public ResponseEntity<Void> update(@PathVariable String appId, @PathVariable String namespaceName,
                                     @RequestBody NamespaceSyncModel model) {
    checkModel(!model.isInvalid() && model.syncToNamespacesValid(appId, namespaceName));
    NamespaceIdentifier noPermissionNamespace = null;
    // check if user has every namespace's ModifyNamespace permission
    boolean hasPermission = true;
    for (NamespaceIdentifier namespaceIdentifier : model.getSyncToNamespaces()) {
      // once user has not one of the namespace's ModifyNamespace permission, then break the loop
      hasPermission = permissionValidator.hasModifyNamespacePermission(
          namespaceIdentifier.getAppId(),
          namespaceIdentifier.getEnv().getName(),
          namespaceIdentifier.getClusterName(),
          namespaceIdentifier.getNamespaceName()
      );
      if (!hasPermission) {
        noPermissionNamespace = namespaceIdentifier;
        break;
      }
    }
    if (hasPermission) {
      configService.syncItems(model.getSyncToNamespaces(), model.getSyncItems());
      return ResponseEntity.status(HttpStatus.OK).build();
    }
    throw new AccessDeniedException(String.format("You don't have the permission to modify namespace: %s", noPermissionNamespace));
  }

思考 & 风险点 & 改进点

这样每次进行一次权限校验,可能会涉及到多次数据库IO,可能会成为性能瓶颈,后续可以改为一次查询,或引入一些缓存方案来解决。

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/portalapollo-portalfeature requestCategorizes issue as related to a new feature.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions