API Console
test
Sign in with BES

接入 BOSS

初始化 Control 和测试台后,登记 BOSS 的证书、内部产品和登录客户端,开通平台租户与员工,填好部署配置,再接入登录、账户设置和事件接收。

环境登录地址带证书调用的接口地址
localhttps://id.local.globalbes.sghttps://id.local.globalbes.sg:19443
testhttps://id.test.globalbes.sghttps://id.test.globalbes.sg:9443
staging待定待定
prod待定待定

Sign in to send requests to see which of these steps are done. Sign in

1. 开始前
1.1

确认环境就绪

在哪里做:测试台

确认 Control 和测试台初始化完成,找到 active 的平台租户,为登记 BOSS 和开通员工准备环境。

GETTenant List
  • Control 按 bes-control-service/docs/runbooks/operations.md 第 1 节做完迁移、密钥、同步目录和首位平台管理员初始化(bes-control seed-person)。
  • 首位管理员初始化同时建好平台租户,code 为 BES_CONTROL_PLATFORM_TENANT_CODE,状态为 active。
  • 测试台按 bes-api-console/docs/setup.md 登记,并能用平台管理员登录。
  • 在 tenant.list 返回的 entries 中确认平台租户的 code 和 active 状态;有 next_cursor 时填入 cursor 继续查。

本环境(test)

  • Control 与测试台的部署配置位置:待定。
  • 平台租户代码取本环境的 BES_CONTROL_PLATFORM_TENANT_CODE。
其他环境

local

bes-infra 仓的 ./envs/local/dev stack up 与 ./envs/local/dev stack seed-demo 已做完初始化,平台租户代码为 bes。

staging

  • Control 与测试台的部署配置位置:待定。
  • 平台租户代码取本环境的 BES_CONTROL_PLATFORM_TENANT_CODE。

prod

  • Control 与测试台的部署配置位置:待定。
  • 平台租户代码取本环境的 BES_CONTROL_PLATFORM_TENANT_CODE。

做完这一部分,手里有:可用的 Control 和测试台、active 的平台租户;测试台可用平台管理员登录。

2. 登记 BOSS,拿到证书
2.1

生成私钥和证书请求

在哪里做:BOSS 服务器

跑一条 openssl 命令,得到私钥 boss-service-key.pem 和证书请求 boss-service.csr。证书请求拿去签发;私钥留在 BOSS 服务器上,不发给任何人。

  • SERVICE_ACCOUNT_ID 每次新生成,写进证书唯一的 URI SAN urn:bes:service:<uuid>;登记平台账号后它就是账号的 service_account_id。
  • 私钥 boss-service-key.pem 放进 BOSS 部署的密钥存储。

本环境(test)

在 BOSS 服务器上执行:

CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -subj "/CN=BOSS" \
  -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=clientAuth" \
  -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"
其他环境

local

bes-infra 仓的 ./envs/local/dev stack up 已做完这一步:私钥在 ~/.local/share/bes/local/stack/secrets/control/boss-service-key.pem,本地后面的步骤用它。要另做一套,在本机执行:

CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -subj "/CN=BOSS" \
  -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=clientAuth" \
  -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"

staging

在 BOSS 服务器上执行:

CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -subj "/CN=BOSS" \
  -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=clientAuth" \
  -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"

prod

在 BOSS 服务器上执行:

CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -subj "/CN=BOSS" \
  -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=clientAuth" \
  -keyout "$CERT_DIR/boss-service-key.pem" -out "$CERT_DIR/boss-service.csr"
2.2

签发证书

在哪里做:客户端 CA 所在的机器

用客户端 CA 给证书请求签名,得到证书 boss-service.pem。证书拷回 BOSS 服务器,和私钥一起调 Control;登记平台账号时也要用它。

前提:OpenSSL 3.0 及以上,openssl version 显示 OpenSSL 3.x。Mac 自带的 /usr/bin/openssl 是 LibreSSL,不认 -copy_extensions:先执行 brew install openssl@3,再执行 export PATH="$(brew --prefix openssl@3)/bin:$PATH"。

本环境(test)

  • CA_DIR:待定,test 环境的客户端 CA 位置未定。
  • 签发前把 boss-service.csr 拷到客户端 CA 所在机器的 ~/bes-certs/boss/;签发后把 boss-service.pem 拷回 BOSS 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
CA_DIR="待定"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/boss-service.pem"
其他环境

local

bes-infra 仓的 ./envs/local/dev stack up 已做完这一步:证书在 ~/.local/share/bes/local/stack/secrets/control/boss-service.pem,客户端 CA(client-ca.pem、client-ca-key.pem)在同一目录,本地后面的步骤用这张证书。给上一步另做的证书请求签发,在本机执行(证书请求不用拷):

CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
CA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/boss-service.pem"

staging

  • CA_DIR:待定,staging 环境的客户端 CA 位置未定。
  • 签发前把 boss-service.csr 拷到客户端 CA 所在机器的 ~/bes-certs/boss/;签发后把 boss-service.pem 拷回 BOSS 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
CA_DIR="待定"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/boss-service.pem"

prod

  • CA_DIR:待定,prod 环境的客户端 CA 位置未定。
  • 签发前把 boss-service.csr 拷到客户端 CA 所在机器的 ~/bes-certs/boss/;签发后把 boss-service.pem 拷回 BOSS 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/boss"
mkdir -p "$CERT_DIR"
CA_DIR="待定"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/boss-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/boss-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/boss-service.pem"
2.3

登记平台账号

在哪里做:Control 服务器

执行 bes-control seed-trusted-caller,得到 BOSS 的平台账号,状态 active。之后 BOSS 带这张证书调管理接口,Control 按这个账号放行。

  • 账号带 Control 的 platform-administrator 角色(role),在 service-account.list 里 product_id 为 null,能管所有产品(product)。
  • --display-name 写 BOSS:测试台按这个名字找 BOSS 的平台账号。
  • 重复执行保留同一个账号。

本环境(test)

  • 在 Control 服务器上执行 bes-control 的方式:待定,test 环境的 Control 部署方式未定。
  • boss-service.pem 拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --certificate "待定/boss-service.pem" --display-name BOSS --role platform-administrator
其他环境

local

bes-infra 仓的 ./envs/local/dev stack seed-demo 已做完这一步:执行的就是下面这条命令,登记的证书是 ~/.local/share/bes/local/stack/secrets/control/boss-service.pem。它还做完了:创建产品 boss、登记并启用 boss-web(不保存 client_secret,要用时调 client.rotate-credential 换一个)、创建并启用平台租户 bes 和开通记录、创建员工 yang.staff(Control 的 platform-administrator 角色)并发首次登录链接。

BES_INFRA="$HOME/Projects/bes-infra"   # bes-infra 仓的路径
"$BES_INFRA/envs/local/dev" stack compose exec -T control-api /bin/sh /opt/bes/stack-entrypoint.sh \
  bes-control seed-trusted-caller --certificate /run/bes/control/boss-service.pem --display-name BOSS --role platform-administrator
  • 本地 Control 在容器 control-api 里;~/.local/share/bes/local/stack/secrets/control/ 下的文件,每次执行时复制成容器里的 /run/bes/control/<文件名>。

staging

  • 在 Control 服务器上执行 bes-control 的方式:待定,staging 环境的 Control 部署方式未定。
  • boss-service.pem 拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --certificate "待定/boss-service.pem" --display-name BOSS --role platform-administrator

prod

  • 在 Control 服务器上执行 bes-control 的方式:待定,prod 环境的 Control 部署方式未定。
  • boss-service.pem 拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --certificate "待定/boss-service.pem" --display-name BOSS --role platform-administrator --confirm-production
2.4

用证书调第一个接口

在哪里做:BOSS 代码

用证书和私钥调 tenant.list,得到租户列表。证明证书、地址和平台账号都对上,之后的管理接口照这样调。

GETTenant List

程序里发这个请求(id 用前面步骤返回的):

带:证书

请求:

GET https://id.test.globalbes.sg:9443/internal/control/v1/tenants?limit=100

返回:

HTTP/1.1 200 OK

{
  "entries": [
    {
      "tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
      "code": "bes",
      "display_name": "BES",
      "status": "active",
      "created_at": "2026-01-01T08:00:00+00:00",
      "mfa_required": false
    }
  ],
  "next_cursor": null
}

程序发的请求和下面各环境的 curl 一样,可以先用它试。

  • 返回 200 和租户列表:证书、地址和平台账号都对上。
  • 地址:带证书的接口发到 https://id.test.globalbes.sg:9443,只带用户令牌的接口发到 https://id.test.globalbes.sg。
  • 证书:TLS 客户端证书 boss-service.pem、私钥 boss-service-key.pem,用服务端 CA 验证 Control 的证书。
  • 写操作:请求体带 request_ref,同一个 request_ref 重发返回第一次的结果。
  • X-BES-Operator:可选,写代表哪个人操作的 person_id。
  • 出错时返回 {"error": {"code": …, "reference": …, "message": …}},程序按 code 分支。

本环境(test)

  • SERVER_CA:待定,test 环境验证 Control 服务端证书的 CA 文件未定。

在 BOSS 服务器上执行:

CERT_DIR="$HOME/bes-certs/boss"
SERVER_CA="待定"
curl -i --cert "$CERT_DIR/boss-service.pem" --key "$CERT_DIR/boss-service-key.pem" \
  --cacert "$SERVER_CA" "https://id.test.globalbes.sg:9443/internal/control/v1/tenants?limit=100"
其他环境

local

用 ./envs/local/dev stack up 签发、./envs/local/dev stack seed-demo 登记的证书,在本机执行:

CA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control"
SERVER_CA="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/stack/server-ca.pem"
curl -i --cert "$CA_DIR/boss-service.pem" --key "$CA_DIR/boss-service-key.pem" \
  --cacert "$SERVER_CA" "https://id.local.globalbes.sg:19443/internal/control/v1/tenants?limit=100"

staging

  • SERVER_CA:待定,staging 环境验证 Control 服务端证书的 CA 文件未定。

在 BOSS 服务器上执行:

CERT_DIR="$HOME/bes-certs/boss"
SERVER_CA="待定"
curl -i --cert "$CERT_DIR/boss-service.pem" --key "$CERT_DIR/boss-service-key.pem" \
  --cacert "$SERVER_CA" "待定/internal/control/v1/tenants?limit=100"

prod

  • SERVER_CA:待定,prod 环境验证 Control 服务端证书的 CA 文件未定。

在 BOSS 服务器上执行:

CERT_DIR="$HOME/bes-certs/boss"
SERVER_CA="待定"
curl -i --cert "$CERT_DIR/boss-service.pem" --key "$CERT_DIR/boss-service-key.pem" \
  --cacert "$SERVER_CA" "待定/internal/control/v1/tenants?limit=100"

错误:

HTTP 错误码 处理
401 CALLER_AUTHENTICATION_FAILED 证书不是登记的那张,或账号没启用。
403 ACCESS_DENIED 账号的 Control 角色不含这个操作。

做完这一部分,手里有:BOSS 的证书、私钥和 active 平台账号;用证书调通 tenant.list。

3. 登记产品和登录客户端
3.1

创建产品 boss

在哪里做:测试台

创建产品 boss,得到它的 product_id。登记登录客户端、功能、角色和开通记录时都要填这个 product_id。

POSTProduct Create

在测试台点本步的 Product Create 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "request_ref": "create-boss",
  "code": "boss",
  "display_name": "BOSS",
  "scope_level": "tenant",
  "sellable": false,
  "status": "active"
}

返回:

{
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "code": "boss",
  "scope_level": "tenant",
  "sellable": false,
  "display_name": "BOSS",
  "status": "active",
  "version": 0
}
  • 产品 boss 按租户(tenant)开通,scope_level 写 tenant。
  • BOSS 是内部系统,不对客户售卖。

错误:

HTTP 错误码 处理
409 PRODUCT_CODE_CONFLICT boss 已登记:用 product.list 查出 product_id。
3.2

登记登录客户端

在哪里做:测试台

登记员工登录用的客户端 boss-web,得到 client_secret。BOSS 用 code 换令牌时带 client_id 和 client_secret。

POSTClient Register

在测试台点本步的 Client Register 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "request_ref": "register-boss-web",
  "client_id": "boss-web",
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "deployment_version": "1",
  "display_name": "BOSS",
  "kind": "confidential_web",
  "auth_method": "client_secret_basic",
  "redirect_uris": [
    "https://boss.example.com/callback"
  ],
  "post_logout_redirect_uris": [
    "https://boss.example.com/"
  ],
  "scopes": [
    "openid",
    "profile",
    "email"
  ],
  "token_profile": "web",
  "dpop_required": false
}

返回:

{
  "client_id": "boss-web",
  "display_name": "BOSS",
  "kind": "confidential_web",
  "auth_method": "client_secret_basic",
  "redirect_uris": [
    "https://boss.example.com/callback"
  ],
  "post_logout_redirect_uris": [
    "https://boss.example.com/"
  ],
  "backchannel_logout_uri": null,
  "scopes": [
    "openid",
    "profile",
    "email"
  ],
  "dpop_required": false,
  "token_profile": "web",
  "service_account_id": null,
  "status": "pending",
  "version": 0,
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "public_key_jwk": null,
  "deployment_version": "1",
  "client_secret": "<client_secret>"
}
  • product_id 写 boss 产品的:登录后可选的租户和权限按这个产品的开通记录(entitlement)和角色算。
  • redirect_uris 写 BOSS 实际的回调地址,逐字符一致。
  • client_secret 只在这次返回里出现,放进 BOSS 部署的密钥存储;丢了就调 client.rotate-credential 换一个。
  • 退出后回到 BOSS 的地址在这里登记,end-session 用它。

错误:

HTTP 错误码 处理
409 CLIENT_ID_CONFLICT boss-web 已登记:用 client.get 查看,跳过这一步。
3.3

启用登录客户端

在哪里做:测试台

把 boss-web 改成 active,得到可用的登录客户端。员工从此能通过它登录 BOSS。

POSTClient Set Status

在测试台点本步的 Client Set Status 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "client_id": "boss-web",
  "request_ref": "activate-boss-web",
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "status": "active"
}

返回:

{
  "client_id": "boss-web",
  "display_name": "BOSS",
  "kind": "confidential_web",
  "auth_method": "client_secret_basic",
  "redirect_uris": [
    "https://boss.example.com/callback"
  ],
  "post_logout_redirect_uris": [],
  "backchannel_logout_uri": null,
  "scopes": [
    "openid",
    "profile",
    "email"
  ],
  "dpop_required": false,
  "token_profile": "web",
  "service_account_id": null,
  "status": "active",
  "version": 1,
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "public_key_jwk": null,
  "deployment_version": "1"
}

做完这一部分,手里有:内部产品 boss 的 product_id、启用的 boss-web 登录客户端及 client_secret;登录回调与退出地址已登记。

4. 功能和角色
4.1

登记功能

在哪里做:测试台

把 BOSS 的全部路由登记成功能,得到功能清单。验证员工的请求时,Control 按它找到功能码。

PUTFeature Register

在测试台点本步的 Feature Register 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "deployment_version": "1",
  "request_ref": "register-boss-features-1",
  "features": [
    {
      "code": "document.read",
      "route_template": "/documents/{id}",
      "method": "GET",
      "risk": "low",
      "display_name": "Read documents"
    },
    {
      "code": "document.write",
      "route_template": "/documents",
      "method": "POST",
      "risk": "low",
      "display_name": "Write documents"
    },
    {
      "code": "payment.approve",
      "route_template": "/payments/{id}/approve",
      "method": "POST",
      "risk": "high",
      "display_name": "Approve payments"
    }
  ]
}

返回:

[
  {
    "feature_id": "3d8a1b6f-c247-42a5-99f4-16eaf5c38c7b",
    "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
    "code": "document.read",
    "route_template": "/documents/{id}",
    "method": "GET",
    "risk": "low",
    "display_name": "Read documents",
    "deployment_version": "1",
    "status": "active"
  },
  {
    "feature_id": "4e9b2c70-d358-43b6-8a05-27fb06d49d8c",
    "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
    "code": "document.write",
    "route_template": "/documents",
    "method": "POST",
    "risk": "low",
    "display_name": "Write documents",
    "deployment_version": "1",
    "status": "active"
  },
  {
    "feature_id": "5fac3d81-e469-44c7-9b16-380c17e5ae9d",
    "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
    "code": "payment.approve",
    "route_template": "/payments/{id}/approve",
    "method": "POST",
    "risk": "high",
    "display_name": "Approve payments",
    "deployment_version": "1",
    "status": "active"
  }
]
  • features 写 BOSS 自己的全部路由;BOSS 的功能清单:待定。
  • 每次调用替换这个 deployment_version 的整份清单,所以每次都发全部功能。
  • deployment_version 是 BOSS 验证请求时发的版本。
4.2

定义角色

在哪里做:测试台

把几个功能码组成一个角色,得到 role_id。创建员工时填它,员工就有这些功能。

PUTRole Define

在测试台点本步的 Role Define 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "code": "document-controller",
  "request_ref": "define-boss-document-controller",
  "display_name": "Document controller",
  "feature_codes": [
    "document.read",
    "document.write"
  ]
}

返回:

{
  "role_id": "6abd4e92-f57a-45d8-8c27-491d28f6bfae",
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "code": "document-controller",
  "display_name": "Document controller",
  "scope_level": "tenant",
  "feature_codes": [
    "document.read",
    "document.write"
  ],
  "status": "active",
  "tenant_id": null,
  "project_id": null,
  "version": 0
}
  • 角色按 code 创建或整体替换;feature_codes 只能写 BOSS 已登记且 active 的功能码。

错误:

HTTP 错误码 处理
400 ROLE_CONFIGURATION_INVALID feature_codes 有没登记的功能码,先登记功能。

做完这一部分,手里有:BOSS 的功能清单和角色的 role_id;创建员工时填这个 role_id。

5. 开通平台租户和员工
5.1

查平台租户

在哪里做:测试台

用 tenant.list 找到平台租户代码对应的那一行,得到 tenant_id,创建开通记录和员工时填它。

GETTenant List

在测试台点本步的 Tenant List 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "limit": 100
}

返回:

{
  "entries": [
    {
      "tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
      "code": "bes",
      "display_name": "BES",
      "status": "active",
      "created_at": "2026-01-01T08:00:00+00:00",
      "mfa_required": false
    }
  ],
  "next_cursor": null
}
  • 用本环境的 BES_CONTROL_PLATFORM_TENANT_CODE 在 entries 中找到平台租户,保存这一行的 tenant_id;本地代码为 bes。
  • 有 next_cursor 时填入 cursor 继续查,找到后确认状态为 active。
  • 创建开通记录和员工时填写这个 tenant_id。

错误:

HTTP 错误码 处理
401 CALLER_AUTHENTICATION_FAILED 核对测试台登记的证书和账号状态。
403 ACCESS_DENIED 测试台账号使用 Control 的 platform-administrator 角色。
5.2

创建开通记录

在哪里做:测试台

给平台租户开通 BOSS,得到待启用的 entitlement_id。下一步启用时填它。

POSTEntitlement Create

在测试台点本步的 Entitlement Create 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "request_ref": "create-bes-boss-entitlement",
  "tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
  "entitlement": {
    "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
    "starts_at": "2026-01-01T00:00:00Z",
    "ends_at": "2036-01-01T00:00:00Z",
    "source": "internal",
    "source_ref": "seed-demo-boss",
    "source_version": 1
  }
}

返回:

{
  "entitlement_id": "c3a7e1f9-5b26-4d48-9a0c-e8f2b4d6a135",
  "tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "project_id": null,
  "starts_at": "2026-01-01T00:00:00+00:00",
  "ends_at": "2036-01-01T00:00:00+00:00",
  "source": "internal",
  "source_ref": "seed-demo-boss",
  "source_version": 1,
  "seat_limit": null,
  "status": "pending",
  "readiness": "pending",
  "readiness_ref": null,
  "revoked_at": null,
  "version": 0
}
  • 时段:同一租户、同一产品的时段不能重叠;ends_at 必填。
  • source、source_ref、source_version 写这段时间出自哪份订单或合同。

错误:

HTTP 错误码 处理
409 ENTITLEMENT_PERIOD_OVERLAP 已有重叠的开通记录:用 entitlement.list 查出并启用它。
5.3

启用开通记录

在哪里做:测试台

启用开通记录,得到生效的开通。员工登录 BOSS 后能选平台租户。

POSTEntitlement Activate

在测试台点本步的 Entitlement Activate 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "entitlement_id": "c3a7e1f9-5b26-4d48-9a0c-e8f2b4d6a135",
  "request_ref": "activate-bes-boss-entitlement",
  "tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "runtime_ready": true
}

返回:

{
  "entitlement_id": "c3a7e1f9-5b26-4d48-9a0c-e8f2b4d6a135",
  "tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "project_id": null,
  "starts_at": "2026-01-01T00:00:00+00:00",
  "ends_at": "2036-01-01T00:00:00+00:00",
  "source": "internal",
  "source_ref": "seed-demo-boss",
  "source_version": 1,
  "seat_limit": null,
  "status": "active",
  "readiness": "ready",
  "readiness_ref": "4bf92f3577b34da6a3ce929d0e0e4736",
  "revoked_at": null,
  "version": 1
}
  • runtime_ready: true 表示产品的运行环境已就绪。

错误:

HTTP 错误码 处理
409 ENTITLEMENT_NOT_READY runtime_ready 写 true。
5.4

创建员工

在哪里做:测试台

在平台租户里创建员工,得到 person_id。发首次登录链接时填它。

POSTPerson Create

在测试台点本步的 Person Create 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "request_ref": "create-staff-weiling-tan",
  "tenant_id": "4f2a9c6e-1b83-4d57-8e0a-c6d3b9f1a572",
  "display_name": "Tan Wei Ling",
  "channels": [
    {
      "channel_type": "email",
      "channel_identifier": "weiling.tan@bes.example.com"
    }
  ],
  "role_id": "6abd4e92-f57a-45d8-8c27-491d28f6bfae",
  "person_type": "staff"
}

返回:

{
  "person_id": "2c7e9b41-5d08-4a3f-b6e2-91f4a8c3d057",
  "membership_id": "6e1a4c9d-3f57-4b28-8d06-a2c7e5b1f938",
  "person_role_id": "f2c6ea04-8d57-4b93-ae2f-d5b9c3a7f480",
  "channel_identity_ids": [
    "7d3b9f15-2a68-4c04-b1e7-e8a2d6c4f591"
  ]
}
  • person_type 写 staff,tenant_id 写平台租户的;员工只能建进平台租户,只有平台级服务账号能建。
  • role_id 写 BOSS 的角色。管平台的员工另外要 Control 的 platform-administrator 角色,在平台租户里用 role-assignment.assign 分配;这个角色对所有租户生效。

错误:

HTTP 错误码 处理
409 CHANNEL_IDENTITY_BOUND 邮箱已属于某人:用 membership.create 把这个人加进平台租户。
5.5

发首次登录链接

在哪里做:测试台

给员工发首次登录链接,得到 action_id。员工用链接设好密码后登录 BOSS;用 action_id 查链接状态。

POSTCredential Issue

在测试台点本步的 Credential Issue 标签,按下面的值填表(id 用前面步骤返回的),点 Send request。

填表的值:

{
  "request_ref": "first-login-weiling-tan",
  "person_id": "2c7e9b41-5d08-4a3f-b6e2-91f4a8c3d057",
  "purpose": "initial_login",
  "reason": "BOSS staff onboarding"
}

返回:

{
  "action_id": "4d9f2b6e-8a13-4c75-b2e0-f6a1c9d3e587",
  "purpose": "initial_login",
  "expires_at": "2026-01-03T08:00:00+00:00",
  "delivery": "queued"
}
  • 返回只说明链接已排队发送;用 credential-action.get 和返回的 action_id 查是 pending、completed 还是 expired。
  • 首次登录链接最长 48 小时有效。
  • 员工首次登录要在手机验证器 App 上登记两步验证。

错误:

HTTP 错误码 处理
409 CREDENTIAL_CONFLICT 此人已设过密码,不用再发。

做完这一部分,手里有:平台租户的 tenant_id 和启用的 BOSS 开通记录、员工的 person_id;员工收到首次登录链接并登记两步验证。

6. 交给 BOSS 的配置
6.1

写入 BOSS 配置

在哪里做:BOSS 服务器

把地址、证书、产品和登录客户端等值写入 BOSS 部署配置,得到调用 Control 和员工登录所需的配置。

配置值 来源与填写内容
control_issuer 本环境的登录地址,见下面环境配置
control_gateway 本环境带证书调用的接口地址,见下面环境配置
服务端 CA 文件 本环境验证 Control 服务端证书的 CA 文件,first-call 的 SERVER_CA
证书与私钥 sign-certificate 的 boss-service.pem、generate-key 的 boss-service-key.pem
product_id create-product 返回的 product_id
client_id 与 client_secret register-client 返回的 client_id(boss-web)和 client_secret
deployment_version register-features 登记的版本,示例为 1
平台租户 tenant_id find-platform-tenant 找到的 tenant_id
  • client_secret 与私钥放密钥存储,BOSS 部署配置引用它们。

本环境(test)

  • control_issuer:待定。
  • control_gateway:待定。
  • 服务端 CA 文件:待定。
  • 证书与私钥:~/bes-certs/boss/boss-service.pem、~/bes-certs/boss/boss-service-key.pem。
其他环境

local

  • control_issuer:https://id.local.globalbes.sg。
  • control_gateway:https://id.local.globalbes.sg:19443。
  • 服务端 CA 文件:~/.local/share/bes/local/stack/secrets/stack/server-ca.pem。
  • 证书与私钥:~/.local/share/bes/local/stack/secrets/control/boss-service.pem、~/.local/share/bes/local/stack/secrets/control/boss-service-key.pem。

staging

  • control_issuer:待定。
  • control_gateway:待定。
  • 服务端 CA 文件:待定。
  • 证书与私钥:~/bes-certs/boss/boss-service.pem、~/bes-certs/boss/boss-service-key.pem。

prod

  • control_issuer:待定。
  • control_gateway:待定。
  • 服务端 CA 文件:待定。
  • 证书与私钥:~/bes-certs/boss/boss-service.pem、~/bes-certs/boss/boss-service-key.pem。

做完这一部分,手里有:BOSS 部署配置已填入地址、证书、产品、登录客户端、功能版本和平台租户,秘密存进密钥存储。

7. 验证登录状态
7.1

把用户跳到登录页

在哪里做:BOSS 代码

员工没登录时,BOSS 把浏览器跳到 BES 登录页。登录完,浏览器带着 code 回到 BOSS,下一步用它换令牌。

GETAuthorize (sign-in page)
GET https://id.test.globalbes.sg/authorize?response_type=code&client_id=boss-web&redirect_uri=https%3A%2F%2Fboss.example.com%2Fcallback&scope=openid%20profile%20email&state=<state>&nonce=<nonce>&code_challenge=<code_challenge>&code_challenge_method=S256
  • 端点地址从 GET https://id.test.globalbes.sg/.well-known/openid-configuration 读。
  • state、nonce 每次随机生成,存在用户会话里。
  • code_challenge = BASE64URL(SHA256(code_verifier)),只支持 S256;以上参数缺一个就被拒。
  • 回调收到 ?code=…&state=…;code 60 秒内有效,只能用一次。
  • Java 示例见「接入新产品」的「把用户跳到登录页」。
7.2

用 code 换令牌

在哪里做:BOSS 代码

用 code 和 client_secret 换令牌,得到 access_token 和 refresh_token。存在服务端会话里,之后每个请求和刷新都要用。

POSTToken
POST https://id.test.globalbes.sg/oauth/token
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<code>&code_verifier=<code_verifier>&redirect_uri=https%3A%2F%2Fboss.example.com%2Fcallback
HTTP/1.1 200 OK

{
  "access_token": "<access_token>",
  "id_token": "<id_token>",
  "refresh_token": "<refresh_token>",
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "openid profile email"
}
  • access_token、refresh_token:存在服务端的用户会话里,浏览器只拿会话 cookie。
  • access_token 有效 5 分钟。
  • id_token 里的 sub 是这个人的 person_id,长期不变。
7.3

让用户选租户

在哪里做:BOSS 代码

带员工的 access_token 发起选租户,员工选好平台租户后,BOSS 得到 context_id。存进会话,每次验证请求时带上。

POSTSession Tenant Selection Start

程序里发这个请求(id 用前面步骤返回的):

带:证书 + 用户的 access_token

请求:

POST https://id.test.globalbes.sg:9443/api/sessions/tenant-selection
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "continue_url": "https://boss.example.com/callback"
}

返回:

HTTP/1.1 201 Created

{
  "selection_id": "9c4e1b7a-2d58-4f3c-b6a0-e81d5f2c7a94",
  "selection_url": "/tenants?selection=9c4e1b7a-2d58-4f3c-b6a0-e81d5f2c7a94",
  "expires_at": "2026-01-01T08:05:00+00:00"
}
  • continue_url 必须与 boss-web 登记的 redirect_uris 或 post_logout_redirect_uris 之一完全一致。
  • 把浏览器跳到 https://id.test.globalbes.sg + selection_url;员工选好后回到 continue_url?context_id=0f6b8d2e-4a17-4c93-9e5b-a1d27c4f3b68,把 context_id 存进服务端会话。
  • 员工只能选平台租户 bes。
  • 选择 5 分钟内有效;同一会话再发起一次会取消上一次。
  • Java 示例见「接入新产品」的「让用户选租户」。

错误:

HTTP 错误码 处理
401 LOGIN_REQUIRED BES 登录已结束,重新登录后再发起。
7.4

验证每个请求

在哪里做:BOSS 代码

之后员工每发一个请求,BOSS 带证书、access_token、context_id 问 Control,返回这个人的权限列表。BOSS 按它放行或拒绝。

POSTSession Validate

程序里发这个请求(id 用前面步骤返回的):

带:证书 + 用户的 access_token

请求:

POST https://id.test.globalbes.sg:9443/api/sessions/validate
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "product_id": "1d4b8e2f-6a35-4c71-9e08-b3f5a7c2d941",
  "context_id": "0f6b8d2e-4a17-4c93-9e5b-a1d27c4f3b68",
  "target": {
    "method": "GET",
    "path": "/documents/42",
    "deployment_version": "1"
  }
}

返回:

HTTP/1.1 200 OK

{
  "context": "<signed bes-context+jwt>",
  "checked_at": "2026-01-01T08:00:00.123456+00:00",
  "hard_expiry": "2026-01-01T08:00:30.123456+00:00",
  "action": "document.read",
  "risk": "low"
}
  • 请求必须带 product_id,写 boss 的;BOSS 是平台级账号,不带会被拒绝。
  • target.method、target.path:BOSS 收到的请求,不含查询串;deployment_version:登记功能清单时用的版本。
  • context 用 https://id.test.globalbes.sg/.well-known/jwks.json 验证(ES256,类型 bes-context+jwt),aud 是 boss 的 product_id;claim features 是此人的全部功能码。
  • Control 不可达、超时或返回 5xx 时拒绝请求。
  • Java 示例见「接入新产品」的「验证每个请求」。

错误:

HTTP 错误码 处理
401 INVALID_TOKEN 刷新令牌后重试;刷新失败就重新登录。
401 LOGIN_REQUIRED 重新登录。
403 ACCESS_DENIED 返回 403。
403 TARGET_UNKNOWN 把这条路由登记进这个版本的功能清单。
7.5

刷新令牌

在哪里做:BOSS 代码

access_token 过期前用 refresh_token 换一对新令牌。两个都存下,下次请求和刷新用新的。

POSTToken
POST https://id.test.globalbes.sg/oauth/token
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=<refresh_token>
  • 返回新的 access_token 和 refresh_token,两个都存下。
  • 每个 refresh_token 只能用一次;旧的再用一次,整个会话被撤销。同一会话的刷新要串行。
  • 网页会话空闲 2 小时或总共 12 小时后结束,刷新也不延长。
7.6

结束登录

在哪里做:BOSS 代码

员工退出时,BOSS 带 access_token 结束在 Control 里的登录,手里的令牌失效。接着结束浏览器里的登录。

POSTSession Logout

程序里发这个请求(id 用前面步骤返回的):

带:用户的 access_token

请求:

POST https://id.test.globalbes.sg/api/sessions/logout
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "login_scope": "product"
}

返回:

HTTP/1.1 204 No Content
  • login_scope: product(默认)登出 BOSS 所有登录客户端,client 只登出这一个。
  • 先调这个接口,再清掉自己的会话。
  • Java 示例见「接入新产品」的「结束登录」。
7.7

结束浏览器登录

在哪里做:BOSS 代码

把浏览器跳到 BES 的退出地址,结束 BES 在浏览器里的登录。员工下次进 BOSS 要重新登录。

GETEnd session
GET https://id.test.globalbes.sg/end_session?client_id=boss-web&state=<state>
  • BES 显示确认页。要回到 BOSS,先给 boss-web 登记 post_logout_redirect_uris,再在这里带上 post_logout_redirect_uri。
7.8

加「Account settings」入口

在哪里做:BOSS 代码

在 BOSS 用户菜单里加「Account settings」链接,得到打开 BES 账户页并管理密码、两步验证等设置的入口。

  • 在 BOSS 的用户菜单里加「Account settings」链接,在新标签页或约 560×760 的小窗口打开 https://id.test.globalbes.sg/account。
  • 密码、两步验证、恢复码、已登录的浏览器、手机和邮箱、语言都在那里管理,BOSS 使用这个入口管理账户设置。
  • 用户菜单只留:当前用户、切换公司(session.tenant-selection-start)、Account settings、登出(end_session)。

做完这一部分,手里有:员工登录后的令牌和 context_id、每个请求的权限列表、账户设置入口;退出时两边的登录都结束。

8. 接收事件
8.1

登记接收地址

在哪里做:Control 服务器

在 Control 部署配置里登记 BOSS 的接收地址、audience 和 12 种事件,得到 Control 投递事件的接收方配置。

在 Control 部署配置的 event_consumers(环境变量 BES_CONTROL_EVENT_CONSUMERS)JSON 对象里配置接收方 boss:

{
  "boss": {
    "endpoint": "https://boss.local.globalbes.sg/internal/events/control",
    "audience": "boss-api",
    "event_types": [
      "entitlement.changed",
      "membership.joined",
      "membership.revoked",
      "role_assignment.changed",
      "channel_access.revoked",
      "invitation.created",
      "tenant.policy_changed",
      "person.status_changed",
      "role.defined",
      "role.changed",
      "support_grant.issued",
      "support_grant.closed"
    ]
  }
}
  • audience 写 boss-api,event_types 与 bes-control-service/.env.example 中 boss 的 12 种事件一致,事件内容见契约仓 events/control/。

本环境(test)

  • 接收地址:待定。
  • Control 部署配置的位置和改完后怎样生效:待定。
其他环境

local

  • 接收地址:https://boss.local.globalbes.sg/internal/events/control。
  • 本地 Control 的配置在 bes-infra 仓 envs/local/compose.yaml,把 boss 写进 BES_CONTROL_EVENT_CONSUMERS 的同一个 JSON 对象;更新部署配置后重新启动栈使配置生效:
BES_INFRA="$HOME/Projects/bes-infra"
"$BES_INFRA/envs/local/dev" stack up
  • 本地 Control 信任栈的服务端 CA(~/.local/share/bes/local/stack/secrets/stack/server-ca.pem),接收地址要能从栈的容器里访问,证书由这张 CA 签发。

staging

  • 接收地址:待定。
  • Control 部署配置的位置和改完后怎样生效:待定。

prod

  • 接收地址:待定。
  • Control 部署配置的位置和改完后怎样生效:待定。
8.2

验证收到的事件

在哪里做:BOSS 代码

BOSS 收到事件后验证签名并按 event_id 存下再回 2xx,得到 BOSS 表里的事件供后续处理。

Control 每个事件发一个请求:

POST https://boss.local.globalbes.sg/internal/events/control
Content-Type: application/jwt
Idempotency-Key: 3e9a7c51-0b46-4d2f-9e83-a1c5f7d2b064

<event_jws>
  • 请求体是 ES256 签名的 JWT,类型 bes-event+jwt,用 https://id.test.globalbes.sg/.well-known/jwks.json 验证;aud 是登记的 boss-api,5 分钟后过期。
  • claim 是 common/event-envelope.json 的信封:event_id、event_type、aggregate_type、aggregate_id、tenant_id、product_id、sequence、occurred_at、trace_id、payload。
  • 先按 event_id(主键)存下再回 2xx,之后从自己的表里处理;重复投递按 event_id 去重。
  • 同一接收方的事件严格按顺序到达。没回 2xx 时 Control 从 30 秒起重试到 24 小时、共 10 次;之后这次投递保持失败,后面的事件都停住。

本环境(test)

  • 接收地址:待定。
其他环境

local

  • 接收地址:https://boss.local.globalbes.sg/internal/events/control。

staging

  • 接收地址:待定。

prod

  • 接收地址:待定。
Java(Spring Boot)
@PostMapping(path = "/internal/events/control", consumes = "application/jwt")
ResponseEntity<Void> receive(@RequestBody String jws) throws Exception {
    JWTClaimsSet event = events.process(jws, null);      // same setup as the context processor,
                                                         // type "bes-event+jwt", audience "boss-api"
    try {
        jdbc.update("insert into bes_event_inbox(event_id, event_type, body) values (?, ?, ?::jsonb)",
            event.getStringClaim("event_id"), event.getStringClaim("event_type"), event.toString());
    } catch (DuplicateKeyException alreadyStored) {
        // delivered before: acknowledge again
    }
    return ResponseEntity.noContent().build();
}

做完这一部分,手里有:接收方 boss 已登记 12 种事件;BOSS 验证签名并按 event_id 去重存储后处理。

其他功能
功能:开通客户功能:管理团队成员功能:支持访问