跳到主要内容
SDK 如何工作

本页尚未翻译,当前显示英文版本。

Access & Roles

角色由权限组合而成,从不硬编码。 原子权限组合成角色;角色把数据访问控制到行和列,还决定一个 build 能看到哪些模块接口。它取代了客户端密钥 / 服务器密钥的划分:凭据只标明身份,角色按每次请求解析。

何时使用

  • 你需要比“客户端”或“服务器”更窄的凭据:凭据背后组合出的角色(role)按每次请求解析。
  • 数据访问必须精确到行和列:按地区限定范围、屏蔽 PII、只读的外包人员。
  • build 只应看到它的角色解锁的接口:对访客来说,踢人和关闭房间根本不存在。
  • UI 必须准确地把按钮置灰:CanI 评估的正是服务器将要执行的同一套策略。
  • 无需使用:如果平台自带的预设(player、room-owner、seller……)已经符合你的主体(actor)。每个模块默认都遵循这些预设;完整列表见 核心概念。

分工

主体在本页中的职责
operator声明角色和策略,设置行 / 列限制,授予角色,签发密钥
match-organizer锦标赛工作人员:持有组合角色的密钥,审核报名,不能退款
每个主体行动前先检查 CanI;只看到自己解锁的接口

概览

声明带行 / 列限制的 entry-validator,授予该角色,然后在行动前检查 CanI
[Role("entry-validator")]
public class EntryValidator
{
    [Allow(Rooms.Membership.Administer)] public Permit GateEntries;
    [Allow(Data.Records.Read, table: "player_profile", rows: "banned == false",
           columns: "id, display_name")] public Permit SeeProfiles;
}

await PlayServ.Access.Grant(staffId, Roles.EntryValidator, Roles.MatchOrganizer);
var key = await PlayServ.Access.IssueKey(staffId);   // the credential names no roles

// any actor, before attempting an operation:
if (await PlayServ.Access.CanI(Commerce.Orders.Administer)) Hud.ShowRefund();
@Role('entry-validator')
export class EntryValidator {
  @Allow(Rooms.membership.administer) gateEntries: Permit;
  @Allow(Data.records.read, { table: 'player_profile', rows: 'banned == false',
                              columns: ['id', 'display_name'] }) seeProfiles: Permit;
}

await playserv.access.grant(staffId, Roles.entryValidator, Roles.matchOrganizer);
const key = await playserv.access.issueKey(staffId);  // the credential names no roles

// any actor, before attempting an operation:
if (await playserv.access.canI(Commerce.orders.administer)) hud.showRefund();
@role("entry-validator")
class EntryValidator:
    gate_entries = allow(rooms.membership.administer)
    see_profiles = allow(data.records.read, table="player_profile",
                         rows="banned == false", columns=["id", "display_name"])

await playserv.access.grant(staff_id, roles.ENTRY_VALIDATOR, roles.MATCH_ORGANIZER)
key = await playserv.access.issue_key(staff_id)   # the credential names no roles

# any actor, before attempting an operation:
if await playserv.access.can_i(commerce.orders.administer):
    hud.show_refund()
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

// declarations compile into the same pushed model — playserv push from the UE project or CI
USTRUCT(PSRole = "entry-validator")
struct FEntryValidator
{
    GENERATED_BODY()

    UPROPERTY(PSAllow = (Atom = "Rooms.Membership.Administer"))
    FPSPermit GateEntries;

    UPROPERTY(PSAllow = (Atom = "Data.Records.Read", Table = "player_profile",
                         Rows = "banned == false", Columns = "id, display_name"))
    FPSPermit SeeProfiles;
};

// granting is an operator act; a build checks what its identity resolves to
const FPSActor Me = Client->Whoami();   // which interfaces this actor unlocks
Client->Access->CanI(TEXT("Commerce.Orders.Administer"),
    TPSOnResult<bool>::CreateWeakLambda(this, [this](const TPSResult<bool>& Result)
    {
        if (!Result.HasValue()) { return; }
        if (Result.Value()) { Hud->ShowRefund(); }
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
// the same `[Role]` / `[Allow]` declaration as the server tab, on the Unity 2021.3 runtime
var me = playserv.Whoami();               // which interfaces this actor unlocks
if (await playserv.Access.CanI(Commerce.Orders.Administer)) hud.ShowRefund();

原子权限(permission atom)是一对组合:一个资源,加上四个动词之一——read、write、execute、administer。动词集合是固定的;遇到不合适的情况,就拆分资源,而不是扩充动词列表。所以审核别人的报名用的是 Rooms.Membership.Administer,而不是单独的 ValidateEntry 动词:操作另一个主体的成员资格属于管理,而自己加入则是同一资源上的 Rooms.Membership.Write。

模型

数据 ACL 是 role × operation × row predicate × field mask——同一个模型,无论是在代码中、通过 API 还是在管理面板的角色网格中编写,都完全一致。

访问控制由什么构成。

术语含义
原子权限一对 资源 × 动词。四个动词是 read(获取、选择、订阅)、write(创建、修改、删除,以及为自己行动——加入、离开)、execute(调用函数、施放技能)和 administer(对他人行动:踢人、关闭、强制切换状态)
角色一组具名的原子权限。可以包含另一个角色
角色预设由原子权限构成,随平台提供;对已经部署的使用方,它会保持原样继续生效。它是起点,而不是约束:项目可以用同样的原子权限声明自己的角色
行谓词哪些行——基于会话值的布尔谓词
字段掩码哪些字段,按角色和操作分别声明

凭据解析为什么。

凭据解锁什么
玩家密钥由引擎 build 携带;它背后的玩家随登录确定,build 能看到每张操作表中的 cl 行
主机密钥由专用服务器或主客户端持有,它的角色解锁 mc 行
推送的代码以项目的 backend-service 角色运行——排行榜的 Authoritative = true 检查的就是这一点
已注册的 hook不额外授予任何权限:你的函数保留部署时的角色

标签说明(fn / cl / mc / adm)见 核心概念。

每次检查都成立的规则。

始终成立含义
凭据不携带角色列表:它只标明身份(identity),角色在服务器端按每次请求解析。撤销会立即让缓存的解析结果失效,而不是等到声明的过期上限
委托只改变范围,从不改变能力:代表玩家行动时,改变的是哪些行可见、写入记在谁名下,不会授予主体原本没有的任何操作
动词回答产生哪类效果,谓词回答涉及哪些行。如果两种情况只在于行属于谁,那是谓词的事;如果效果本身不同,那就是另一个操作,可能还是另一个动词——所以“踢人”是 administer,而不是配上宽泛谓词的 write
所有者无论谓词怎么规定,总能看到自己
可见性不等于安全——频道优化和权限是不同的机制,谁也替代不了谁
已禁用的模块没有任何接口:模块是否启用是 build 的属性,因此代码生成不会为已禁用的模块输出任何内容,调用不可用的接口会在编译期报错,而不是在运行时被拒绝
模块接口跟随主体,而不是跟随哪一端(见 由谁决策):room-visitor build 能看到加入、离开和读取,room-owner build 还能看到踢人、关闭和配置,whoami 报告当前主体解锁了哪些接口

由谁分配角色。 给玩家授予和撤销角色,以及项目为新玩家声明的默认角色,都是 Auth & Players 上的操作——身份归这个模块管理,角色则根据凭据中的身份解析。

错误

  • 谓词隐藏的内容返回 not found,而不是 forbidden:否则拒绝本身就会告诉调用方这个东西存在。
  • 调用方不具备的权利返回 forbidden,前提是对象的存在本身不是秘密;错误会指明缺少什么,而不是笼统地失败。
  • 掩码之外的字段不会出现在响应中,而不是出现但为空:否则空值和被屏蔽的值就无法区分。
  • 角色直接或经由链条包含自己,属于配置错误——在声明阶段就会拒绝,而不是留到运行时解析。
  • 委托从不扩大能力:主体以自己名义无权发起的调用,代表玩家发起时同样会遭到拒绝。

限制

具体数值会随平台限制一章给出;触及每个上限时会发生什么,现在已经确定。

  • 行谓词下选择集的大小有上限,这个上限由 ACL 模型声明,而不是在运行中才发现。超过上限的读取,会返回上限数量的行,并附带表示结果已截断的标记,绝不会不声不响地只返回不完整的一页。
  • 已解析权限的过期上限是声明出来的,撤销不会等它到期——会立即失效。