Zanzibar与Ory/Keto: 权限管理服务简介


May 07, 2021

用户的权限管理对每个项目来说都至关重要。不同的业务场景决定了不同的权限管理需求,不同的技术栈也有不同的解决方案:如果你在写一个Ruby On Rails应用,那你可能会选择cancan, 如果你正在使用K8S,那你很可能需要与K8S的RBAC系统打交道。那如果你面对一个非常复杂的业务,需要实现极为灵活的权限配置,并且同时对接多个服务怎么办呢?谷歌的一致性全球授权系统Zanzibar以及其开源实现Ory/Keto或许可以帮到你。

Zanzibar

简介

Google Zanzibar是谷歌2016年起上线的一致性全球授权系统。这套系统的主要功能是

  1. 储存来自各个服务的访问控制列表(Access Control Lists, ACLs),也就是所谓的权限(Permission)。
  2. 根据储存的ACL,进行权限校验。

这套系统上线后对接的服务有谷歌地图,谷歌图片,谷歌云盘,GCP,以及Youtube等等重要的服务 google-zanzibar 为了服务如此重要的业务,Zanzibar有着以下特点:

  1. 一致性:面对并发度如此大的业务场景,Zanzibar在检查权限的同时必须保证按照各个ACL的添加顺序判断。比如A添加了一条规则后立即删除,这两个动作如果没有按照正确的顺序执行,那么会造成权限泄露。
  2. 灵活性:各个业务场景的鉴权需求都不尽相同,所以Zanzibar灵活地支持不同的权限模式
  3. 横向扩展:以横向扩展支持数万亿条规则,每秒百万级鉴权
  4. 性能:95%的请求10毫秒内完成,99%的请求100毫秒内完成
  5. 可用性:上线三年间保证了99.999%的可用时间

以上的各个特性中除了灵活性之外都是性能或算法上的特点,性能和可靠性上也有很大一部分得益于底层的Spanner数据库。如果有兴趣可以阅读以下这篇论文:Zanzibar: Google’s Consistent, Global Authorization System对Zanzibar进行更深入的了解。下面我们就灵活性这一特点看一下Zanzibar是如何定义鉴权模型的。

概念与定义

关系元组(Relation Tuples)

Relation Tuples是Zanzibar的核心概念,一条Relation Tuples就对应了一条ACL。关系元组由命名空间(Namespace),对象(Object),关系(Relation)和主体(Subject)组成。一条Relation Tuples可以用BNF语法这样描述:

<relation-tuple> ::= <object>'#'relation'@'<subject>
<object> ::= namespace':'object_id
<subject> ::= subject_id | <subject_set>
<subject_set> ::= <object>'#'relation

一条Relation Tuples写作

namespace:object#relation@subject

意味着subjectnamespace中的object有一种relation

换成更有语义的例子:

videos:cat.mp4#view@felix

就意味着felixvideos中的cat.mp4view的关系。

上述BNF定义的第四条subject_set是由<object>'#'relation组成的,也就是代表了一群对某种objectrelationsubject。举例来说就是

groups:admin#member@felix
groups:admin#member@john
videos:cat.mp4#view@(groups:admin#member)

在这个例子中,felixjohn都对groups:adminmember的关系,而对groups:adminmember的关系的subject_setvideos:cat.mp4view的关系。也就是说felixjohn都对videos:cat.mp4view的关系。这种嵌套的语法可以有很多层,从而达到了整个ACL规则灵活可配的目的。

命名空间(Namespaces), 对象(Object)与主体(Subject)

Zanzibar中的Namespace并不是起隔离作用的,就像上面的那个例子,在编写videosNamespace时也可以引用groupsNamespace。这里的命名空间概念更多是用来将数据分为同质的分块(并应用不同的配置),并且在储存层面上也是分离的。所以在多租户的使用场景中,用租户的UUID作为Namespace并不是一个好的选择,而应该使用tenants作为Namespace,从而实现

tenants:tenant-id-1#member@felix
tenants:tenant-id-1#member@john

这样的Relation Tuples,并且用tenants:tenant-id-1#member作为鉴权的subject_set。在命名方面,一般建议Namespace使用单词的复数形式,而Object和Subject使用UUID。 将Relation Tuples转换为图有助于更好地理解object与subject之间的关系,考虑Keto官方文档上的以下例子

// user1 has access on dir1
dir1#access@user1
// Have a look on the subjects concept page if you don't know the empty relation.
dir1#parent@(file1#)
// Everyone with access to dir1 has access to file1. This would probably be defined
// through a subject set rewrite that defines this inherited relation globally.
// In this example, we define this tuple explicitly.
file1#access@(dir1#access)
// Direct access on file2 was granted.
file2#access@user1
// user2 is owner of file2
file2#owner@user2
// Owners of file2 have access to it; possibly defined through subject set rewrites.
file2#access@(file2#owner)

将其转换为图可以得到:

Subject ID region
Object region
parent
access
file1#access
access
access
owner
access
file2#access
user1
user2
file1
dir1
file2
dir1#access
file2#owner

其中实线代表了直接定义的关系,而虚线代表了由Subject Set继承而来的关系。

Keto

Ory/Keto是谷歌Zanzibar的第一个也是目前唯一一个开源实现。Keto用golang实现并兼容Zanzibar的概念,它作为一个单独的服务部署提供了RestfulAPI以及Grpc两种调用方式。后端存储使用了PostgreSQL/MySQL,官方并未公布其具体的性能表现,但比起使用Spanner的Zanzibar来说,性能应该是差一些的。

动手尝试

Keto提供了一个简单的例子来展示他是如何工作的

# clone the repository if you don't have it yet
git clone git@github.com:ory/keto.git && cd keto

docker-compose -f contrib/cat-videos-example/docker-compose.yml up

注意如果这里如果报出了以下错误

ERROR: The Compose file './contrib/cat-videos-example/docker-compose.yml' is invalid because:
services.keto.volumes contains an invalid type, it should be a string
services.keto-init.volumes contains an invalid type, it should be a string

那么你需要将contrib/cat-videos-example/docker-compose.yml这个文件的第一行改为version: '3.2' 这里的keto实例使用了内存储存,所以并不需要一个额外的数据库。 执行

docker-compose -f contrib/cat-videos-example/docker-compose.yml exec keto sh

进入到容器中后就可以使用keto的命令行工具了。这里默认装载了contrib/cat-videos-example/relation-tuples下的所有规则,在生产环境中可以像这样用keto命令行 直接装载relation tuples文件,也可以使用API动态添加/删除。执行

keto relation-tuple get videos

可以输出在videos namespace 下的所有规则

videos          /cats           owner           cat lady
videos          /cats           view            videos:/cats#owner
videos          /cats/1.mp4     owner           videos:/cats#owner
videos          /cats/1.mp4     view            *
videos          /cats/1.mp4     view            videos:/cats/1.mp4#owner
videos          /cats/2.mp4     owner           videos:/cats#owner
videos          /cats/2.mp4     view            videos:/cats/2.mp4#owner

检查所有人对/cats/2.mp4的访问权限:

keto check "*" view videos /cats/2.mp4
Denied

可以看到输出了拒绝访问, 而cat lady由于第1,6,7条规则,是可以访问的

keto check "cat lady" view videos /cats/2.mp4
Allowed

如果想搞明白/cats/2.mp4可以被谁访问,以及其原因,可以使用expand命令

keto expand view videos /cats/2.mp4
∪ videos:/cats/2.mp4#view
├─ ∪ videos:/cats/2.mp4#owner
│  ├─ ∪ videos:/cats#owner
│  │  ├─ ☘ cat lady️

输出的树中代表了Subject Set, 而代表了最终可以访问ObjectSubject

应用

学会了keto和Zanzibar的基础概念和用法后,让我们通过一个例子看一下如何将keto应用到服务的鉴权中去。

假设我们维护一个多租户的视频服务,我们仅允许特定的视频对某一个租户(tenant)的成员访问。比如下在面这个例子中,我们有一个租户cat,视频cats.mp4仅允许租户catmember观看。对于这个业务我们部署一个Profile Service用于用于的注册登陆和验证,以及一个Video Service用于为用户提供视频。另外部署了一个Keto服务做集中式的鉴权。

整个鉴权的流程如下图所示:

keto-sequence-flow

让我们走一遍整个流程:

  1. Video Service预先在Keto中写入了一条规则videos:cats.mp4#view@(tenants:cat#member),即仅允许tenants:catmemberview目标videos:cats.mp4
  2. 用户Felix想要访问cats.mp4,但他在系统里并没有注册过,所以以匿名用户的身份对Video Service发起请求
  3. Video Service接收到请求后向Keto确认主体*是否可以访问videos:cats.mp4,由于之前写入的规则,请求被Keto判断为拒绝,随即felix的请求也失败了
  4. felix向Profile Service发起请求注册为租户catmember, Profile Service完成注册后向Keto写入了tenants:cat#member@felix
  5. 注册完成的felix再次向Video Service发起对cats.mp4的请求,此时他已经获得了用户felix的身份,假设这里他将用户的session放在cookie中发起请求
  6. Video Service将Cookie中的session提取出来后向Profile Service验证用户,通过后向Keto确认主体felix是否可以访问videos:cats.mp4
  7. 此时由于felix已经是tenants:catmember,Keto判定请求通过,最后Video Service将cats.mp4返回给Felix

从这个例子我们可以看到,负责提供视频服务的Video Service自始至终只作了两件事:1. 向Keto写入视频权限规则 2. 向Keto询问某个用户是否有权限。而具体用户的角色Role的管理则是由Profile Service统一负责。在这个架构下,我们自己的两个服务在权限这个问题上完全是解耦的,所有的权限规则写入和检验全都由Keto统一负责,显著地增强了整个系统的弹性和可扩展性。

总结

Zanzibar及其开源实现Ory/Keto致力于实现一套灵活,弹性,可扩展,高可用的鉴权服务体系。Zanzibar在过去的几年中支持了几乎所有的谷歌云上服务,已经印证了它灵活性的价值,而谷歌这一套系统支持全球业务的运维能力也令人赞叹。而Keto也让我们使用这套理念治理自己项目的权限成为可能,但这不意味着Keto就是解决权限问题的银弹,部署一个单独的服务来做中央式权限管理对运维的要求相当高,并且不是每一个项目都需要如此灵活的权限管理。Zanzibar所提出的这套基于Relation Tuple的权限管理概念十分值得学习,它找到一个灵活性与复杂度的平衡点,使得相对简单的语法也能支持复杂多变的场景。

参考链接

  1. https://research.google/pubs/pub48190/
  2. https://www.youtube.com/watch?v=mstZT431AeQ
  3. https://research.google/pubs/pub39966/
  4. https://github.com/ory/keto
  5. https://www.ory.sh/keto/
  6. https://www.ory.sh/looking-at-keto/

本作品采用知识共享署名4.0署名-非商业性使用-禁止演绎(BY-NC-ND)国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,但不得对本创作进行修改,亦不得依据本创作进行再创作,不得将本创作运用于商业用途。