# 安全规则

为了保护用户的数据安全,云开发提供更灵活、可扩展、更细粒度的安全规则能力,开发者可以在云后台或者小程序开发工具上自定义安全规则,限制 C 端用户对数据库的访问权限.

与之前数据库的权限设置一样,安全规则对数据库的权限控制是文档级别的访问控制,对文档的属性访问控制目前还不支持。

安全规则仅限制 C 端用户的访问请求,对于开发者的请求(如通过云函数的访问请求)不做任何限制。

# 配置

每个集合可以单独配置安全规则,通过 json 来描述安全规则配置,示例配置如下

{
  "read": "auth.uid==doc.uid",
  "write": "doc.name=='zzz'"
}

json 配置的 key,是用户的操作类型value 是一个表达式。当表达式执行结果的值是 true 的时候,用户的操作允许执行;否则,用户的操作则不被允许。

操作类型 说明 默认值
read 读文档 false
write 写文档,可以细分为 create、update、delete false
create 新建文档
update 更新文档
delete 删除文档

# 说明

如果开发者没有详细指定写操作具体类型的规则时,默认读取 write 的规则。例如说开发者的安全规则配置如下

{
  "read": ".....",
  "write": "....",
  "create": "...."
}

用户的新建文档操作会读取 create 的对应规则,而 update 操作会读取 write 的规则。

# 表达式

表达式是伪代码的语句,配置的时候不能过长,单条表达式限制 1024 字符。

# 变量

# 全局变量

变量名 类型 说明
request Request 请求挂载的根对象
auth Auth 用户登录信息,字段说明参见下文
now number 当前时间
doc any 文档数据或查询条件

# Request

字段名 类型 说明
data object 请求数据,create,update 时存在,代表请求 data

# Auth

字段名 类型 说明
loginType string 登录方式,微信小程序过来的请求是没有此值的;web 登录下才有此值
uid string 用户唯一 id,微信小程序的请求没有此值
openid string 用户 openid,仅在微信登录方式下存在值

# 运算符

运算符 说明 示例 示例解释(集合查询)
== 等于 auth.uid == 'zzz' 用户的 uid 为 zzz
!= 不等于 auth.uid != 'zzz' 用户的 uid 不为 zzz
> 大于 doc.age>10 查询条件的 age 属性大于 10
>= 大于等于 doc.age>=10 查询条件的 age 属性大于等于 10
< 小于 doc.age>10 查询条件的 age 属性小于 10
<= 小于等于 doc.age>=10 查询条件的 age 属性小于等于 10
in 存在于集合中 auth.uid in ['zzz','aaa'] 用户的 uid 是['zzz','aaa']中的一个
!(xx in []) 不存在于集合中,使用 in 的方式描述 !(a in [1,2,3]) !(auth.uid in ['zzz','aaa']) 用户的 uid 不是['zzz','aaa']中的任何一个
&& auth.uid == 'zzz' && doc.age>10 用户的 uid 为 zzz 并且查询条件的 age 属性大于 10
|| 或者 auth.uid == 'zzz' || doc.age>10 用户的 uid 为 zzz 或者查询条件的 age 属性大于 10
. 对象元素访问符 auth.uid 用户的 uid
[] 数组访问符属性 get('database.collection_a.user')[auth.uid] == 'zzz' collection_a 集合中 iduser 的文档,key 为用户 uid 的属性值为 zzz

# 函数

目前仅支持 get 函数,唯一的参数必须为 database.集合名称.文档id。通过访问其它文档的数据来判断用户操作是否符合安全规则。

# get

static

get(path) returns document 对象

根据参数获取指定 doc 内容。

# paramter
Parameter Type Description
path string 非空,格式为 database.集合名称.文档id 的字符串,值可以通过多种计算方式得到,例如使用字符串模版进行拼接(`database.${doc.collction}.${doc._id}`)
# Returns

undefined or null 表征 doc 不存在,否则为 document 对象,表征查询得到的数据

# Example
  1. 用户的权限是写在一个独立的文档 , 用一个数值表示用户的权限范围
{
  "read": "get('database.test.123')[auth.uid] in [1,2,3]",
  "delete": "get('xxxx')[auth.uid] == 1 && doc.user in ['ersed','sfsdf'] "
}
  1. 集合 A 包含 shopId、orderId 关联关系,集合 B 包含 owner,shopId 关联关系,对集合 A 查询,希望限制只查到当前用户有权限的 shop 的订单。
{
  "read:": "auth.openid in get(`database.B.${doc.shopId}`).owner"
}
# Limit
  1. get 参数中存在的变量 doc 需要在 query 条件中以 == 或 in 方式出现,若以 in 方式出现,只允许 in 唯一值, 即 doc.shopId in array, array.length == 1
  2. 一个表达式最多可以有 3get 函数,最多可以访问 10 个不同的文档。
  3. get 函数的嵌套深度最多为 2, 即 get(get(path))
# 说明
  1. 在未使用变量的情况下,每个 get 会产生一次读操作,在使用变量时,每个 get,每个变量值会产生一次读操作,例如:规则 get(`database.collection.${doc._id}`).test,在查询 _.or([{_id:1},{_id:2},{_id:3},{_id:4},{_id:5}])} 下会产生 5 次读取。系统会对同 doc,同 field 的读取进行缓存。

# 查询

# 有效的查询

在实际使用的过程中,查询主要分为两种,指定文档 id 的查询集合查询

指定文档 id 的查询通过 doc 条件指定单个文档 id。

集合查询可以是通过 where 条件的查询或者是聚合搜索 match 限制条件的查询,如果是聚合搜索,只匹配第一个 match 限制条件。

查询条件中,如果 key_openid 并且 value{openid},或者 keyuid 并且 value{uid},则在服务端自动将 value 替换为实际的值。

# 集合查询

对于集合查询,查询条件必须是安全规则的子集,doc 此时表示的是查询条件。这个子集是指规则上所有可能的子集,而不是实际数据的子集。

操作类型包括 readupdatedelete

示例

// colleciton_a 的安全规则的配置
{
    "read":"doc.age>10"
}

// 符合安全规则
let queryRes = db.collection('colleciton_a').where({
    age:_.gt(10)
}).get()

// 不符合安全规则
let queryRes = db.collection('colleciton_a').where({
    age:_.gt(8)
}).get

// 符合安全规则
let res = await db.collection('collection_a').aggregate().match({
  age: _.gt(10)
}).project({
  age: 1
}).end()

// 不符合安全规则
let res = await db.collection('collection_a').aggregate().match({
  age: _.gt(8)
}).project({
  age: 1
}).end()

# 指定文档id查询

对于指定文档 id 的查询,文档的数据必须符合安全规则,doc此时表示的是文档的数据。

操作类型包括 readupdatedeletecreate

如果操作类型是 create,会校验写入的数据是否符合安全规则的限制条件。

如果操作类型是 readupdatedelete,同时安全规则包含了 doc 的限制,则会先从 db 中读取一次文档数据,然后判断是否符合安全规则。

对于 update 操作,只会校验存在于数据库中已有的文档数据,不会对写入的数据进行校验;同时不保证这个操作的原子性。

示例

// colleciton_a 的安全规则的配置
{
    "read":"doc.age>10"
}

// 文档id='ccc'的数据是
{
  age:12
}

// 文档id='ddd'的数据是
{
  age:8
}

// 符合安全规则
let queryRes = db.collection('colleciton_a').doc('ccc').get()

// 不符合安全规则
let queryRes = db.collection('colleciton_a').doc('ddd').get()

# 支持的数据库 command

# logic command

command description
or || 逻辑或
and && 逻辑与

# query command

command description
eq ==
ne/neq !=
gt >
gte >=
lt <
lte <=
in in
nin !(in [])

# update command

command description
set 覆盖写,{key: set(object)}
remove 删除字段, {key: remove()}

# 计费

安全规则本身不收费,但是安全规则额外的数据访问会统计到计费中。

  • get 函数会产生额外的数据访问
  • 指定文档 id 查询的所有写操作会产生一次数据访问

# 之前的权限设置对比

# 所有用户可读,仅创建者及管理员可写

{
  "read": true,
  "write": "doc._openid == auth.openid" // 登录方式为微信
  "write": "doc._openid == auth.uid" // 登录方式为非微信
}

# 仅创建者及管理员可读写

{
  "read": "doc._openid == auth.openid" //登录方式为微信
  "read": "doc._openid == auth.uid" // 登录方式为非微信
  "write": "doc._openid == auth.openid" //登录方式为微信
  "write": "doc._openid == auth.uid" // 登录方式为非微信
}

# 所有用户可读,仅管理员可写

{
  "read": true,
  "write": false
}

# 仅管理员可读写

{
  "read": false,
  "write": false
}

# 实现基于角色的权限控制

您可以利用 TCB 的数据模型以及自定义的安全规则在您的应用中实现基于角色的访问权限控制。

假设您正在构建一款协作式撰文应用,用户可以按照以下安全要求在其中撰写“故事”和“评论”:

每个故事都有一名所有者;故事可共享给撰写者

撰写者除了拥有评论者所拥有的全部访问权限之外,还可以编辑故事内容。 所有者可以编辑故事的任意部分,并且可以控制其他用户的访问权限。 普通用户只能查看故事和评论,写自己的评论,不能编辑故事。

# 数据结构

假设您的应用有一个 stories 集合,其中每个文档代表一个故事。

还有一个 comments 集合,其中每个文档代表一条针对该故事的评论。

一个 roles 集合,每个集合包含一个故事的用户的角色

为了跟踪访问角色,需要添加一个 roles 字段,用来将用户 ID 映射至角色:

# stories 集合

每一个 story 文档数据数据如下

{
  "id": `storyid`,
  "title": "A Great Story",
  "content": "Once upon a time ..."
}

# roles 集合

每一个 role 文档数据数据如下

{
  "id": `storyid`,
  "roles": {
    "alice": "owner",
    "bob": "writer",
    "david": "writer"
    // ...
  }
}

# comments 集合

每一个 comment 文档数据如下,评论仅包含三个字段,留言者的用户 ID 、内容、storyid

{
  "id": `commentId`,
  "storyid": `storyid`,
  "user": "alice",
  "content": "I think this is a great story!"
}

# 规则

既然数据库中记录有用户角色,那么便需要编写安全规则来进行角色验证。下列规则假定应用使用的是 TCB 身份验证,所以 auth.uid 变量为用户 ID。

# roles 添加规则

管理者可以更改角色,允许故事撰写者读取角色

{
  "write": "doc.roles[auth.uid] === 'owner' ",
  "read": "doc.roles[auth.uid] in ['owner', 'writer']"
}

# stories 添加规则

管理者和故事撰写者可以更改故事,其它人可以读取故事

{
  "read": true,
  "write": "get(`database.roles.${doc.id}`).roles[auth.uid] in ['owner', 'writer'] "
}

# comments 规则

允许所有人发表评论。 只有评论的拥有者,可以更新和删除评论

{
  "read": true,
  "create": true,
  "update": "doc.user == auth.uid",
  "delete": "doc.user == auth.uid "
}