SVN/JFrog/NFS的ACL权限模型介绍
2022-03-06 / modified at 2022-05-27 / 2.2k words / 8 mins
️This article has been over 2 years since the last update.

很多版本管理工具都基于ACL Path实现了管理权限,本文加以综述介绍。

分析方法论

在权限设计讨论中,难度最高的就是关系和颗粒度

  • 继承与优先级关系:即 inheritance 和 priority 的问题 ,比如精确匹配优先 most specific path always matches first,还是最短优先(比如正则表达式)
  • 颗粒度:基于路径的读写即可。

本文对主流的SVN/Artifactory/NAS三种ACL模型进行分析。

背景技术介绍

什么是WebDAV

WebDAV可以被简单地被理解为基于HTTP的网盘,可以将一个HTTP Endpoint挂载到本地机器上读写。在日常生活中,常见的比如坚果云、日历等同步服务,在IT领域,有Artifactory/Subversion/S3等兼容服务,常见的Nginx、Apache、Caddy、Traefik、SVNKit服务均支持WebDAV插件。

同时,它与传统的NFS也有差距,本质是对象存储,比如不支持chmod,性能肯定不如Block储存。

WebDAV的权限模型

RFC规范中,它是一种基于HTTP XML的定义。是基于目录-用户-RW操作的ACL实现,没有RBAC(角色)的概念。

  • 在最主流实现的Apache(mod_dav)中,采用了XML来定义URL目录资源和用户。
  • 在Artifactory中,采用了基于JSON的PermissionTarget纯Java实现。
  • 在SVN中,采用了authz的文件实现,这个文件需要同时被Apache的httpd.conf读取到;SVN仓库本身的authz是可以非必要不操作的。

SVN的鉴权方式

虽然SVN是一种很老的工具,但是传统领域中仍然有庞大的存量用户。本文对SVN的权限模型进行介绍。SVN支持Socket协议和HTTP(S)协议(基于WebDAV),但是由于安全与兼容性,管理员更加偏好使用HTTP实现。本文只介绍HTTP的实现,即svn不开启socket,而通过apache代理静态源码文件的方案(即SVN侧没有任何守护进程)。

SVN权限权限拦截器的实现思路

  • 基于Apache mod_dav_svn,写一堆XML,但是需要同步authz文件,和文件系统绑死了,同时它是C语言项目,天天爆出漏洞,可想而知源码中有大量的祖传加固代码。
  • 定制开发,可以参考SVNKit/scm-manager的DAVServlet,自己开发filter,测试的难度高,但是高度定制,可以纯Java实现。
  • 定制开发,也可以参考其它云原生的middleware,或者策略引擎(比如OPA),进行网络层的ACL

SVN的Authn

这里即证明你是你,一般在Apache侧采用集成LDAP实现,直接Basic auth即可。

SVN的Authz文件格式介绍

这个文件本质上和AWS的JSON权限配置是一样的,唯一不同是历史非常悠久。它的语法类似toml

1
2
3
4
5
6
7
8
9
10
11
12
13
[groups]
<groupname> = <user>[,<user>...]
...

[<path in repository>]
@<group> = [rw|r]
<user> = [rw|r]
* = [rw|r]

[<repository name>:<path in repository>]
@<group> = [rw|r]
<user> = [rw|r]
* = [rw|r]

这个配置文件设计的非常糟糕,体现在

  • 群组groups与路径path共用一个语义
  • 蕴含的exclude语义没有体现,即* =表示禁止任何人访问;优先级也没有体现
  • 非常难测试,搭建依赖非常繁琐

它的源码位置位于 libsvn_repos/authz.c 的 svn_repos_authz_check_access,但是足足有1800的C代码,测试用例4500行,基本上是一个迷你的规则引擎了,所以分析源码不是上策。

Artifactory的权限模型

Artifactory是DevOp业界知名二进制仓库托管平台,它内部有一套简洁的ACL模型(可以扩展为RBAC),以下为OSS版的研究。个人认为这个项目的Authn和Authz非常完善,比SpringSecurity等框架有更好的实践。

Artifactory的Authn

AuthN即证明你是你。通过如下方法,采用编码方案(而非AOP/注解)在filter或者controller中实现

1
2
// 来自Spring security框架
org.springframework.security.core.context.SecurityContext#setAuthentication

关键类:

  • Authentication: 证明你是你,并录入缓存中。
  • SecurityContext:缓存,可以是ThreadLocal或者redis等,本场景中是ThreadLocal。

这里处理了LDAP/BasicAuth/Token等各种接入方案,但是它没有接入集中式的Session方案,相反从当前开源代码中猜测,收费版可能是转发Header/Token/License的方案实现的。

Artifactory的Authz

  • 全局与仓库:基于SpringSecurity的@RolesAllowed注解的模型,判断当前用户是管理员还是普通用户,一个简单的boolean存储,这个is_admin是直接扔到用户表中的,因此它不是重点。

  • 仓库内:基于Ant-style pattern实现的权限规则引擎,一般落地时以仓库key+include为主键。

  • 同样pathkey下,exclude的优先级更高

假如有如下仓库文件

1
2
3
4
5
/aaa
----/bbb
--------/ccc
--------test.csv
----/ddd

我希望让某个用户只能读写某个路径,那么可以配置如下

1
2
3
# includes
aaa/bbb/*
# grand user: abc with rw

这样用户就只能请求到符合要求的url

当需要判断权限时,比如当用户下载某个目录下的文件时,将调用如下服务

1
org.artifactory.api.security.AuthorizationService#canRead(org.artifactory.repo.RepoPath)

最核心的代码在

1
org.artifactory.security.SecurityServiceImpl#permissionCheckOnAcl

它是一个主要为两个for循环的方法,伪代码如下

1
2
3
4
5
boolean check(Repo repo, String dir){
return repo.targets.anyMatch(target->{
return !target.excludes.allMatch(dir) && target.includes.anyMatch(dir)
})
}

它通过牺牲空间避免了树的深度遍历,缺点就是target干涉太强了,导致excludes基本上就是废材。假如一个仓库有2个PermissionTarget,一个同意通过,一个被否决,最终eval的结果仍然是通过,导致exclude不生效。

三者测试对比

我们拿它与常见的工具进行对比

Artifactory场景(全局默认为禁止访问)

child dirparent dir实现方案子目录最终支持效果
rorw需要子节点配置excludero(只能做到叶子目录级别,否则需要递归)
rwro不涉及覆盖rw
rodefault不涉及覆盖ro
rwdefault不涉及覆盖rw

SVN场景(全局默认为禁止访问)

child dirparent dir实现方案子目录最终支持效果
rorw子节点覆盖父配置ro
rwro子节点覆盖父配置rw
rodefault不涉及覆盖ro
rwdefault不涉及覆盖rw

NAS场景(全局默认为禁止访问 )

child dirparent dir实现方案子目录最终支持效果
rorw子目录chmod 500ro
rwro子目录chmod 700rw
rodefault不涉及覆盖ro
rwdefault不涉及覆盖rw

综上,策略排序算法如下

child dirparent dirSVNArtifactoryNFS
rorwroro(limited)ro(500)
rwrorwrwrw(700)
rodefaultrorodeny
rwdefaultrwrwdeny

结论如下

  • SVN:路径先从长到短排序,找到第一个通过为止,类似回溯法的后续遍历。
  • Artifactory:两层for循环,第一层AnyMatch;第二层excludes一票否决,再任意匹配includes,是贪心方案。
  • NFS:访问子目录必须支持父目录,也是贪心方案

Node的元数据管理的系统设计

在最终落地时,需要用Postgres等数据去存储path的key-value数据(比如tag标记),主要有两个方案

  • 通过hstore/json存储数据,但是产生了供应商锁定
  • 通过node_prop的外键存储方案,更新与索引比较麻烦,比如Artifactory就设计了4个索引

经过对比,大容量(200M+)项目比如sonarqube/Artifactory的表结构均采用了传统外键的方案,而且这类项目均是高读写比的类型,建议采用传统方案。

附录

Artifactory的替代方案

假如你的场景仅仅是当作存储盘,而不会高度定制元数据,那么分布式存储是更好的方案。比如Lustre或者MinIO等,它们本身也有基础的ACL、挂载和replicate能力。但是NFS协议的Authn是基于客户端的UID实现,比较弱

Artifactory的挂载能力

Artifactory提供基础的WebDAV挂载能力,但是它需要单一凭证,无法实现类似NFS的按用户的LDAP AutoFS挂载能力。协议本身不支持文件属性,无法进行chmod。

Artifactory的Masterkey方案

它采用AES对称加密,初始安装时,masterKey与数据库口令等均通过配置文件明文保存,启动后此文件会被自动加密。如果需要更安全,可以移除masterKey,但是重启时需要修改配置文件并重新录入masterKey。目前看起来不支持定期CredentialRotation的功能,因此需要reset后重新加密,将涉及到停机。

关于SpringSecurity

研究了多款知名框架后,真正按照它默认注解模型的项目几乎没有,而全部是高度自定义+手写的方案,可以使用其中的SecureContext等模型,但是它的注解的确与configure的确反人类。