Keto 实现 ABAC 权限

前言

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

Zanzibar是什么?

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

  1. 储存来自各个服务的访问控制列表(Access Control Lists, ACLs),也就是所谓的权限(Permission)。
  2. 根据储存的 ACL,进行权限校验。
    这套系统上线后对接的服务有谷歌地图,谷歌图片,谷歌云盘,GCP,以及 Youtube 等等重要的服务
    [图片]

为了服务如此重要的业务,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

意味着subject对namespace中的object有一种relation。换成更有语义的例子:

videos:cat.mp4#view@felix

就意味着felix对videos中的cat.mp4有view的关系。上述BNF定义的第四条subject_set是由'#'relation组成的,也就是代表了一群对某种object有relation的subject。举例来说就是

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

在这个例子中,felix和john都对groups:admin有member的关系,而对groups:admin有member的关系的subject_set对videos:cat.mp4有view的关系。也就是说felix和john都对videos:cat.mp4有view的关系。这种嵌套的语法可以有很多层,从而达到了整个ACL规则灵活可配的目的。

命名空间 / 客体 / 主体 / 关系图

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#child@(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)

以上 BNF 语句表示的关系图如下:
[图片]

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
# or
./contrib/cat-videos-example/up.sh

# output: all initially created relation tuples

# NAMESPACE       OBJECT          RELATION NAME   SUBJECT
# videos          /cats/1.mp4     owner           videos:/cats#owner
# videos          /cats/1.mp4     view            videos:/cats/1.mp4#owner
# videos          /cats/1.mp4     view            *
# videos          /cats/2.mp4     owner           videos:/cats#owner
# videos          /cats/2.mp4     view            videos:/cats/2.mp4#owner
# videos          /cats           owner           cat lady
# videos          /cats           view            videos:/cats#owner

进入到容器中后就可以使用 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, 而☘代表了最终可以访问Object的Subject。

数据表结构

Relation tuple table

CREATE TABLE keto_relation_tuples
(
    shard_id                 char(36)    NOT NULL,
    nid                      char(36)    NOT NULL,
    namespace_id             INTEGER     NOT NULL,
    object                   VARCHAR(64) NOT NULL,
    relation                 VARCHAR(64) NOT NULL,
    subject_id               VARCHAR(64) NULL,
    subject_set_namespace_id INTEGER NULL,
    subject_set_object       VARCHAR(64) NULL,
    subject_set_relation     VARCHAR(64) NULL,
    commit_time              TIMESTAMP   NOT NULL,

    PRIMARY KEY (shard_id, nid),

    -- enforce to have exactly one of subject_id or subject_set
    CONSTRAINT chk_keto_rt_subject_type CHECK
        ((subject_id IS NULL AND
          subject_set_namespace_id IS NOT NULL AND subject_set_object IS NOT NULL AND subject_set_relation IS NOT NULL)
            OR
         (subject_id IS NOT NULL AND
          subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL))
);

Namespace

CREATE TABLE keto_namespace
(
    id             INTEGER PRIMARY KEY,
    schema_version INTEGER NOT NULL
);
Networks(2015 年维护的表,现在可能没了)
CREATE TABLE networks 
(
    id              char(36) NOT NULL,
    created_at      TIMESTAMP   NOT NULL,
    updated_at        TIMESTAMP   NOT NULL
); 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值