RustFS AWS IAM 策略变量(Policy Variables)端到端测试全解析
【免费下载链接】rustfs🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting migration and coexistence with other S3-compatible platforms such as MinIO and Ceph.项目地址: https://gitcode.com/GitHub_Trending/rus/rustfs
RustFS 是开源的、兼容 S3 的对象存储系统,其访问控制(IAM)能力对标 AWS IAM 策略体系。本文以 crates/e2e_test/src/policy/README.md 为骨架,结合端到端测试源码与策略引擎实现,系统讲解 RustFS 中 AWS IAM 策略变量的解析与验证:包括单值、多值、拼接、嵌套、Deny 与 STS 临时凭证六类场景的测试设计与运行方式,并深入crates/policy的变量解析器源码,帮助读者理解${\aws:username`}` 这类占位符在 RustFS 中的真实解析流程,以及如何在日常部署中编写可用的变量化策略。
一、为什么需要策略变量
在对象存储的多租户场景中,管理员通常希望为每个用户编写"个人专属"的授权策略,例如"只允许用户操作以自己用户名命名的桶"。如果策略是静态的,那么每新增一个用户就要复制一份策略,维护成本随用户数线性增长。
AWS IAM 的解决方案是策略变量(policy variables):在策略的 Resource 或 Condition 字段中写入形如${\aws:username`}` 的占位符,由服务在鉴权时根据请求上下文动态替换为真实值。RustFS 完整继承了这一机制,并通过一套独立的端到端测试套件(位于 crates/e2e_test/src/policy)来保证其与 AWS 行为的一致性。
二、测试总览:六类核心场景
根据 crates/e2e_test/src/policy/README.md,测试套件覆盖以下 AWS 策略变量场景:
| 序号 | 场景 | 说明 | 对应测试函数 |
|---|---|---|---|
| 1 | 单值变量(Single-value) | 基础变量解析,如${\aws:username`}|test_aws_policy_variables_single_value` | |
| 2 | 多值变量(Multi-value) | 一个变量可展开为多个值 | test_aws_policy_variables_multi_value |
| 3 | 变量拼接(Concatenation) | 变量与静态文本组合,如prefix-${aws:username}-suffix | test_aws_policy_variables_concatenation |
| 4 | 嵌套变量(Nested) | 复杂嵌套模式,如${\${`aws:username`}-test`}|test_aws_policy_variables_nested` | |
| 5 | Deny 场景 | 含变量的 Deny 策略优先级验证 | test_aws_policy_variables_deny |
| 6 | STS 临时凭证 | 变量解析被 AssumeRole 产生的临时凭证继承 | test_aws_policy_variables_sts |
每个测试函数都使用#[tokio::test(flavor = "multi_thread")]异步运行,并调用RustFSTestEnvironment::new()启动一个隔离的 RustFS 服务器实例,测试结束后通过清理函数移除用户、策略与测试桶,互不干扰。
三、前置条件与运行方式
3.1 前置条件
awscurl工具:用于以 SigV4 签名的方式驱动 RustFS 管理 API(添加用户、创建/附加策略等),测试辅助函数统一固定使用AdminTransport::Awscurl传输方式;- AWS SDK for Rust:项目自带,测试通过
aws_sdk_s3::Client执行真实的 S3 操作(建桶、List、Put、Get),从而验证策略变量在真实请求链路上的生效情况。
3.2 运行命令
从仓库根目录执行:
cargo test -p e2e_test policy:: -- --nocapture该命令会:
- 编译
e2e_testcrate 并以policy::为前缀过滤出全部策略变量测试; --nocapture使tracing输出的日志(如Starting AWS policy variables single-value test)直接显示在终端;- 每个测试在动态分配的本地端口上启动独立 RustFS 服务器,测试结束即清理,端口分配器实现在 crates/e2e_test/src/common.rs(默认端口区间 20000 起,可通过
RUSTFS_E2E_TEST_PORT_MIN/RUSTFS_E2E_TEST_PORT_RANGE环境变量调整)。
四、测试基础设施:环境、辅助函数与资源隔离
完整的测试实现位于 crates/e2e_test/src/policy/policy_variables_test.rs,其代码结构清晰展示了端到端测试的标准范式。
4.1 测试环境与客户端构建
每个测试遵循统一生命周期:
let mut env = RustFSTestEnvironment::new().await?; env.start_rustfs_server(vec![]).await?; // ... 执行断言 ...随后通过env.create_s3_client_with_credentials(test_user, test_password)为被测用户创建独立 S3 客户端,确保所有请求都携带该用户的访问密钥,而不是管理员密钥。
4.2 管理 API 调用链
测试通过awscurl完成策略的"创建-附加-清理"三步操作:
- 创建用户:
PUT {env.url}/rustfs/admin/v3/add-user?accessKey={username},请求体为{"secretKey": "...", "status": "enabled"}; - 创建策略:
PUT /rustfs/admin/v3/add-canned-policy?name={policy_name},请求体为完整策略 JSON; - 附加策略:
PUT /rustfs/admin/v3/set-user-or-group-policy?policyName={name}&userOrGroup={user}&isGroup=false; - 清理:
DELETE /rustfs/admin/v3/remove-user与DELETE /rustfs/admin/v3/remove-canned-policy。
这些辅助函数封装在 crates/e2e_test/src/common.rs 的admin_create_user_via、admin_add_canned_policy_via、admin_attach_user_policy_via中,测试代码只关心业务逻辑,不关心签名细节。
4.3 资源清理模式
cleanup_user_and_policy会遍历所有可能被测试创建出的桶名模式({username}-test-bucket、prefix-{username}-suffix等),依次删除对象与桶,再移除用户和策略,保证多次运行互不残留。
五、六类场景的测试设计与断言细节
5.1 单值变量:${\aws:username`}`
以用户testuser1为例,策略将桶资源限制为arn:aws:s3:::{username}-*:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListAllMyBuckets"], "Resource": ["arn:aws:s3:::*"] }, { "Effect": "Allow", "Action": ["s3:CreateBucket"], "Resource": ["arn:aws:s3:::${aws:username}-*"] }, { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::${aws:username}-*"] }, { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject"], "Resource": ["arn:aws:s3:::${aws:username}-*/*"] } ] }测试按顺序断言:
- 用户可以
ListBuckets(通配策略允许); - 可以创建
testuser1-test-bucket(命中变量展开后的testuser1-*); - 可以在该桶内 List、Put、Get 对象;
- 负向断言:创建
other-user-bucket必须返回AccessDenied,且校验服务端错误码:
assert_eq!(denied.as_service_error().and_then(ProvideErrorMetadata::code), Some("AccessDenied"));正负双向断言是这套测试的核心手法——既验证"该放行的放行",也验证"该拒绝的拒绝"。
5.2 多值变量:一个策略、多个展开值
多值场景(用户testuser2)在同一个 Statement 的 Resource 数组中列出三个展开结果:
{ "Effect": "Allow", "Action": ["s3:CreateBucket", "s3:ListBucket"], "Resource": [ "arn:aws:s3:::${aws:username}-bucket1", "arn:aws:s3:::${aws:username}-bucket2", "arn:aws:s3:::${aws:username}-bucket3" ] }测试依次验证三个桶均可创建、均可 List,而testuser2-other-bucket(不在任何允许模式内)必须被拒绝。这印证了变量解析在 Resource 数组层面是"逐元素展开"的。
5.3 变量拼接:prefix-${aws:username}-suffix
拼接场景(用户testuser3)验证变量可以与静态文本自由组合:
arn:aws:s3:::prefix-${aws:username}-suffix解析后等价于资源prefix-testuser3-suffix。测试创建同名桶并验证 List 权限,确认解析器并非简单地"整段替换",而是保留了前缀与后缀静态文本。
5.4 嵌套变量:${\${`aws:username`}-test`}`
嵌套场景(用户testuser4)是最复杂的解析用例,策略中写有:
arn:aws:s3:::${${aws:username}-test}其语义是:先把内层${\aws:username`}解析为testuser4,得到新变量名testuser4-test,再把整个占位符解析为桶名testuser4-test。测试验证该桶可创建,而other-user-test` 必须被拒绝。
从源码结构看,这一能力由 crates/policy/src/policy/variables.rs 中的花括号配对扫描算法支持(下文第六节详述)。
5.5 Deny 场景:显式拒绝优先于变量化的 Allow
Deny 测试(用户testuser5)同时包含 Allow 与 Deny 两条 Statement:
{ "Effect": "Allow", "Action": ["s3:CreateBucket"], "Resource": ["arn:aws:s3:::${aws:username}-*"] }, { "Effect": "Deny", "Action": ["s3:CreateBucket"], "Resource": ["arn:aws:s3:::*private*"] }预期行为:
testuser5-test-bucket创建成功(命中 Allow 且不触碰 Deny);testuser5-private-bucket创建失败(名字含private,被显式 Deny 拦截,返回AccessDenied)。
该用例验证了 AWS IAM 的核心原则——显式 Deny 优先于任何 Allow——在 RustFS 中得到遵守。
5.6 STS 临时凭证:变量随会话继承
STS 场景(用户testuser-sts)最贴近真实生产链路:
- 为用户附加含
${\aws:username`}的策略,并授权sts:AssumeRole`; - 通过
build_test_sts_client(...).assume_role()换取临时凭证(AccessKeyId + SecretAccessKey + SessionToken),角色 ARN 为arn:aws:iam::123456789012:role/policy-variable; - 使用临时凭证构造新的 S3 客户端;
- 验证临时凭证可以创建
testuser-sts-sts-bucket并完成 Put/Get/List 对象操作。
这证明变量解析在临时凭证的会话上下文中依然保留原始用户名,会话的"借用身份"不会覆盖aws:username的值。对应实现中,VariableResolver::resolve_principal_type会依据 claims 中是否包含roleArn来区分AssumedRole与User,而用户名本身仍取自初始用户上下文。
六、源码级原理:RustFS 策略变量解析器
测试验证的是"行为正确",而行为背后是 crates/policy/src/policy/variables.rs 的解析引擎。
6.1 变量上下文VariableContext
解析所需的请求上下文被封装为VariableContext:
pub struct VariableContext { pub is_https: bool, pub source_ip: Option<String>, pub account_id: Option<String>, pub region: Option<String>, pub username: Option<String>, pub claims: Option<HashMap<String, Value>>, pub conditions: HashMap<String, Vec<String>>, pub custom_variables: HashMap<String, String>, }其中claims来自 STS/OIDC 等身份源的 JWT claims,custom_variables用于支持custom:*前缀的自定义变量,is_https供aws:SecureTransport使用。
6.2 解析器 Trait 与内置变量
核心抽象是PolicyVariableResolvertrait:
pub trait PolicyVariableResolver: Sync { async fn resolve(&self, variable_name: &str) -> Option<String>; async fn resolve_multiple(&self, variable_name: &str) -> Option<Vec<String>>; fn is_dynamic(&self, variable_name: &str) -> bool; }resolve_multiple是"多值"能力的来源——例如aws:userid可以从sub或parentclaim 中取出多个值。默认实现VariableResolver支持以下变量(与key_name.rs中#[strum(serialize = "aws:username")]定义的键名一一对应):
| 变量名 | 取值来源 |
|---|---|
aws:username | 请求上下文的username |
aws:userid | JWT claims 的sub/parent |
aws:PrincipalType | 依据 claims 判定为AssumedRole/ServiceAccount/User |
aws:SecureTransport | 依据is_https返回true/false |
aws:CurrentTime | 当前 UTC 时间的 RFC3339 字符串 |
aws:EpochTime | 当前 Unix 时间戳 |
aws:AccountId/aws:Region/aws:SourceIp | 对应上下文字段 |
custom:* | 上下文中的自定义变量表 |
6.3 缓存解析器:静态缓存、动态直通
CachedAwsVariableResolver用 Moka 缓存(容量 100 条、TTL 5 分钟)加速静态变量的解析,但对时间类动态变量直通:
impl PolicyVariableResolver for CachedAwsVariableResolver { async fn resolve(&self, variable_name: &str) -> Option<String> { if self.is_dynamic(variable_name) { return self.inner.resolve(variable_name).await; // 动态:不缓存 } // 静态:查缓存,未命中则解析后写入 } }is_dynamic只对aws:CurrentTime与aws:EpochTime返回true,确保时间变量每次请求都重新计算——这一点由单元测试test_cached_aws_variable_resolver_dynamic_variables验证(间隔 1 秒的两次解析结果必须不同)。
6.4 迭代解析算法:从占位符到资源集合
resolve_aws_variables(pattern, resolver)是整个解析的入口,采用迭代展开策略:
- 初始结果集只含原始模式字符串;
- 每一轮对结果集中每个字符串调用
resolve_single_pass,扫描${\...`}` 占位符; - 使用花括号计数定位匹配的右花括号(支持
${\${`a`}-b`}` 这类嵌套); - 若变量名内还包含
${\...`}`,先递归解析内层,再用结果拼接出新的外层变量名继续解析(这正是嵌套变量的实现原理); - 若变量解析出多个值(
resolve_multiple返回多个),则每个值生成一条新结果——这是多值展开的机制; - 变量不存在时跳过,若解析为空则移除占位符;
- 去重后进入下一轮,直到结果集不再变化,最多迭代 10 次防止死循环。
一个值得注意的细节是:算法用while changed控制外层迭代,使拼接(prefix-${aws:username}-suffix)在一次扫描内完成,而嵌套(${${aws:username}-test})需要多轮递归配合,这解释了为什么嵌套是测试中最"重"的场景。
6.5 单元测试佐证
variables.rs 内置了针对解析器的单元测试,例如:
test_resolve_aws_variables_with_username:${\aws:username`}-bucket→testuser-bucket`;test_resolve_aws_variables_with_userid:从subclaim 解析aws:userid;test_resolve_aws_variables_with_multiple_variables:${aws:username}-${aws:userid}-bucket同时解析两个变量;test_resolve_aws_variables_with_unicode_prefix:中文前缀中文${aws:username}正常解析为中文alice。
这些单测与端到端测试形成"底层单元 + 顶层行为"的双层验证体系。
七、把策略变量用到生产配置
端到端测试中的策略 JSON 完全可以直接迁移到真实部署(通过管理 API 或配置文件)。一个推荐的"用户自管命名空间"策略模板如下:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListAllMyBuckets"], "Resource": ["arn:aws:s3:::*"] }, { "Effect": "Allow", "Action": ["s3:CreateBucket", "s3:ListBucket"], "Resource": ["arn:aws:s3:::${aws:username}-*"] }, { "Effect": "Allow", "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"], "Resource": ["arn:aws:s3:::${aws:username}-*/*"] } ] }配套的运维建议(均来自本仓库已验证的行为):
- 命名规范先行:变量化策略高度依赖"桶名包含用户名"的约定,团队内部应统一
{username}-*前缀规范; - 用 Deny 做红线:如测试 5.5 所示,可在变量化 Allow 之上叠加 Deny 语句封锁敏感命名(如
*private*),且 Deny 优先级高于 Allow; - 临时凭证场景放心用:5.6 已验证 AssumeRole 临时凭证继承变量解析结果,可安全用于 CI/CD 或跨账号访问;
- 注意时间类变量的动态性:
aws:CurrentTime/aws:EpochTime不缓存,适合在 Condition 中做访问时效控制。
八、总结
RustFS 的 IAM 策略变量不是"纸面兼容",而是通过一套完整的三层验证被固化的能力:
- 文档层:crates/e2e_test/src/policy/README.md 定义了六类场景的验收清单;
- 端到端层:policy_variables_test.rs 用真实 S3 客户端 +
awscurl管理 API 验证正负双向行为,涵盖普通用户与 STS 临时凭证; - 实现层:crates/policy/src/policy/variables.rs 以可缓存、可递归、支持多值展开的解析器兑现 AWS 语义,并有配套单元测试兜底。
对开发者而言,这意味着:在 RustFS 上做多租户授权时,可以放心使用${\aws:username`}` 等变量写出"一份策略、全量用户复用"的 IAM 体系,其行为一致性已被端到端测试持续守护。
【免费下载链接】rustfs🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting migration and coexistence with other S3-compatible platforms such as MinIO and Ceph.项目地址: https://gitcode.com/GitHub_Trending/rus/rustfs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考