diff --git a/Cargo.lock b/Cargo.lock index 5fad0ea1..cf28748a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2393,6 +2393,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "iam" +version = "0.1.0" +dependencies = [ + "actix-web", + "anyhow", + "async-trait", + "base64 0.21.7", + "chrono", + "clap", + "config", + "env_logger 0.10.2", + "jsonwebtoken", + "log", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "uuid", +] + [[package]] name = "iana-time-zone" version = "0.1.61" diff --git a/Cargo.toml b/Cargo.toml index b771e292..e4e84d7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "deps/verifier", "deps/eventlog", "deps/kms", + "iam", ] resolver = "2" @@ -25,7 +26,7 @@ async-trait = "0.1.31" base64 = "0.21" byteorder = "1.5.0" cfg-if = "1.0.0" -chrono = "0.4.19" +chrono = { version = "0.4.19", features = ["serde"] } clap = { version = "4", features = ["derive"] } config = "0.13.3" ear = "0.3.0" @@ -61,4 +62,4 @@ tokio = { version = "1", features = ["full"] } toml = "0.8.23" tempfile = "3.4.0" tonic = "0.12" -tonic-build = "0.12" \ No newline at end of file +tonic-build = "0.12" diff --git a/Dockerfile.iam b/Dockerfile.iam new file mode 100644 index 00000000..b90d7a95 --- /dev/null +++ b/Dockerfile.iam @@ -0,0 +1,55 @@ +ARG BASE_IMAGE=alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/alinux3:latest + +FROM ${BASE_IMAGE} AS builder + +ARG ALL_PROXY +ARG NO_PROXY +ENV ALL_PROXY=$ALL_PROXY +ENV NO_PROXY=$NO_PROXY + +ARG CARGO_JOBS + +RUN yum install -y perl wget curl clang openssh-clients openssl-devel protobuf-devel git + +WORKDIR /usr/src/trustee +COPY . . + +# Install Rust toolchain from mirrored source for faster builds +RUN export RUSTUP_DIST_SERVER='https://mirrors.ustc.edu.cn/rust-static' && \ + export RUSTUP_UPDATE_ROOT='https://mirrors.ustc.edu.cn/rust-static/rustup' && \ + curl --proto '=https' --tlsv1.2 -sSf https://mirrors.aliyun.com/repo/rust/rustup-init.sh | \ + sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" +RUN export RUSTUP_DIST_SERVER='https://mirrors.ustc.edu.cn/rust-static' && \ + export RUSTUP_UPDATE_ROOT='https://mirrors.ustc.edu.cn/rust-static/rustup' && \ + rustup toolchain install 1.79.0-x86_64-unknown-linux-gnu + +RUN printf '\ + [source.crates-io]\n\ + replace-with = "aliyun"\n\ + [source.aliyun]\n\ + registry = "sparse+https://mirrors.aliyun.com/crates.io-index/"\n\ + ' >> /root/.cargo/config + +RUN if [ -n "$CARGO_JOBS" ]; then \ + [ ! -d /root/.cargo ] && mkdir /root/.cargo; \ + echo -e "[build]\njobs = $CARGO_JOBS" >> /root/.cargo/config.toml; \ + fi + +RUN cargo build --locked --release -p iam + +FROM ${BASE_IMAGE} + +RUN yum install -y ca-certificates tzdata && \ + mkdir -p /app/config + +COPY --from=builder /usr/src/trustee/target/release/iam /usr/local/bin/iam +COPY iam/config/iam.toml /app/config/iam.toml + +WORKDIR /app +EXPOSE 8090 + +ENV RUST_LOG=info + +CMD ["/usr/local/bin/iam", "--config", "/app/config/iam.toml"] + diff --git a/deploy/configs/trustee-gateway.yml b/deploy/configs/trustee-gateway.yml index d3d7882c..0070731f 100644 --- a/deploy/configs/trustee-gateway.yml +++ b/deploy/configs/trustee-gateway.yml @@ -11,6 +11,11 @@ kbs: insecure_http: true ca_cert_file: "" +iam: + url: "http://iam:8090" + insecure_http: true + ca_cert_file: "" + attestation_service: url: "http://as-restful:50005" insecure_http: true diff --git a/dist/Dockerfile b/dist/Dockerfile index cf0495ef..b118910a 100644 --- a/dist/Dockerfile +++ b/dist/Dockerfile @@ -14,5 +14,6 @@ VOLUME /etc/trustee EXPOSE 8081 EXPOSE 8082 +EXPOSE 8090 CMD ["/usr/bin/start.sh"] \ No newline at end of file diff --git a/dist/Makefile b/dist/Makefile index 3b977d84..85d070a0 100644 --- a/dist/Makefile +++ b/dist/Makefile @@ -41,6 +41,7 @@ build: cargo build --bin grpc-as --release --features grpc-bin --locked cargo build --bin rvps --release cargo build --bin rvps-tool --release + cargo build -p iam --release --locked @echo "编译 trustee-gateway..." @echo "编译完成" @@ -68,6 +69,7 @@ install: install -m 644 system/as-restful.service $(BUILDROOT)$(PREFIX)/lib/systemd/system/as-restful.service install -m 644 system/trustee.service $(BUILDROOT)$(PREFIX)/lib/systemd/system/trustee.service install -m 644 system/trustee-gateway.service $(BUILDROOT)$(PREFIX)/lib/systemd/system/trustee-gateway.service + install -m 644 system/iam.service $(BUILDROOT)$(PREFIX)/lib/systemd/system/iam.service # 创建配置目录并安装配置文件 install -d -p $(BUILDROOT)$(CONFIG_DIR) @@ -75,6 +77,7 @@ install: install -m 644 configs/as-config.json $(BUILDROOT)$(CONFIG_DIR)/as-config.json install -m 644 configs/rvps.json $(BUILDROOT)$(CONFIG_DIR)/rvps.json install -m 644 configs/gateway.yml $(BUILDROOT)$(CONFIG_DIR)/gateway.yml + install -m 644 configs/iam.toml $(BUILDROOT)$(CONFIG_DIR)/iam.toml # 创建 bin 目录并安装可执行文件 install -d -p $(BUILDROOT)$(PREFIX)/bin @@ -84,6 +87,7 @@ install: install -m 755 ../target/release/rvps $(BUILDROOT)$(PREFIX)/bin/rvps install -m 755 ../target/release/rvps-tool $(BUILDROOT)$(PREFIX)/bin/rvps-tool install -m 755 ../trustee-gateway/gateway $(BUILDROOT)$(PREFIX)/bin/trustee-gateway + install -m 755 ../target/release/iam $(BUILDROOT)$(PREFIX)/bin/iam install -d -p $(BUILDROOT)$(PREFIX)/include install -d -p $(BUILDROOT)$(PREFIX)/lib64 diff --git a/dist/configs/gateway.yml b/dist/configs/gateway.yml index cd61e082..29a6cafb 100644 --- a/dist/configs/gateway.yml +++ b/dist/configs/gateway.yml @@ -5,6 +5,9 @@ server: kbs: url: "http://127.0.0.1:8080" +iam: + url: "http://127.0.0.1:8090" + rvps: grpc_addr: "127.0.0.1:50003" diff --git a/dist/configs/iam.toml b/dist/configs/iam.toml new file mode 100644 index 00000000..9ee3335f --- /dev/null +++ b/dist/configs/iam.toml @@ -0,0 +1,8 @@ +[server] +bind_address = "0.0.0.0:8090" + +[crypto] +issuer = "trustee-iam" +hmac_secret = "change-me" +default_ttl_seconds = 900 + diff --git a/dist/install.sh b/dist/install.sh index dd16a638..be6a9e08 100755 --- a/dist/install.sh +++ b/dist/install.sh @@ -48,10 +48,12 @@ install -m 644 system/as.service ${BUILDROOT}${PREFIX}/lib/systemd/system/as.ser install -m 644 system/rvps.service ${BUILDROOT}${PREFIX}/lib/systemd/system/rvps.service install -m 644 system/as-restful.service ${BUILDROOT}${PREFIX}/lib/systemd/system/as-restful.service install -m 644 system/trustee.service ${BUILDROOT}${PREFIX}/lib/systemd/system/trustee.service +install -m 644 system/iam.service ${BUILDROOT}${PREFIX}/lib/systemd/system/iam.service install -d -p ${BUILDROOT}/etc/trustee install -m 644 configs/kbs-config.toml ${BUILDROOT}${CONFIG_DIR}/kbs-config.toml install -m 644 configs/as-config.json ${BUILDROOT}${CONFIG_DIR}/as-config.json install -m 644 configs/rvps.json ${BUILDROOT}${CONFIG_DIR}/rvps.json +install -m 644 configs/iam.toml ${BUILDROOT}${CONFIG_DIR}/iam.toml install -d -p ${BUILDROOT}${PREFIX}/bin install -m 755 ../target/release/kbs ${BUILDROOT}${PREFIX}/bin/kbs install -m 755 ../target/release/restful-as ${BUILDROOT}${PREFIX}/bin/restful-as @@ -59,6 +61,7 @@ install -m 755 ../target/release/grpc-as ${BUILDROOT}${PREFIX}/bin/grpc-as install -m 755 ../target/release/rvps ${BUILDROOT}${PREFIX}/bin/rvps install -m 755 ../target/release/kbs-client ${BUILDROOT}${PREFIX}/bin/kbs-client install -m 755 ../target/release/rvps-tool ${BUILDROOT}${PREFIX}/bin/rvps-tool +install -m 755 ../target/release/iam ${BUILDROOT}${PREFIX}/bin/iam install -d -p ${BUILDROOT}${PREFIX}/include install -d -p ${BUILDROOT}${PREFIX}/lib64 cp intel-deps/include/sgx_* ${BUILDROOT}${PREFIX}/include/ diff --git a/dist/start.sh b/dist/start.sh index 047ffa1a..c75776b6 100755 --- a/dist/start.sh +++ b/dist/start.sh @@ -30,7 +30,7 @@ EOF } # Setup log rotation for each service -for service in rvps as as-restful kbs trustee-gateway trustee-frontend nginx; do +for service in rvps as as-restful kbs iam trustee-gateway trustee-frontend nginx; do setup_log_rotation $service done @@ -66,6 +66,14 @@ start_kbs() { echo "KBS service started, PID: $(cat /opt/trustee/logs/kbs.pid)" } +# Start IAM service +start_iam() { + echo "Starting IAM service..." + nohup /usr/bin/iam --config /etc/trustee/iam.toml > >(tee -a /opt/trustee/logs/iam.log) 2>&1 & + echo $! > /opt/trustee/logs/iam.pid + echo "IAM service started, PID: $(cat /opt/trustee/logs/iam.pid)" +} + # Start Trustee-Gateway service start_trustee_gateway() { echo "Starting Trustee-Gateway service..." @@ -91,6 +99,8 @@ start_as_restful sleep 2 start_kbs sleep 1 +start_iam +sleep 1 start_trustee_gateway sleep 1 start_trustee_frontend @@ -100,7 +110,7 @@ echo "All services started. Log files are located in /opt/trustee/logs/ director # Check service status check_services() { echo "Checking service status..." - for service in rvps as as-restful kbs trustee-gateway nginx-trustee-frontend; do + for service in rvps as as-restful kbs iam trustee-gateway nginx-trustee-frontend; do if [ -f "/opt/trustee/logs/${service}.pid" ]; then pid=$(cat /opt/trustee/logs/${service}.pid) if ps -p $pid > /dev/null; then diff --git a/dist/system/iam.service b/dist/system/iam.service new file mode 100644 index 00000000..3ae790a5 --- /dev/null +++ b/dist/system/iam.service @@ -0,0 +1,16 @@ +[Unit] +Description=Trustee IAM Service +After=network.target + +[Service] +ExecStart=/usr/bin/iam --config /etc/trustee/iam.toml +Environment=RUST_LOG=info +Restart=always +RestartSec=5 +LimitNOFILE=1048576 +LimitNPROC=infinity +LimitCORE=infinity + +[Install] +WantedBy=multi-user.target + diff --git a/dist/system/trustee.service b/dist/system/trustee.service index d70416b9..34c659b7 100644 --- a/dist/system/trustee.service +++ b/dist/system/trustee.service @@ -1,14 +1,14 @@ [Unit] Description=Trustee After=network.target -Wants=kbs.service as.service as-restful.service rvps.service trustee-gateway.service +Wants=kbs.service iam.service as.service as-restful.service rvps.service trustee-gateway.service [Service] Type=oneshot RemainAfterExit=yes -ExecStart=/usr/bin/systemctl start kbs as as-restful rvps trustee-gateway -ExecStop=/usr/bin/systemctl stop kbs as as-restful rvps trustee-gateway -ExecReload=/usr/bin/systemctl restart kbs as as-restful rvps trustee-gateway +ExecStart=/usr/bin/systemctl start kbs iam as as-restful rvps trustee-gateway +ExecStop=/usr/bin/systemctl stop kbs iam as as-restful rvps trustee-gateway +ExecReload=/usr/bin/systemctl restart kbs iam as as-restful rvps trustee-gateway [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index dd470909..e0999074 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,18 @@ version: '3.2' services: + iam: + build: + context: . + network: host + dockerfile: Dockerfile.iam + restart: always + ports: + - "8090:8090" + volumes: + - ./iam/config/iam.toml:/app/config/iam.toml:ro + depends_on: + - as + kbs: build: context: . @@ -110,6 +123,7 @@ services: - kbs - as - rvps + - iam frontend: build: diff --git a/docs/iam_user_guide.md b/docs/iam_user_guide.md new file mode 100644 index 00000000..5235c361 --- /dev/null +++ b/docs/iam_user_guide.md @@ -0,0 +1,202 @@ +# Trustee IAM 服务用户指南 + +## 1. 概述 + +Trustee IAM(Identity & Access Management)提供统一的账号、主体、资源、角色与策略管理能力,用于在多租户、多方协作场景下建立可信的授权闭环。该服务是 Trustee 体系中所有信任关系的中心,负责: + +- 管理账户(Account)与主体(Principal)的身份。 +- 注册所有需要保护的资源(Resource),并生成 ARN。 +- 定义角色(Role),并附带信任/访问策略(Policy)。 +- 通过 STS 样式的 `AssumeRole` 接口颁发短期会话令牌。 +- 提供 `authz/evaluate` 接口供 KBS、Gateway、guest-components 等组件执行实时授权判断。 + +## 2. 架构与部署 + +IAM 服务以 Actix Web 提供 REST API,默认监听 `0.0.0.0:8090`。核心模块如下: + +| 模块 | 说明 | +| --- | --- | +| `config.rs` | 解析 `iam.toml` 配置,包括服务监听、签名参数等。 | +| `models.rs` | 定义账户、主体、资源、角色、策略及请求/响应结构。 | +| `policy.rs` | 实现 Action/Resource/Condition 的匹配逻辑。 | +| `attestation.rs` | 解析来自 TEE 环境的 Base64 JSON 证明。 | +| `token.rs` | HMAC-SHA256 的 JWT 签发与校验。 | +| `service.rs` | 处理具体业务流程(创建实体、AssumeRole、鉴权)。 | +| `api.rs` | 将 HTTP 请求映射到服务方法。 | + +### 2.1 配置示例 (`iam/config/iam.toml`) + +```toml +[server] +bind_address = "0.0.0.0:8090" + +[crypto] +issuer = "trustee-iam" +hmac_secret = "replace-with-strong-secret" +default_ttl_seconds = 900 +``` + +- `issuer`:Token 中的 `iss` 字段。 +- `hmac_secret`:HMAC-SHA256 的共享密钥,建议在生产环境替换为 KMS/HSM 托管密钥。 +- `default_ttl_seconds`:未显式指定 `requested_duration_seconds` 时的默认有效期。 + +## 3. API 快速入门 + +### 3.1 创建账号 + +```bash +curl -X POST http://localhost:8090/accounts \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "demo-account", + "labels": { "tenant": "alpha" } + }' +``` + +响应: + +```json +{ + "account": { + "id": "acct-5f5c7a2b-...", + "name": "demo-account", + "labels": { "tenant": "alpha" }, + "created_at": "2025-12-04T06:00:00Z" + } +} +``` + +### 3.2 创建主体 + +```bash +curl -X POST http://localhost:8090/accounts/acct-xxx/principals \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "runtime-a", + "principal_type": "Runtime", + "attributes": { "values": { "cluster": "prod" } } + }' +``` + +### 3.3 注册资源 + +```bash +curl -X POST http://localhost:8090/resources \ + -H 'Content-Type: application/json' \ + -d '{ + "owner_account_id": "acct-xxx", + "resource_type": "kbs/key", + "tags": { "tier": "gold" } + }' +``` + +服务会返回生成的 ARN,如 `arn:trustee::acct-xxx:kbs/key/res-123`. + +### 3.4 创建角色 + +```bash +curl -X POST http://localhost:8090/roles \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "trusted-runtime", + "trust_policy": { + "statements": [{ + "effect": "Allow", + "actions": ["sts:AssumeRole"], + "resources": ["role/*"], + "conditions": [{ + "operator": "StringEquals", + "key": "principal.accountId", + "values": ["acct-xxx"] + }] + }] + }, + "access_policy": { + "statements": [{ + "effect": "Allow", + "actions": ["kbs:GetKey"], + "resources": ["arn:trustee::acct-xxx:kbs/key/*"] + }] + } + }' +``` + +### 3.5 AssumeRole + +```bash +curl -X POST http://localhost:8090/sts/assume-role \ + -H 'Content-Type: application/json' \ + -d '{ + "principal_id": "prn-xxx", + "role_id": "role-yyy", + "session_name": "demo-session", + "attestation_token": "" + }' +``` + +返回字段: + +| 字段 | 说明 | +| --- | --- | +| `token` | HMAC-SHA256 签名的 JWT,供 Gateway/KBS/guest-components 使用。 | +| `expires_at` | RFC3339 时间戳。 | + +### 3.6 鉴权评估 + +```bash +curl -X POST http://localhost:8090/authz/evaluate \ + -H 'Content-Type: application/json' \ + -d '{ + "token": "eyJhbGciOiJIUzI1NiIs...", + "action": "kbs:GetKey", + "resource": "arn:trustee::acct-xxx:kbs/key/res-123", + "context": { "caller_ip": "10.0.0.8" } + }' +``` + +返回: + +```json +{ "allowed": true } +``` + +## 4. 与其他组件的协作 + +1. **Gateway**:新增 `/api/iam` 前缀,自动将请求转发至 IAM;因此外部访问入口保持统一。 +2. **KBS**:可在获取密钥之前调用 `/authz/evaluate`,并将资源 ARN/Action 作为输入;若 `allowed=false` 则拒绝请求。 +3. **guest-components / TNG**: + - guest-components 在加载模型时先通过 attestation 获取临时令牌,再在访存/推理前验证 token 的 `env`、`principal` 等字段。 + - TNG 可将用户凭证转换为 IAM 的 `AssumeRole` 请求,从而生成统一的推理会话 token。 + +典型流程如下: + +``` +Principal -> Gateway (/api/iam/sts/assume-role) + -> IAM (验证信任策略 + Attestation) + -> 返回短期 token +Principal + Token -> 服务 (KBS/TNG) -> /api/iam/authz/evaluate + -> 允许访问受保护资源 +``` + +## 5. 最佳实践 + +1. **最小权限**:为不同协作者创建独立角色,细化 `actions` 与 `resources`,避免使用通配符 `*`。 +2. **Attestation 绑定**:在信任策略与访问策略中引入 `env.tee_type`、`env.measurement` 条件,确保 token 只能在可信环境中获取与使用。 +3. **短期令牌**:默认 TTL 为 15 分钟,可按场景缩短;建议业务侧缓存但不要长期保存。 +4. **审计**:通过 Gateway 或服务侧日志记录 `action`、`resource`、`principal`、`allowed` 等字段,便于排查授信问题。 + +## 6. 故障排查 + +| 现象 | 排查步骤 | +| --- | --- | +| `401 Unauthorized` | 检查 `AssumeRole` 请求中的 `principal_id` / `role_id` 是否存在,信任策略是否允许该主体。 | +| `403` (evaluate 返回 `{ "allowed": false }`) | 查看角色的 `access_policy` 是否覆盖对应 Action/ARN;确认传入的 `context` 字段(IP、标签等)是否满足条件。 | +| `400 invalid attestation token` | 确认 `attestation_token` 为 Base64 编码且内容为 JSON 对象。 | +| Token 过期 | 调用 `AssumeRole` 时指定较短的 `requested_duration_seconds`;定时刷新。 | + +如需要更高级的策略操作(数值、时间区间等),可扩展 `policy.rs` 中的 `ConditionOperator`,并同步更新 Gateway/调用方的请求格式。 + +--- + +通过以上步骤即可完成 IAM 服务的部署与调用。如果需要进一步的集成协助,可参考 `trustee-iam-architecture.md` 与 `trustee_gateway_api.md` 中的整体流程说明。 + diff --git a/iam/ARCHITECTURE_CN.md b/iam/ARCHITECTURE_CN.md new file mode 100644 index 00000000..53252fa4 --- /dev/null +++ b/iam/ARCHITECTURE_CN.md @@ -0,0 +1,134 @@ +# Trustee IAM 服务架构设计(中文) + +## 1. 背景与目标 + +Trustee IAM(Identity & Access Management)旨在为多参与方(基础设施提供方、模型提供方、推理调用方等)提供统一的身份、资源和策略管理能力。其核心目标包括: + +1. **统一身份管理**:抽象出 Account / Principal / ServicePrincipal 等通用概念,避免与具体业务角色(WP/MP/IU)强耦合。 +2. **资源与 ARN 命名**:通过统一的资源注册表,为 KBS、模型、数据等提供可追踪的 ARN。 +3. **角色-策略体系**:以 Trust Policy + Access Policy 的组合实现跨租户授权、联合授权、条件约束等场景。 +4. **可验证的临时凭证**:通过 STS `AssumeRole` 颁发短期 Token,将安全上下文(账户、主体、TEE 证明等)绑定在一起。 +5. **通用接入点**:Gateway、KBS、TNG、guest-components 等均可通过标准 REST API 调用 IAM,实现鉴权放大器的角色。 + + +## 2. 核心概念 + +| 概念 | 说明 | +| --- | --- | +| Account | 租户/组织边界,包含若干 Principal 与 Resource。 | +| Principal | 具体身份:人、服务、运行时、外部实体等。 | +| Resource | 需要受控访问的目标,注册时生成 ARN(如 `arn:trustee::acct-1:kbs/key/res-1`)。 | +| Role | 权限集合,由信任策略(谁能扮演)与访问策略(能访问什么)组成。 | +| Policy | PolicyDocument,包含 Statement(Effect + Actions + Resources + Conditions)。 | +| STS Token | `AssumeRole` 成功后签发的短期 JWT,携带 principal/role/env/自定义上下文。 | +| Evaluate API | 给定 token + action + resource,在策略引擎做 Allow/Deny 判断。 | + + +## 3. 系统组件 + +``` +┌──────────┐ ┌───────────┐ ┌────────────┐ +│ 调用方 │ ---> │ Gateway │ ---> │ IAM API │ +│ (终端、 │ │ (/api/iam) │ │ │ +│ KBS/TNG) │ └────┬──────┘ │ ┌─────────┐ │ +└──────────┘ │ │ │Policy │ │ + │ │ │Engine │ │ + │ │ └─────────┘ │ + │ │ ┌─────────┐ │ + └────────────> │ Token │ │ + │ Signer │ │ + │ └─────────┘ │ + │ ┌─────────┐ │ + │ │ Storage │ │ + │ └─────────┘ │ + └────────────┘ +``` + +- **API 层(Actix Web)**:处理 `POST /accounts`、`/roles`、`/sts/assume-role`、`/authz/evaluate` 等 REST 请求。 +- **Service 层**:封装业务逻辑,负责参数校验、资源 ID 生成、上下文构建。 +- **Storage**:当前为内存实现(HashMap + RwLock),后续可替换为持久化存储。 +- **Policy Engine**:判断 Action/Resource 是否匹配,并根据 Condition 解析上下文(principal/env/request 等)。 +- **Attestation 模块**:解析 Base64 JSON 形式的 TEE Claims,可在信任策略或访问策略中引用。 +- **Token Signer**:使用 HMAC-SHA256 (JWT) 颁发/验证 Token,后续可接入 KMS/HSM。 + + +## 4. 数据流与关键流程 + +### 4.1 资源注册 +1. 资源所有者调用 `POST /resources`,指定 `owner_account_id` 与 `resource_type`。 +2. 服务生成 `res-xxx` 并构造 ARN(`arn:trustee:::type/res-id`)。 +3. 返回资源对象,供策略引用。 + +### 4.2 角色创建 +1. 维护者调用 `POST /roles`,提供 `trust_policy` 与 `access_policy`。 +2. IAM 存储 Role 元数据,并在后续 `AssumeRole`/`Evaluate` 时引用。 + +### 4.3 AssumeRole +1. Principal 携带(可选)Attestation Token 调用 `/sts/assume-role`。 +2. IAM 验证 Attestation,构造上下文:`principal`、`env`、`request`。 +3. Policy Engine 运行角色的 Trust Policy;若允许,则调用 TokenSigner 颁发 Token。 + +### 4.4 Access Evaluate +1. 业务服务(KBS/TNG 等)调用 `/authz/evaluate`,传入 Token + Action + Resource。 +2. IAM 验证 Token,加载角色 Access Policy。 +3. Policy Engine 匹配 action/resource + condition,返回 Allow/Deny 布尔值。 + + +## 5. 策略上下文设计 + +PolicyEngine 接收 `MatchContext = { principal, env, resource, request }`,字段说明: + +| 字段 | 示例 | 来源 | +| --- | --- | --- | +| `principal` | `{ id, accountId, type, attributes }` | Principal 信息 | +| `env` | `{ tee_type, measurement }` | Attestation Claims | +| `resource` | `{ arn, ownerAccountId, resourceType, tags }` | `Evaluate` 时根据 ARN 查得 | +| `request` | `{ action, resource, context }` | API 调用参数 | + +Condition Key 使用点号访问,如 `principal.accountId`、`env.tee_type`、`request.context.ip` 等。 + + +## 6. API 汇总 + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| `POST /accounts` | 创建账号 | +| `POST /accounts/{account_id}/principals` | 创建主体 | +| `POST /resources` | 注册资源(生成 ARN) | +| `POST /roles` | 创建角色(含策略) | +| `POST /sts/assume-role` | 获取 STS Token,支持 attestation | +| `POST /authz/evaluate` | 校验 Token + Action + Resource | + +**说明**:Gateway 中的 `/api/iam/**` 与上表完全映射,便于外部组件统一调用。 + + +## 7. 部署与运维 + +1. **容器化**:`Dockerfile.iam` 构建二进制;`docker-compose.yml` 中已有 `iam` 服务,并被 Gateway 依赖。 +2. **配置**:默认 `iam/config/iam.toml`,可通过挂载覆盖。生产环境需替换 `hmac_secret`。 +3. **dist 集成**:在 `dist/start.sh` 中加入 IAM 启动逻辑;systemd 单元 `iam.service` 支持单机部署。 +4. **日志 & 监控**:当前使用 stdout + logrotate;建议通过 `RUST_LOG` 设置调试级别,并接入外部日志系统。 + + +## 8. 与其他组件的关系 + +- **Gateway**:代理 `/api/iam/*` 请求;更新配置后可通过 `iam.url` 指向不同环境。 +- **KBS**:在发放密钥前可调用 `/authz/evaluate`,确保请求方拥有相应角色。 +- **Attestation Service**:IAM 不直接依赖 AS,但通过 Attestation Token 获取 TEE 信息,与 `env.*` 条件结合。 +- **Guest Components / TNG**:可在可信环境内使用 Attestation Token 兑换 STS Token,实现端到端闭环。 + + +## 9. 后续演进方向 + +1. **持久化存储**:替换内存 Store,增加多副本或外部数据库,更好地支持生产部署。 +2. **策略引擎增强**:支持数值比较、时间窗口、集合运算等更丰富的条件表达式。 +3. **Token 托管**:接入 KMS/HSM 管控密钥,或支持多种签名算法(如 ES256、EdDSA)。 +4. **多租户隔离**:在 API 层引入多租户/AuthN 机制(OAuth2/OIDC),细化调用方权限。 +5. **审计日志**:记录 `AssumeRole` / `Evaluate` 事件,方便安全分析与计费。 +6. **SDK & 模板**:提供语言 SDK、策略模板,降低接入成本。 + + +## 10. 总结 + +Trustee IAM 通过 **Account + Resource + Role + Policy + STS** 的组合,提供通用且可扩展的授权体系。在现有实现中,核心流程已具备“注册资源 → 创建角色 → AssumeRole → Evaluate”闭环,并可与 Gateway/KBS/guest-components 等其他组件协同使用。随着持久化、审计与 SDK 的完善,IAM 将成为 Trustee 生态中连接多方信任的关键枢纽。 + diff --git a/iam/Cargo.toml b/iam/Cargo.toml new file mode 100644 index 00000000..1ad0c2e6 --- /dev/null +++ b/iam/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "iam" +version.workspace = true +authors.workspace = true +description.workspace = true +documentation.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = { workspace = true } +anyhow = { workspace = true } +async-trait = { workspace = true } +base64 = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true, features = ["derive"] } +config = { workspace = true } +env_logger = { workspace = true } +jsonwebtoken = { workspace = true } +log = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +uuid = { version = "1", features = ["serde", "v4"] } diff --git a/iam/INTEGRATION_CN.md b/iam/INTEGRATION_CN.md new file mode 100644 index 00000000..7bd5757c --- /dev/null +++ b/iam/INTEGRATION_CN.md @@ -0,0 +1,279 @@ +# Trustee IAM 与 KBS / Attestation Token / TNG / guest-components 联动设计说明 + +本说明文档聚焦于:**在已有的 KBS、Attestation、TNG、guest-components 体系基础上,IAM 如何嵌入并形成端到端的可信授权闭环**。 +对应的整体架构思想可以结合根目录下的 `trustee-iam-architecture.md` 一起阅读。 + +--- + +## 1. 参与组件与定位 + +- **IAM 服务(本仓库 `iam`)** + - 提供统一的账号(Account)、主体(Principal)、资源(Resource)、角色(Role)、策略(Policy)与 Token 能力。 + - API 示例: + - `POST /accounts` / `POST /principals` / `POST /resources` / `POST /roles` + - `POST /sts/assume-role` + - `POST /authz/evaluate` + +- **KBS(Key Broker Service)** + - 管理密钥、机密资源(例如模型加密密钥、启动密钥等)。 + - 接收来自可信运行环境的证明(Attestation)和请求,按策略将密钥下发给客户端(如 guest-components)。 + +- **Attestation Service(AS)** + - 面向 TEE 运行时提供远程证明接口。 + - 验证测量值、证书链等,生成 Attestation Token 或 Attestation Result(包含安全声明 Claims)。 + +- **TNG(如推理网关 / Token Generation Service)** + - 面向推理调用方和上层应用,将用户身份、调用意图转换为一组 Token(可能包括 IAM STS Token)。 + - 是“用户世界”与“受保护运行时世界”的连接器。 + +- **guest-components(机密 VM / 容器内部的 Agent/Runtime)** + - 在 TEE 内运行,负责: + - 携带 Attestation 结果向 IAM / KBS 等组件证明自己; + - 拉取模型或密钥; + - 执行推理请求。 + +- **Gateway** + - 作为统一入口,将 `/api/iam`、`/api/kbs`、`/api/attestation-service` 等请求代理到后端服务。 + - 当前实现中,`/api/iam/**` 全量转发到 IAM。 + +--- + +## 2. 核心设计思想:将“身份 + 环境 + 资源”统一成策略可见上下文 + +IAM 的目标不是替代 KBS 或 AS,而是将它们已有的“证明结果”和“资源概念”统一在一个策略评估框架下: + +1. **身份(Principal)**:谁在发起请求? +2. **环境(Env / Attestation Claims)**:这个请求从什么运行环境发出?TEE 测量值、平台类型是否可信? +3. **资源(Resource)**:操作的目标是哪个 KBS Key / 模型 / Endpoint / 部署实例等? +4. **操作(Action)**:例如 `kbs:GetKey`、`model:Invoke`、`endpoint:Invoke`。 + +IAM 通过: + +- Attestation 模块:将 Attestation Token 转换为 `env.*`; +- Resource Registry:将 KBS 的 Resource ID 统一成 ARN; +- STS Token:将 Principal + Role + Env + Custom Context 封装成一个短期 Token; +- Policy Engine:在 Evaluate 阶段同时看到 action/resource/principal/env 等信息, + +从而帮助 KBS、TNG、guest-components 简化授权逻辑,仅需在关键点调用 IAM 即可。 + +--- + +## 3. Attestation Token 与 IAM 的联动 + +### 3.1 引导关系 + +Attestation Token 在当前设计中被定位为 **AssumeRole 的输入材料**,流程如下: + +1. guest-components 或运行时在启动时向 AS 发起证明请求。 +2. AS 返回 Attestation 结果(可包含 TEE 测量值、证书链、平台信息等)。 +3. guest-components 将该结果封装/转换为 Base64 JSON 字符串(Attestation Token)。 +4. guest-components 调用 IAM 的 `POST /sts/assume-role`,将 Attestation Token 作为 `attestation_token` 字段上传。 +5. IAM 内部: + - 使用 `attestation.rs` 解析 token 为 `Map`(`env` 部分); + - 将 `env` 与 `principal`、`request` 一起作为策略上下文; + - 使用角色 trust_policy 中的 Condition 判断是否允许该环境和主体 `AssumeRole`。 +6. 若通过,IAM 签发 STS Token,后续由 guest-components/KBS/TNG 等组件使用。 + +### 3.2 策略中的 Attestation Claim + +在 Trust Policy 或 Access Policy 中,可以写出类似规则: + +- `env.tee_type == "TDX"` +- `env.measurement in { "0x1234...", "0xabcd..." }` +- `env.owner == "cloud-provider-A"` + +Policy Engine 将 `env` 视为普通 JSON 对象,对这些字段做字符串匹配或通配匹配。 +接口层无需关心 Attestation 的内部格式,只需要约定好 Claim 字段名称与含义即可。 + +--- + +## 4. IAM 与 KBS 的联动 + +### 4.1 KBS 资源与 ARN 映射 + +在已有 KBS 中,“Key / Secret / Blob 等资源”已存在自己的标识(如 repository/path、key-id 等)。IAM 将其抽象为: + +- `arn:trustee::acct-123:kbs/key/key-001` +- `arn:trustee::acct-123:kbs/blob/blob-xyz` + +映射关系可以在: + +- 部署时由控制面统一注册;或 +- 在 KBS 首次访问对应资源时动态注册(lazy registration)。 + +### 4.2 KBS 调用 IAM 进行授权评估 + +KBS 在“即将返回 Key 或 Secret”前,引入如下授权钩子: + +1. 从来访请求中解析出: + - 主体 STS Token(由 guest-components 或上层服务提供); + - 目标资源 ARN; + - 访问操作 Action(如 `kbs:GetKey`、`kbs:Decrypt`)。 +2. 调用 IAM 的 `POST /authz/evaluate`: + ```json + { + "token": "", + "action": "kbs:GetKey", + "resource": "arn:trustee::acct-123:kbs/key/key-001", + "context": { + "caller_ip": "10.0.0.5", + "channel": "kbs" + } + } + ``` +3. IAM: + - 验证 token(签名、时效); + - 解析其中的 `sub`、`tenant`、`role`、`env`、`custom`; + - 根据 resource ARN 装配 `resource` 上下文; + - 执行角色 Access Policy: + - 若匹配,则返回 `{ "allowed": true }`; + - 否则返回 `{ "allowed": false }`。 +4. KBS 根据结果决定是否下发 Key/Secret。 + +从而实现: + +- 授权逻辑从 KBS 抽象出来,由 IAM 统一管理; +- 策略统一控制“哪个运行环境 + 哪个主体 + 对哪类 Key 拥有哪些操作能力”。 + +--- + +## 5. IAM 与 TNG 的联动 + +IAM 与 TNG 的联动应当站在“网络平面 + 证明平面”和“授权平面”分层的角度来设计。 + +### 5.1 TNG 在整体方案中的角色 + +- 在 **数据平面** 上: + - TNG 通过 `add_ingress` / `add_egress` 配置(`mapping` / `http_proxy` / `socks5` / `netfilter` 等模式),将业务流量封装进经 RA 保护的隧道中。 + - 在 `attest` / `verify` 字段开启时,TNG 会对接 AS,发起或验证远程证明,只有通过验证的对端才能建立隧道。 +- 在 **控制/信任平面** 上: + - TNG 自身只负责 **“保证对端运行环境可信 + 建立加密通道”**; + - 业务层的“用户是谁/能干什么”不由 TNG 直接决策,而是交由上层(例如 Gateway 或具体服务)与 IAM 协作完成。 + +换句话说:**TNG 给 IAM/KBS/Gateway 提供“连接来自可信 TEE 的网络上下文”,而 IAM 在此基础上做“谁对什么资源拥有什么权限”的细粒度授权。** + +### 5.2 推荐的协作模式 + +1. **TNG + AS:负责建立可信通路** + - 在 client/container 侧部署 TNG(`attest` 角色),在服务端(靠近 Gateway/KBS/IAM 一侧)部署 TNG(`verify` 角色)。 + - server 侧 TNG 通过配置的 `as_addr` 与 `policy_ids` 调用 AS,验证对端环境(例如 TDX VM / 可信容器),并按策略决定是否接受连接。 + - 一旦连接建立,TNG 将业务 TCP/HTTP 流量透明地转发到本地的 Gateway/KBS/IAM 服务端口。 + +2. **Gateway/KBS/IAM:在应用层感知“连接已被 TNG 保护”** + - 对应用服务(Gateway/KBS/IAM)而言,请求源现在包括两层含义: + - 网络上:来自本地 TNG verify 实例(通常是 127.0.0.1 或同一 VPC 内地址); + - 语义上:实际来源是通过 RA 验证的远端 TEE 环境。 + - 目前 TNG 默认并不会修改上层 HTTP 头,但可在后续演进中考虑以下扩展: + - server 侧 TNG 在完成验证后,将 Attestation Result 的摘要(如 status、platform、measurement hash)注入到上游 HTTP 头中(例如自定义 `X-TNG-Attestation`); + - Gateway/KBS 将这一摘要转换为 IAM `AssumeRole` / `authz/evaluate` 所需的 `attestation_token` 或 `env.*` 字段。 + +3. **IAM:只承担“证明 +身份 + 资源”的策略裁决,不关心 TNG 的内部细节** + - 对 IAM 而言,TNG 是否存在、采用何种隧道协议,是透明的; + - IAM 只需要: + - 从上游(Gateway/KBS/guest-components)获取 Attestation Token 或环境 Claims; + - 在 Condition 中使用 `env.tee_type`、`env.measurement` 等字段; + - 针对 `action=...` / `resource=arn:...` 做 Allow/Deny 判定。 + +### 5.3 典型链路(网络 + 授权) + +以“client 容器访问 KBS 获取密钥”为例: + +1. client 容器将请求发往本地 TNG ingress(例如 http_proxy 模式)。 +2. 本地 TNG 使用 OHTTP + RA 与服务端 TNG 建立隧道,服务端 TNG 通过 AS 验证 client 侧 TEE 证据。 +3. 通路建立后,服务端 TNG 把 HTTP 请求转发到本机的 Gateway/KBS。 +4. KBS 在处理 `/kbs/v0/...` 时,从请求中取出: + - 来自上层的 IAM STS Token(身份 + 角色); + - 可选的 Attestation Result Token(如果通过 TNG / AS 传递上来)。 +5. KBS 调用 IAM 的 `/authz/evaluate`,在 Context 中写入 `env.*`(来自 Attestation)和 `request.*` 等字段。 +6. IAM 返回 `{ allowed: true|false }`,KBS 决定是否返回密钥。 + +在整个过程中: + +- **TNG 只负责“确保链路对端的运行环境可信,并安全转发流量”**; +- **IAM 只负责“在这个前提下,对具体操作做授权判断”**; +- 二者通过 Gateway/KBS 等上层服务桥接起来,而非直接互相调用。 + +--- + +## 6. IAM 与 guest-components 的联动 + +guest-components 位于 TEE 内部,是 Attestation 的主要执行者,也是实际执行“获取密钥 / 加载模型 / 执行推理”的组件。 + +### 6.1 运行时自举(Runtime Bootstrap) + +1. guest-components 在 TEE 启动后,向 AS 发起远程证明,获得 Attestation 结果。 +2. guest-components 将 Attestation 结果编码为 Base64 JSON,调用 IAM 的 `AssumeRole`: + - 该 role 一般为“受保护运行时角色”,其 Trust Policy 要求: + - `env.tee_type` 合法; + - `env.measurement` 在白名单内; + - `principal` 满足某些 account id/标签约束。 +3. IAM 返回运行时 STS Token,guest-components 将其缓存,用于后续访问 KBS 或其他服务。 + +### 6.2 运行时访问检查 + +当 guest-components 要从 KBS 拉取密钥时: + +1. 使用上一步获取的 STS Token 调用 KBS 的 `/kbs/v0/...` API; +2. KBS 内部调用 IAM 的 `/authz/evaluate` 判定; +3. 若通过,KBS 发送密钥,guest-components 在 TEE 内解密/使用。 + +同理,在后续访问 TNG 或其他服务时,也可以采用同一 STS Token 作为“运行时身份”,避免重复 Attestation。 + +--- + +## 7. 典型端到端场景串联(以“通过 TNG 访问 KBS”为例) + +以“应用容器通过 TNG 隧道访问 KBS 获取密钥”为例,整体链路如下: + +1. **应用容器 → 本地 TNG(ingress)** + - 应用将所有访问 KBS 的流量(例如 `http://kbs.internal:8080`)配置为通过 TNG 的 `http_proxy` 或 `socks5` 入口。 + - TNG ingress 将明文流量封装,并扮演 Attester,准备与 server 侧 TNG 建立 OHTTP/RA 隧道。 + +2. **TNG ingress ↔ TNG egress + AS** + - TNG egress 侧(靠近 KBS/Gateway 的一侧)在 `verify` 配置下向 AS 发起远程证明验证; + - AS 对 TEE 证据进行评估,返回 Attestation Result(或标记为不可信); + - TNG egress 仅在验证通过时允许隧道建立。 + +3. **TNG egress → Gateway/KBS** + - 隧道建立后,KBS/Gateway 实际看到的网络连接来自 TNG egress(例如 `127.0.0.1:port`),但语义上代表“某个通过 RA 验证的远端 TEE 环境”; + - 如果后续扩展 TNG,将 Attestation Result 摘要通过 HTTP 头传给 Gateway,则 Gateway 可以据此构造 `attestation_token` 提供给 IAM。 + +4. **KBS → IAM(authz/evaluate)** + - 应用在访问 KBS 前,已通过业务层或 IAM 获取了用户/服务的 STS Token(由 `AssumeRole` 产生); + - KBS 在即将返回敏感密钥前,调用 `POST /authz/evaluate`,将: + - STS Token(身份 + 角色); + - 资源 ARN(如某个 Key); + - 从 TNG/AS 获得的 Attestation Claims(映射到 `env.*`); + - 以及请求自身信息(来源 IP、通道类型)等, + 一并提交给 IAM。 + +5. **IAM 策略评估** + - IAM 在 Policy Engine 中综合考虑: + - `principal.*`(调用主体是谁、属于哪个 Account); + - `env.*`(连接对端是否来自预期的 TEE 环境、测量值是否合法); + - `resource.*`(密钥属于谁、是否高敏); + - `request.*`(操作为 `kbs:GetKey`、来源 IP、访问路径等); + - 若满足策略条件,则返回 `{ "allowed": true }`,否则 `{ "allowed": false }`。 + +6. **KBS → 应用容器** + - 若授权通过,KBS 将密钥通过 TNG 隧道回传给应用; + - 整个过程对应用而言只是一条“HTTP 调用”,但实际在网络层已受到 TNG + AS 的保护,在授权层受到 IAM 的控制。 + +在这个过程中: + +- **TNG + AS**:负责确保“连到 KBS/Gateway 的这条链路对端,是某个通过远程证明验证的可信环境”; +- **IAM**:负责在此基础上,对具体 Action/Resource 做细粒度权限裁决; +- **KBS**:负责密钥和机密数据的持有与下发,实现数据面的实际访问控制。 + +--- + +## 8. 小结 + +通过将 Attestation Token、KBS 资源、TNG 用户授权和 guest-components 运行时身份统一在 IAM 的“身份 + 资源 + 策略”模型下: + +- **安全属性**:可在策略中表达非常细粒度的要求(例如“只有特定测量值的 TEE、来自某账号的运行时、代表某个用户,在某个时间段内才可访问某个密钥”)。 +- **可运营性**:授权与回收可以通过修改 Role/Policy 实现,而不需要改动 KBS/TNG/guest-components 的核心代码。 +- **可扩展性**:未来接入更多服务或资源类型时,只需要注册新的 ResourceType 和 Action,将其接入 IAM 的 Evaluate 流程即可。 + +这套联动方案既保持各组件的职责边界,又通过 IAM 提供统一可控的授权平面,是整个 Trustee 体系在多方协作场景下的关键设计。*** + diff --git a/iam/README.md b/iam/README.md new file mode 100644 index 00000000..e2e823ba --- /dev/null +++ b/iam/README.md @@ -0,0 +1,43 @@ +# Trustee IAM Service + +This crate implements the first iteration of the Trustee IAM service that powers the unified role / policy / token flow described in `../../trustee-iam-architecture.md`. + +## Features + +- Account, principal, resource and role registration APIs. +- JSON policy language with trust and access policies, including string wildcard matching and basic condition operators. +- STS-style `AssumeRole` that can take attestation tokens as input. +- Access evaluation endpoint designed to be consumed by control planes (e.g. KBS, TNG, guest-components). +- JWT-based session tokens signed with an HMAC secret configurable through `config/iam.toml`. + +## Usage + +```bash +cargo run -p iam -- --config config/iam.toml +``` + +Example `iam.toml`: + +```toml +[server] +bind_address = "0.0.0.0:8090" + +[crypto] +issuer = "trustee-iam" +hmac_secret = "replace-with-strong-secret" +default_ttl_seconds = 900 +``` + +## API Overview + +| Endpoint | Description | +| --- | --- | +| `POST /accounts` | Create a logical account. | +| `POST /accounts/{account_id}/principals` | Create a principal (user/service/runtime) under an account. | +| `POST /resources` | Register a resource ARN. | +| `POST /roles` | Create a role with trust and access policies. | +| `POST /sts/assume-role` | Evaluate trust policy + attestation and issue a session token. | +| `POST /authz/evaluate` | Validate a token and evaluate the access policy for an action/resource. | + +All payloads are JSON and map one-to-one to the request structs defined in `src/models.rs`. + diff --git a/iam/config/iam.toml b/iam/config/iam.toml new file mode 100644 index 00000000..49969f62 --- /dev/null +++ b/iam/config/iam.toml @@ -0,0 +1,8 @@ +[server] +bind_address = "0.0.0.0:8090" + +[crypto] +issuer = "trustee-iam" +hmac_secret = "replace-with-strong-secret" +default_ttl_seconds = 900 + diff --git a/iam/src/api.rs b/iam/src/api.rs new file mode 100644 index 00000000..9b02974f --- /dev/null +++ b/iam/src/api.rs @@ -0,0 +1,83 @@ +//! HTTP routing glue for the IAM REST API. + +use actix_web::{web, HttpResponse}; + +use crate::error::IamError; +use crate::models::{ + AssumeRoleRequest, CreateAccountRequest, CreatePrincipalRequest, CreateRoleRequest, + EvaluateRequest, RegisterResourceRequest, +}; +use crate::service::IamService; + +/// Register all IAM endpoints on the supplied Actix configuration. +pub fn configure(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("") + .route("/accounts", web::post().to(create_account)) + .route( + "/accounts/{account_id}/principals", + web::post().to(create_principal), + ) + .route("/resources", web::post().to(register_resource)) + .route("/roles", web::post().to(create_role)) + .route("/sts/assume-role", web::post().to(assume_role)) + .route("/authz/evaluate", web::post().to(evaluate_request)), + ); +} + +/// POST /accounts +async fn create_account( + service: web::Data, + payload: web::Json, +) -> Result { + let response = service.create_account(payload.into_inner()).await?; + Ok(HttpResponse::Created().json(response)) +} + +/// POST /accounts/{id}/principals +async fn create_principal( + service: web::Data, + path: web::Path, + payload: web::Json, +) -> Result { + let response = service + .create_principal(&path.into_inner(), payload.into_inner()) + .await?; + Ok(HttpResponse::Created().json(response)) +} + +/// POST /resources +async fn register_resource( + service: web::Data, + payload: web::Json, +) -> Result { + let response = service.register_resource(payload.into_inner()).await?; + Ok(HttpResponse::Created().json(response)) +} + +/// POST /roles +async fn create_role( + service: web::Data, + payload: web::Json, +) -> Result { + let response = service.create_role(payload.into_inner()).await?; + Ok(HttpResponse::Created().json(response)) +} + +/// POST /sts/assume-role +async fn assume_role( + service: web::Data, + payload: web::Json, +) -> Result { + let response = service.assume_role(payload.into_inner()).await?; + Ok(HttpResponse::Ok().json(response)) +} + +/// POST /authz/evaluate +async fn evaluate_request( + service: web::Data, + payload: web::Json, +) -> Result { + let response = service.evaluate(payload.into_inner()).await?; + Ok(HttpResponse::Ok().json(response)) +} diff --git a/iam/src/attestation.rs b/iam/src/attestation.rs new file mode 100644 index 00000000..2340e350 --- /dev/null +++ b/iam/src/attestation.rs @@ -0,0 +1,40 @@ +//! Minimal attestation token parsing helpers (Base64 JSON -> env map). + +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use serde_json::{Map, Value}; + +use crate::error::IamError; + +/// Parsed attestation claims kept in a structured map. +#[derive(Debug, Clone, Default)] +pub struct AttestationContext { + pub claims: Map, +} + +impl AttestationContext { + /// Convert claims into the env map stored within session tokens. + pub fn to_env(&self) -> Map { + self.claims.clone() + } +} + +/// Verify (decode + JSON parse) the optional attestation token. +pub fn verify_attestation(token: Option<&str>) -> Result { + match token { + None => Ok(AttestationContext::default()), + Some(raw) => { + let bytes = STANDARD.decode(raw).map_err(|err| { + IamError::InvalidRequest(format!("invalid attestation token: {err}")) + })?; + let claims: Value = serde_json::from_slice(&bytes).map_err(|err| { + IamError::InvalidRequest(format!("invalid attestation payload: {err}")) + })?; + match claims { + Value::Object(map) => Ok(AttestationContext { claims: map }), + _ => Err(IamError::InvalidRequest( + "attestation payload must be a JSON object".to_string(), + )), + } + } + } +} diff --git a/iam/src/config.rs b/iam/src/config.rs new file mode 100644 index 00000000..86d7aa1c --- /dev/null +++ b/iam/src/config.rs @@ -0,0 +1,52 @@ +//! Configuration helpers for the IAM service. + +use std::path::Path; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +/// Full configuration structure loaded from `iam.toml`. +#[derive(Debug, Clone, Deserialize)] +pub struct IamConfig { + /// HTTP server configuration. + pub server: ServerConfig, + /// Cryptography/signing related configuration. + pub crypto: CryptoConfig, +} + +/// HTTP server configuration. +#[derive(Debug, Clone, Deserialize)] +pub struct ServerConfig { + /// Bind address such as `0.0.0.0:8090`. + #[serde(default = "default_bind_address")] + pub bind_address: String, +} + +/// Signing configuration for the STS token issuer. +#[derive(Debug, Clone, Deserialize)] +pub struct CryptoConfig { + /// Issuer string embedded into every token. + pub issuer: String, + /// Shared secret for HMAC signing (replace with KMS/HSM in production). + pub hmac_secret: String, + /// Default TTL (seconds) for issued sessions. + #[serde(default = "default_ttl_seconds")] + pub default_ttl_seconds: u64, +} + +impl IamConfig { + /// Load configuration from a filesystem path. + pub fn from_file(path: &Path) -> Result { + let builder = config::Config::builder().add_source(config::File::from(path)); + let raw = builder.build().context("failed to build config loader")?; + raw.try_deserialize().context("failed to parse IAM config") + } +} + +fn default_bind_address() -> String { + "0.0.0.0:8090".to_string() +} + +fn default_ttl_seconds() -> u64 { + 900 +} diff --git a/iam/src/error.rs b/iam/src/error.rs new file mode 100644 index 00000000..58341296 --- /dev/null +++ b/iam/src/error.rs @@ -0,0 +1,34 @@ +use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum IamError { + #[error("entity not found: {0}")] + NotFound(String), + #[error("entity already exists: {0}")] + Conflict(String), + #[error("unauthorized: {0}")] + Unauthorized(String), + #[error("invalid request: {0}")] + InvalidRequest(String), + #[error("internal error: {0}")] + Internal(String), +} + +impl ResponseError for IamError { + fn status_code(&self) -> StatusCode { + match self { + IamError::NotFound(_) => StatusCode::NOT_FOUND, + IamError::Conflict(_) => StatusCode::CONFLICT, + IamError::Unauthorized(_) => StatusCode::UNAUTHORIZED, + IamError::InvalidRequest(_) => StatusCode::BAD_REQUEST, + IamError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).json(serde_json::json!({ + "error": self.to_string() + })) + } +} diff --git a/iam/src/main.rs b/iam/src/main.rs new file mode 100644 index 00000000..a13835a5 --- /dev/null +++ b/iam/src/main.rs @@ -0,0 +1,48 @@ +mod api; +mod attestation; +mod config; +mod error; +mod models; +mod policy; +mod service; +mod storage; +mod token; + +use std::path::PathBuf; + +use actix_web::{web, App, HttpServer}; +use clap::Parser; + +use crate::config::IamConfig; +use crate::service::IamService; + +/// Command-line switches for the IAM binary. +#[derive(Parser, Debug)] +#[command(author, version, about = "Trustee IAM Service")] +struct Cli { + #[arg(short, long, default_value = "config/iam.toml")] + config: PathBuf, +} + +#[actix_web::main] +async fn main() -> anyhow::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + let cli = Cli::parse(); + let config = + IamConfig::from_file(&cli.config).map_err(|err| anyhow::anyhow!(err.to_string()))?; + let service = IamService::new(&config) + .map_err(|err| anyhow::anyhow!(format!("failed to start IAM: {err}")))?; + let bind_addr = config.server.bind_address.clone(); + let shared_service = web::Data::new(service); + + HttpServer::new(move || { + App::new() + .app_data(shared_service.clone()) + .configure(api::configure) + }) + .bind(&bind_addr)? + .run() + .await?; + + Ok(()) +} diff --git a/iam/src/models.rs b/iam/src/models.rs new file mode 100644 index 00000000..e890bc9a --- /dev/null +++ b/iam/src/models.rs @@ -0,0 +1,287 @@ +//! Core IAM data models and request/response payloads. + +use std::collections::BTreeMap; + +use chrono::{DateTime, Utc}; +use serde::{self, Deserialize, Serialize}; +use uuid::Uuid; + +pub type AccountId = String; +pub type PrincipalId = String; +pub type RoleId = String; +pub type ResourceArn = String; + +/// Logical tenant boundary that owns principals and resources. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Account { + pub id: AccountId, + pub name: String, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub labels: BTreeMap, + pub created_at: DateTime, +} + +/// Security principal (human, workload, service, etc.). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Principal { + pub id: PrincipalId, + pub account_id: AccountId, + pub name: String, + pub principal_type: PrincipalType, + #[serde(default, skip_serializing_if = "Attributes::is_empty")] + pub attributes: Attributes, + pub created_at: DateTime, +} + +/// Known principal persona categories. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PrincipalType { + Human, + Service, + Runtime, + External, + Unknown, +} + +impl Default for PrincipalType { + fn default() -> Self { + Self::Unknown + } +} + +/// Resource registration metadata (maps to ARN). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Resource { + pub arn: ResourceArn, + pub owner_account_id: AccountId, + pub resource_type: String, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub tags: BTreeMap, + #[serde(default, skip_serializing_if = "Attributes::is_empty")] + pub attributes: Attributes, + pub created_at: DateTime, +} + +/// Role combines trust/access policies and metadata. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Role { + pub id: RoleId, + pub name: String, + pub description: Option, + pub trust_policy: PolicyDocument, + pub access_policy: PolicyDocument, + #[serde(default)] + pub labels: BTreeMap, + pub created_at: DateTime, +} + +/// IAM policy document (trust or access). +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PolicyDocument { + #[serde(default = "default_policy_version")] + pub version: String, + #[serde(default)] + pub statements: Vec, +} + +impl Default for PolicyDocument { + fn default() -> Self { + Self { + version: default_policy_version(), + statements: Vec::new(), + } + } +} + +/// Individual statement within a policy document. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Statement { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sid: Option, + pub effect: Effect, + pub actions: Vec, + pub resources: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub conditions: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Effect { + #[serde(rename = "Allow")] + Allow, + #[serde(rename = "Deny")] + Deny, +} + +/// Condition expression using a single operator/key/value-set. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Condition { + pub operator: ConditionOperator, + pub key: String, + pub values: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ConditionOperator { + StringEquals, + StringLike, + Bool, +} + +/// Free-form metadata container stored alongside principals/resources. +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Attributes { + #[serde(default)] + pub values: serde_json::Map, +} + +impl Attributes { + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } +} + +impl From> for Attributes { + fn from(values: serde_json::Map) -> Self { + Self { values } + } +} + +/// Request payload for creating an account. +#[derive(Debug, Deserialize)] +pub struct CreateAccountRequest { + pub name: String, + #[serde(default)] + pub labels: BTreeMap, +} + +/// Response wrapper for account creation/read APIs. +#[derive(Debug, Serialize)] +pub struct AccountResponse { + pub account: Account, +} + +/// Request payload for creating a principal under an account. +#[derive(Debug, Deserialize)] +pub struct CreatePrincipalRequest { + pub name: String, + #[serde(default)] + pub principal_type: PrincipalType, + #[serde(default)] + pub attributes: Attributes, +} + +/// Response wrapper for principal APIs. +#[derive(Debug, Serialize)] +pub struct PrincipalResponse { + pub principal: Principal, +} + +/// Request payload for resource registration. +#[derive(Debug, Deserialize)] +pub struct RegisterResourceRequest { + pub owner_account_id: AccountId, + pub resource_type: String, + #[serde(default)] + pub tags: BTreeMap, + #[serde(default)] + pub attributes: Attributes, +} + +/// Response wrapper for resource operations. +#[derive(Debug, Serialize)] +pub struct ResourceResponse { + pub resource: Resource, +} + +/// Request payload for creating a role. +#[derive(Debug, Deserialize)] +pub struct CreateRoleRequest { + pub name: String, + #[serde(default)] + pub description: Option, + pub trust_policy: PolicyDocument, + pub access_policy: PolicyDocument, + #[serde(default)] + pub labels: BTreeMap, +} + +/// Response wrapper for role operations. +#[derive(Debug, Serialize)] +pub struct RoleResponse { + pub role: Role, +} + +/// Request payload for AssumeRole/STSlike exchange. +#[derive(Debug, Deserialize)] +pub struct AssumeRoleRequest { + pub principal_id: PrincipalId, + pub role_id: RoleId, + #[serde(default)] + pub requested_duration_seconds: Option, + #[serde(default)] + pub session_name: Option, + #[serde(default)] + pub attestation_token: Option, + #[serde(default)] + pub context: Option>, +} + +/// STS response containing the opaque signed token. +#[derive(Debug, Serialize)] +pub struct AssumeRoleResponse { + pub token: String, + pub expires_at: DateTime, +} + +/// Authorization evaluation request (used by proxies/services). +#[derive(Debug, Deserialize)] +pub struct EvaluateRequest { + pub token: String, + pub action: String, + pub resource: ResourceArn, + #[serde(default)] + pub context: Option>, +} + +/// Result of authorization evaluation. +#[derive(Debug, Serialize)] +pub struct EvaluateResponse { + pub allowed: bool, +} + +/// JWT claims embedded inside every IAM session token. +#[derive(Debug, Serialize, Deserialize)] +pub struct SessionClaims { + pub sub: PrincipalId, + pub tenant: AccountId, + pub role: RoleId, + pub iss: String, + pub iat: i64, + pub exp: i64, + #[serde(default)] + pub session_name: Option, + #[serde(default)] + pub env: serde_json::Map, + #[serde(default)] + pub custom: serde_json::Map, +} + +impl SessionClaims { + /// Convert the numeric expiry into `DateTime`. + pub fn expires_at(&self) -> DateTime { + DateTime::::from_timestamp(self.exp, 0) + .unwrap_or_else(|| DateTime::::from_timestamp(0, 0).expect("unix epoch is valid")) + } +} + +/// Generate a random identifier with a stable prefix. +pub fn generate_id(prefix: &str) -> String { + format!("{}-{}", prefix, Uuid::new_v4()) +} + +fn default_policy_version() -> String { + "2025-01-01".to_string() +} diff --git a/iam/src/policy.rs b/iam/src/policy.rs new file mode 100644 index 00000000..f31ff88a --- /dev/null +++ b/iam/src/policy.rs @@ -0,0 +1,128 @@ +//! Lightweight policy evaluation helpers. + +use serde_json::{Map, Value}; + +use crate::models::{Condition, ConditionOperator, Effect, PolicyDocument}; + +/// Evaluate a policy document for the supplied action/resource/context triple. +pub fn evaluate_policy( + policy: &PolicyDocument, + action: &str, + resource: &str, + context: &Map, +) -> bool { + let mut allowed = false; + for statement in &policy.statements { + if !action_matches(&statement.actions, action) { + continue; + } + if !resource_matches(&statement.resources, resource) { + continue; + } + if !conditions_match(&statement.conditions, context) { + continue; + } + match statement.effect { + Effect::Deny => return false, + Effect::Allow => allowed = true, + } + } + allowed +} + +fn action_matches(patterns: &[String], action: &str) -> bool { + patterns + .iter() + .any(|pattern| wildcard_match(pattern, action)) +} + +fn resource_matches(patterns: &[String], resource: &str) -> bool { + patterns + .iter() + .any(|pattern| wildcard_match(pattern, resource)) +} + +fn conditions_match(conditions: &[Condition], context: &Map) -> bool { + for condition in conditions { + let actual = lookup_value(context, &condition.key); + let matches = match condition.operator { + ConditionOperator::StringEquals => actual + .and_then(Value::as_str) + .map(|value| condition.values.iter().any(|candidate| candidate == value)) + .unwrap_or(false), + ConditionOperator::StringLike => match actual.and_then(Value::as_str) { + Some(value) => condition + .values + .iter() + .any(|pattern| wildcard_match(pattern, value)), + None => false, + }, + ConditionOperator::Bool => match actual { + Some(Value::Bool(b)) => condition + .values + .iter() + .filter_map(|candidate| candidate.parse::().ok()) + .any(|expected| expected == *b), + _ => false, + }, + }; + + if !matches { + return false; + } + } + true +} + +fn wildcard_match(pattern: &str, value: &str) -> bool { + if pattern == "*" { + return true; + } + if !pattern.contains('*') { + return pattern == value; + } + + let mut remainder = value; + let mut parts = pattern + .split('*') + .filter(|part| !part.is_empty()) + .peekable(); + + if !pattern.starts_with('*') { + if let Some(prefix) = parts.next() { + if !remainder.starts_with(prefix) { + return false; + } + remainder = &remainder[prefix.len()..]; + } + } + + while let Some(part) = parts.next() { + if parts.peek().is_none() && !pattern.ends_with('*') { + return remainder.ends_with(part); + } + if let Some(index) = remainder.find(part) { + remainder = &remainder[index + part.len()..]; + } else { + return false; + } + } + + pattern.ends_with('*') || remainder.is_empty() +} + +fn lookup_value<'a>(root: &'a Map, key: &str) -> Option<&'a Value> { + let mut current: Option<&Value> = None; + for (index, segment) in key.split('.').enumerate() { + let next = if index == 0 { + root.get(segment)? + } else { + match current? { + Value::Object(map) => map.get(segment)?, + _ => return None, + } + }; + current = Some(next); + } + current +} diff --git a/iam/src/service.rs b/iam/src/service.rs new file mode 100644 index 00000000..4302685e --- /dev/null +++ b/iam/src/service.rs @@ -0,0 +1,208 @@ +//! Business logic façade consumed by HTTP handlers. + +use chrono::Utc; +use serde_json::{json, Map, Value}; + +use crate::attestation::verify_attestation; +use crate::config::IamConfig; +use crate::error::IamError; +use crate::models::{ + generate_id, Account, AccountResponse, AssumeRoleRequest, AssumeRoleResponse, + CreateAccountRequest, CreatePrincipalRequest, CreateRoleRequest, EvaluateRequest, + EvaluateResponse, Principal, PrincipalResponse, RegisterResourceRequest, Resource, + ResourceResponse, Role, RoleResponse, +}; +use crate::policy::evaluate_policy; +use crate::storage::Store; +use crate::token::TokenSigner; + +/// Public entry point that wires storage, policy evaluation and token signing. +pub struct IamService { + store: Store, + signer: TokenSigner, +} + +impl IamService { + /// Build a service instance using the provided configuration. + pub fn new(config: &IamConfig) -> Result { + let signer = TokenSigner::new(&config.crypto)?; + Ok(Self { + store: Store::default(), + signer, + }) + } + + /// Create a logical account boundary. + pub async fn create_account( + &self, + request: CreateAccountRequest, + ) -> Result { + let account = Account { + id: generate_id("acct"), + name: request.name, + labels: request.labels, + created_at: Utc::now(), + }; + let stored = self.store.insert_account(account).await?; + Ok(AccountResponse { account: stored }) + } + + /// Create a principal under a specific account. + pub async fn create_principal( + &self, + account_id: &str, + request: CreatePrincipalRequest, + ) -> Result { + // Ensure the parent account exists + let _ = self.store.get_account(account_id).await?; + let principal = Principal { + id: generate_id("prn"), + account_id: account_id.to_string(), + name: request.name, + principal_type: request.principal_type, + attributes: request.attributes, + created_at: Utc::now(), + }; + let stored = self.store.insert_principal(principal).await?; + Ok(PrincipalResponse { principal: stored }) + } + + /// Register a resource and return its derived ARN. + pub async fn register_resource( + &self, + request: RegisterResourceRequest, + ) -> Result { + let owner = self.store.get_account(&request.owner_account_id).await?; + let resource_id = generate_id("res"); + let arn = format!( + "arn:trustee::{}:{}/{}", + owner.id, request.resource_type, resource_id + ); + let resource = Resource { + arn, + owner_account_id: owner.id, + resource_type: request.resource_type, + tags: request.tags, + attributes: request.attributes, + created_at: Utc::now(), + }; + let stored = self.store.insert_resource(resource).await?; + Ok(ResourceResponse { resource: stored }) + } + + /// Create a role definition with both trust and access policies. + pub async fn create_role(&self, request: CreateRoleRequest) -> Result { + let role = Role { + id: generate_id("role"), + name: request.name, + description: request.description, + trust_policy: request.trust_policy, + access_policy: request.access_policy, + labels: request.labels, + created_at: Utc::now(), + }; + let stored = self.store.insert_role(role).await?; + Ok(RoleResponse { role: stored }) + } + + /// Evaluate trust policy + attestation and issue a session token. + pub async fn assume_role( + &self, + request: AssumeRoleRequest, + ) -> Result { + let AssumeRoleRequest { + principal_id, + role_id, + requested_duration_seconds, + session_name, + attestation_token, + context: assume_context, + } = request; + let principal = self.store.get_principal(&principal_id).await?; + let role = self.store.get_role(&role_id).await?; + let attestation_ctx = verify_attestation(attestation_token.as_deref())?; + let request_context = assume_context.unwrap_or_default(); + + let mut context = Map::new(); + context.insert("principal".to_string(), principal_to_value(&principal)); + context.insert("env".to_string(), Value::Object(attestation_ctx.to_env())); + context.insert( + "request".to_string(), + Value::Object(request_context.clone()), + ); + + let allowed = evaluate_policy(&role.trust_policy, "sts:AssumeRole", &role.id, &context); + if !allowed { + return Err(IamError::Unauthorized(format!( + "principal {} is not allowed to assume role {}", + principal.id, role.id + ))); + } + + let signed = self.signer.issue( + &principal, + &role, + attestation_ctx.to_env(), + request_context, + requested_duration_seconds, + session_name, + )?; + Ok(AssumeRoleResponse { + token: signed.token, + expires_at: signed.claims.expires_at(), + }) + } + + /// Evaluate access policy using a previously issued session token. + pub async fn evaluate(&self, request: EvaluateRequest) -> Result { + let EvaluateRequest { + token, + action, + resource, + context: eval_context, + } = request; + let claims = self.signer.verify(&token)?; + let role = self.store.get_role(&claims.role).await?; + let principal = self.store.get_principal(&claims.sub).await?; + let mut policy_context = Map::new(); + policy_context.insert("principal".to_string(), principal_to_value(&principal)); + policy_context.insert("env".to_string(), Value::Object(claims.env)); + if let Ok(resource_meta) = self.store.get_resource(&resource).await { + policy_context.insert("resource".to_string(), resource_to_value(&resource_meta)); + } + let request_context = eval_context.unwrap_or_default(); + policy_context.insert( + "request".to_string(), + json!({ + "action": action, + "resource": resource, + "context": Value::Object(request_context.clone()), + }), + ); + + let allowed = evaluate_policy(&role.access_policy, &action, &resource, &policy_context); + Ok(EvaluateResponse { allowed }) + } +} + +/// Convert a principal into a JSON object for policy contexts. +fn principal_to_value(principal: &Principal) -> Value { + json!({ + "id": principal.id, + "accountId": principal.account_id, + "name": principal.name, + "type": principal.principal_type, + "attributes": Value::Object(principal.attributes.values.clone()), + }) +} + +/// Convert a resource record into a JSON object for policy contexts. +fn resource_to_value(resource: &Resource) -> Value { + json!({ + "arn": resource.arn, + "ownerAccountId": resource.owner_account_id, + "resourceType": resource.resource_type, + "tags": resource.tags, + "attributes": Value::Object(resource.attributes.values.clone()), + }) +} diff --git a/iam/src/storage.rs b/iam/src/storage.rs new file mode 100644 index 00000000..aca9466f --- /dev/null +++ b/iam/src/storage.rs @@ -0,0 +1,110 @@ +//! Extremely simple in-memory storage used by the IAM MVP. + +use std::collections::HashMap; +use std::sync::Arc; + +use tokio::sync::RwLock; + +use crate::error::IamError; +use crate::models::{ + Account, AccountId, Principal, PrincipalId, Resource, ResourceArn, Role, RoleId, +}; + +/// Cloneable handle to the shared in-memory store. +#[derive(Clone, Default)] +pub struct Store { + accounts: Arc>>, + principals: Arc>>, + resources: Arc>>, + roles: Arc>>, +} + +impl Store { + /// Insert a new account if the identifier is unused. + pub async fn insert_account(&self, account: Account) -> Result { + let mut guard = self.accounts.write().await; + if guard.contains_key(&account.id) { + return Err(IamError::Conflict(format!( + "account {} already exists", + account.id + ))); + } + guard.insert(account.id.clone(), account.clone()); + Ok(account) + } + + /// Fetch an account by id. + pub async fn get_account(&self, account_id: &str) -> Result { + let guard = self.accounts.read().await; + guard + .get(account_id) + .cloned() + .ok_or_else(|| IamError::NotFound(format!("account {}", account_id))) + } + + /// Insert a new principal if not already present. + pub async fn insert_principal(&self, principal: Principal) -> Result { + let mut guard = self.principals.write().await; + if guard.contains_key(&principal.id) { + return Err(IamError::Conflict(format!( + "principal {} already exists", + principal.id + ))); + } + guard.insert(principal.id.clone(), principal.clone()); + Ok(principal) + } + + /// Fetch a principal by id. + pub async fn get_principal(&self, principal_id: &str) -> Result { + let guard = self.principals.read().await; + guard + .get(principal_id) + .cloned() + .ok_or_else(|| IamError::NotFound(format!("principal {}", principal_id))) + } + + /// Insert a new resource ARN entry. + pub async fn insert_resource(&self, resource: Resource) -> Result { + let mut guard = self.resources.write().await; + if guard.contains_key(&resource.arn) { + return Err(IamError::Conflict(format!( + "resource {} already exists", + resource.arn + ))); + } + guard.insert(resource.arn.clone(), resource.clone()); + Ok(resource) + } + + /// Fetch a resource by ARN. + pub async fn get_resource(&self, arn: &str) -> Result { + let guard = self.resources.read().await; + guard + .get(arn) + .cloned() + .ok_or_else(|| IamError::NotFound(format!("resource {}", arn))) + } + + /// Insert a new role definition. + pub async fn insert_role(&self, role: Role) -> Result { + let mut guard = self.roles.write().await; + if guard.contains_key(&role.id) { + return Err(IamError::Conflict(format!( + "role {} already exists", + role.id + ))); + } + guard.insert(role.id.clone(), role.clone()); + Ok(role) + } + + /// Fetch a role by id. + pub async fn get_role(&self, role_id: &str) -> Result { + let guard = self.roles.read().await; + guard + .get(role_id) + .cloned() + .ok_or_else(|| IamError::NotFound(format!("role {}", role_id))) + } +} diff --git a/iam/src/token.rs b/iam/src/token.rs new file mode 100644 index 00000000..11c8eec0 --- /dev/null +++ b/iam/src/token.rs @@ -0,0 +1,77 @@ +//! Token signing and verification utilities. + +use chrono::{Duration, Utc}; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; + +use crate::config::CryptoConfig; +use crate::error::IamError; +use crate::models::{Principal, Role, SessionClaims}; + +/// Wrapper around JWT HMAC helpers. +pub struct TokenSigner { + encoding: EncodingKey, + decoding: DecodingKey, + issuer: String, + default_ttl: Duration, +} + +/// Convenience struct returned by [`TokenSigner::issue`]. +pub struct SignedToken { + pub token: String, + pub claims: SessionClaims, +} + +impl TokenSigner { + /// Build a signer using the configured shared secret. + pub fn new(config: &CryptoConfig) -> Result { + let encoding = EncodingKey::from_secret(config.hmac_secret.as_bytes()); + let decoding = DecodingKey::from_secret(config.hmac_secret.as_bytes()); + Ok(Self { + encoding, + decoding, + issuer: config.issuer.clone(), + default_ttl: Duration::seconds(config.default_ttl_seconds as i64), + }) + } + + /// Issue a short-lived session token for a principal/role pair. + pub fn issue( + &self, + principal: &Principal, + role: &Role, + env: serde_json::Map, + custom: serde_json::Map, + requested_duration: Option, + session_name: Option, + ) -> Result { + let now = Utc::now(); + let duration = requested_duration + .map(|seconds| Duration::seconds(seconds as i64)) + .unwrap_or(self.default_ttl); + let claims = SessionClaims { + sub: principal.id.clone(), + tenant: principal.account_id.clone(), + role: role.id.clone(), + iss: self.issuer.clone(), + iat: now.timestamp(), + exp: (now + duration).timestamp(), + session_name, + env, + custom, + }; + + let token = encode(&Header::new(Algorithm::HS256), &claims, &self.encoding) + .map_err(|err| IamError::Internal(format!("failed to sign token: {err}")))?; + + Ok(SignedToken { token, claims }) + } + + /// Verify and decode a previously issued token. + pub fn verify(&self, token: &str) -> Result { + let mut validation = Validation::new(Algorithm::HS256); + validation.set_issuer(&[self.issuer.as_str()]); + decode::(token, &self.decoding, &validation) + .map(|data| data.claims) + .map_err(|err| IamError::Unauthorized(format!("invalid token: {err}"))) + } +} diff --git a/trustee-gateway/README.md b/trustee-gateway/README.md index 19171497..891d768b 100644 --- a/trustee-gateway/README.md +++ b/trustee-gateway/README.md @@ -1,10 +1,10 @@ # Trustee Gateway -Trustee Gateway 是 Trustee 项目的重要组件,它作为 KBS(Key Broker Service)和 RVPS(Reference Value Provider Service)等后端服务的 API 网关。Gateway 提供了统一的接入点、访问控制和审计功能,简化了 Trustee 系统的整体架构。 +Trustee Gateway 是 Trustee 项目的重要组件,它作为 KBS(Key Broker Service)、IAM(Identity & Access Management)和 RVPS(Reference Value Provider Service)等后端服务的 API 网关。Gateway 提供了统一的接入点、访问控制和审计功能,简化了 Trustee 系统的整体架构。 ## 功能特性 -- **统一接入点**:为 KBS 和 RVPS 等后端服务提供统一的 API 接入点 +- **统一接入点**:为 KBS、IAM 和 RVPS 等后端服务提供统一的 API 接入点 - **请求转发**:智能代理请求到相应的后端服务 - **访问控制**:可对 API 请求进行身份验证和授权 - **审计日志**:记录关键操作,包括证明请求和资源访问 @@ -46,6 +46,9 @@ server: kbs: url: "http://kbs:8080" # KBS 服务地址 +iam: + url: "http://iam:8090" # IAM 服务地址 + rvps: grpc_addr: "rvps:50003" # RVPS gRPC 服务地址 @@ -69,6 +72,15 @@ Gateway 提供以下主要 API 端点: - `/api/kbs/v0/resource-policy` - 资源策略管理 - `/api/kbs/v0/resource/:repository/:type/:tag` - 资源管理 +### IAM 相关 API + +- `/api/iam/accounts` - 账号管理 +- `/api/iam/accounts/{account_id}/principals` - 主体管理 +- `/api/iam/resources` - 资源注册 +- `/api/iam/roles` - 角色管理 +- `/api/iam/sts/assume-role` - 获取会话令牌 +- `/api/iam/authz/evaluate` - 鉴权评估 + ### RVPS 相关 API - `/api/rvps/*` - RVPS 服务代理 diff --git a/trustee-gateway/cmd/server/main.go b/trustee-gateway/cmd/server/main.go index 1579ef53..5bd9cdd8 100644 --- a/trustee-gateway/cmd/server/main.go +++ b/trustee-gateway/cmd/server/main.go @@ -69,6 +69,7 @@ func main() { kbsHandler := handlers.NewKBSHandler(p, auditRepo) rvpsHandler := handlers.NewRVPSHandler(p, rvpsClient) attestationServiceHandler := handlers.NewAttestationServiceHandler(p, auditRepo) + iamHandler := handlers.NewIAMHandler(p) auditHandler := handlers.NewAuditHandler(auditRepo) healthCheckHandler := handlers.NewHealthCheckHandler(p, rvpsClient) aaInstanceHandler := handlers.NewAAInstanceHandler(aaInstanceRepo, &cfg.AttestationAgentInstanceInfo) @@ -87,7 +88,7 @@ func main() { router.Use(middleware.Logger()) // API routes - setupRoutes(router, kbsHandler, rvpsHandler, attestationServiceHandler, auditHandler, healthCheckHandler, p, aaInstanceHandler) + setupRoutes(router, kbsHandler, rvpsHandler, attestationServiceHandler, iamHandler, auditHandler, healthCheckHandler, p, aaInstanceHandler) // Setup HTTP server addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) @@ -144,7 +145,7 @@ func main() { logrus.Info("Server shutdown complete") } -func setupRoutes(router *gin.Engine, kbsHandler *handlers.KBSHandler, rvpsHandler *handlers.RVPSHandler, attestationServiceHandler *handlers.AttestationServiceHandler, auditHandler *handlers.AuditHandler, healthCheckHandler *handlers.HealthCheckHandler, p *proxy.Proxy, aaInstanceHandler *handlers.AAInstanceHandler) { +func setupRoutes(router *gin.Engine, kbsHandler *handlers.KBSHandler, rvpsHandler *handlers.RVPSHandler, attestationServiceHandler *handlers.AttestationServiceHandler, iamHandler *handlers.IAMHandler, auditHandler *handlers.AuditHandler, healthCheckHandler *handlers.HealthCheckHandler, p *proxy.Proxy, aaInstanceHandler *handlers.AAInstanceHandler) { // KBS API routes kbs := router.Group("/api/kbs/v0") { @@ -203,6 +204,13 @@ func setupRoutes(router *gin.Engine, kbsHandler *handlers.KBSHandler, rvpsHandle rvps.Any("/*path", rvpsHandler.HandleRVPSRequest) } + // IAM API routes + iam := router.Group("/api/iam") + { + iam.Any("", iamHandler.HandleIAMProxy) + iam.Any("/*path", iamHandler.HandleIAMProxy) + } + // Audit routes audit := router.Group("/api/audit") { diff --git a/trustee-gateway/config.yaml b/trustee-gateway/config.yaml index 816eb076..ea8e2f9f 100644 --- a/trustee-gateway/config.yaml +++ b/trustee-gateway/config.yaml @@ -11,6 +11,11 @@ kbs: insecure_http: true ca_cert_file: "" +iam: + url: "http://localhost:8090" + insecure_http: true + ca_cert_file: "" + attestation_service: url: "http://localhost:50005" insecure_http: true diff --git a/trustee-gateway/internal/config/config.go b/trustee-gateway/internal/config/config.go index d92c2478..ca1bc2a4 100644 --- a/trustee-gateway/internal/config/config.go +++ b/trustee-gateway/internal/config/config.go @@ -10,6 +10,7 @@ type Config struct { Server ServerConfig `mapstructure:"server"` KBS ServiceConfig `mapstructure:"kbs"` AttestationService ServiceConfig `mapstructure:"attestation_service"` + IAM ServiceConfig `mapstructure:"iam"` RVPS RVPSConfig `mapstructure:"rvps"` Database DatabaseConfig `mapstructure:"database"` Logging LoggingConfig `mapstructure:"logging"` @@ -79,6 +80,7 @@ func LoadConfig(configPath string) (*Config, error) { viper.SetDefault("server.insecure_http", true) viper.SetDefault("kbs.url", "http://localhost:8080") viper.SetDefault("attestation_service.url", "http://localhost:50005") + viper.SetDefault("iam.url", "http://localhost:8090") viper.SetDefault("rvps.grpc_addr", "localhost:50003") viper.SetDefault("database.type", "sqlite") viper.SetDefault("database.path", "./trustee-gateway.db") diff --git a/trustee-gateway/internal/handlers/iam_handlers.go b/trustee-gateway/internal/handlers/iam_handlers.go new file mode 100644 index 00000000..722323c3 --- /dev/null +++ b/trustee-gateway/internal/handlers/iam_handlers.go @@ -0,0 +1,44 @@ +package handlers + +import ( + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/openanolis/trustee/gateway/internal/proxy" + "github.com/sirupsen/logrus" +) + +// IAMHandler proxies IAM API requests to the upstream Trustee IAM service. +type IAMHandler struct { + proxy *proxy.Proxy +} + +// NewIAMHandler creates a new IAM handler. +func NewIAMHandler(proxy *proxy.Proxy) *IAMHandler { + return &IAMHandler{proxy: proxy} +} + +// HandleIAMProxy forwards any IAM request to the upstream service. +func (h *IAMHandler) HandleIAMProxy(c *gin.Context) { + resp, err := h.proxy.ForwardToIAM(c) + if err != nil { + logrus.Errorf("Failed to forward IAM request: %v", err) + c.AbortWithStatusJSON(http.StatusBadGateway, gin.H{"error": "failed to forward IAM request"}) + return + } + defer resp.Body.Close() + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + logrus.Errorf("Failed to read IAM response: %v", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to read IAM response"}) + return + } + + proxy.CopyHeaders(c, resp) + proxy.CopyCookies(c, resp) + + c.Status(resp.StatusCode) + c.Writer.Write(responseBody) +} diff --git a/trustee-gateway/internal/proxy/proxy.go b/trustee-gateway/internal/proxy/proxy.go index b315b92d..b0ce5dd4 100644 --- a/trustee-gateway/internal/proxy/proxy.go +++ b/trustee-gateway/internal/proxy/proxy.go @@ -26,14 +26,18 @@ const ( KBSService ServiceType = "kbs" // AttestationServiceType represents the Attestation Service AttestationServiceType ServiceType = "attestation-service" + // IAMService represents the IAM service + IAMService ServiceType = "iam" ) // Proxy handles the forwarding of requests to backend services type Proxy struct { kbsURL *url.URL attestationServiceURL *url.URL // Added URL for Attestation Service + iamURL *url.URL kbsClient *http.Client attestationClient *http.Client + iamClient *http.Client } // NewProxy creates a new proxy instance @@ -48,6 +52,11 @@ func NewProxy(cfg *config.Config) (*Proxy, error) { return nil, fmt.Errorf("invalid Attestation Service URL: %w", err) } + iamURL, err := url.Parse(cfg.IAM.URL) + if err != nil { + return nil, fmt.Errorf("invalid IAM URL: %w", err) + } + // Create HTTP client for KBS kbsClient, err := createHTTPClient(&cfg.KBS) if err != nil { @@ -60,11 +69,18 @@ func NewProxy(cfg *config.Config) (*Proxy, error) { return nil, fmt.Errorf("failed to create Attestation Service client: %w", err) } + iamClient, err := createHTTPClient(&cfg.IAM) + if err != nil { + return nil, fmt.Errorf("failed to create IAM client: %w", err) + } + return &Proxy{ kbsURL: kbsURL, attestationServiceURL: attestationServiceURL, + iamURL: iamURL, kbsClient: kbsClient, attestationClient: attestationClient, + iamClient: iamClient, }, nil } @@ -115,6 +131,11 @@ func (p *Proxy) ForwardToAttestationService(c *gin.Context) (*http.Response, err return p.forwardRequest(c, AttestationServiceType) } +// ForwardToIAM forwards a request to the IAM service +func (p *Proxy) ForwardToIAM(c *gin.Context) (*http.Response, error) { + return p.forwardRequest(c, IAMService) +} + // RequestBodyBuffer is a buffer that records the request body while forwarding it type RequestBodyBuffer struct { *bytes.Buffer @@ -160,6 +181,8 @@ func (p *Proxy) forwardRequest(c *gin.Context, serviceType ServiceType) (*http.R targetURL = p.kbsURL case AttestationServiceType: targetURL = p.attestationServiceURL + case IAMService: + targetURL = p.iamURL default: return nil, fmt.Errorf("unknown service type: %s", serviceType) } @@ -180,6 +203,11 @@ func (p *Proxy) forwardRequest(c *gin.Context, serviceType ServiceType) (*http.R } else if strings.HasPrefix(targetPath, "/api/as") { targetPath = strings.TrimPrefix(targetPath, "/api/as") } + } else if serviceType == IAMService { + targetPath = strings.TrimPrefix(targetPath, "/api/iam") + if targetPath == "" { + targetPath = "/" + } } targetQuery := c.Request.URL.RawQuery @@ -244,6 +272,8 @@ func (p *Proxy) forwardRequest(c *gin.Context, serviceType ServiceType) (*http.R client = p.kbsClient case AttestationServiceType: client = p.attestationClient + case IAMService: + client = p.iamClient default: return nil, fmt.Errorf("unknown service type: %s", serviceType) } diff --git a/trustee-gateway/trustee_gateway_api.md b/trustee-gateway/trustee_gateway_api.md index 10af7a15..9a4ed6a2 100644 --- a/trustee-gateway/trustee_gateway_api.md +++ b/trustee-gateway/trustee_gateway_api.md @@ -2,52 +2,290 @@ ## 目录 -### [1. KBS API (`/api/kbs/v0`)](#kbs-api-apikbsv0) -- [1.1 认证 (Authentication)](#11-认证-authentication) -- [1.2 证明 (Attestation)](#12-证明-attestation) -- [1.3 设置认证策略 (Set Attestation Policy)](#13-设置认证策略-set-attestation-policy) -- [1.4 获取认证策略 (Get Attestation Policy)](#14-获取认证策略-get-attestation-policy) -- [1.5 删除认证策略 (Delete Attestation Policy)](#15-删除认证策略-delete-attestation-policy) -- [1.6 列出认证策略 (List Attestation Policies)](#16-列出认证策略-list-attestation-policies) -- [1.7 设置资源策略 (Set Resource Policy)](#17-设置资源策略-set-resource-policy) -- [1.8 获取资源策略 (Get Resource Policy)](#18-获取资源策略-get-resource-policy) -- [1.9 获取资源 (Get Resource)](#19-获取资源-get-resource) -- [1.10 设置资源 (Set Resource)](#110-设置资源-set-resource) -- [1.11 列出资源 (List Resources)](#111-列出资源-list-resources) -- [1.12 删除资源 (Delete Resource)](#112-删除资源-delete-resource) - -### [2. AS API (`/api/attestation-service`)](#as-api-apiattestation-service) -- [2.1 证明 (Attestation)](#21-证明-attestation-1) -- [2.2 挑战 (Challenge)](#22-挑战-challenge) -- [2.3 获取证书 (Get Certificate)](#23-获取证书-get-certificate) - -### [3. AS API (`/api/as`)](#as-api-apias) -- [3.1 证明 (Attestation)](#31-证明-attestation-2) -- [3.2 挑战 (Challenge)](#32-挑战-challenge-1) -- [3.3 获取证书 (Get Certificate)](#33-获取证书-get-certificate-1) - -### [4. RVPS API (`/api/rvps`)](#rvps-api-apirvps) -- [4.1 查询参考值 (Query Reference Value)](#41-查询参考值-query-reference-value) -- [4.2 注册参考值 (Register Reference Value)](#42-注册参考值-register-reference-value) -- [4.3 删除参考值 (Delete Reference Value)](#43-删除参考值-delete-reference-value) - -### [5. 审计 API (`/api/audit`)](#审计-api-apiaudit) -- [5.1 列出认证记录 (List Attestation Records)](#51-列出认证记录-list-attestation-records) -- [5.2 列出资源请求记录 (List Resource Requests)](#52-列出资源请求记录-list-resource-requests) - -### [6. 健康检查 API (`/api`)](#6-健康检查-api-api) -- [6.1 基本健康检查](#61-基本健康检查) -- [6.2 服务健康检查](#62-服务健康检查) - -### [7. 实例API (`/api/aa-instance`)](#7-实例api-apiaa-instance) -- [7.1 AA实例心跳](#71-aa实例心跳) -- [7.2 实例列表](#72-实例列表) +### [1. IAM API (`/api/iam`)](#iam-api-apiiam) +- [1.1 创建账号 (Create Account)](#11-创建账号-create-account) +- [1.2 创建主体 (Create Principal)](#12-创建主体-create-principal) +- [1.3 注册资源 (Register Resource)](#13-注册资源-register-resource) +- [1.4 创建角色 (Create Role)](#14-创建角色-create-role) +- [1.5 AssumeRole 获取会话令牌](#15-assumerole-获取会话令牌) +- [1.6 鉴权评估 (Access Evaluation)](#16-鉴权评估-access-evaluation) + +### [2. KBS API (`/api/kbs/v0`)](#kbs-api-apikbsv0) +- [2.1 认证 (Authentication)](#21-认证-authentication) +- [2.2 证明 (Attestation)](#22-证明-attestation) +- [2.3 设置认证策略 (Set Attestation Policy)](#23-设置认证策略-set-attestation-policy) +- [2.4 获取认证策略 (Get Attestation Policy)](#24-获取认证策略-get-attestation-policy) +- [2.5 删除认证策略 (Delete Attestation Policy)](#25-删除认证策略-delete-attestation-policy) +- [2.6 列出认证策略 (List Attestation Policies)](#26-列出认证策略-list-attestation-policies) +- [2.7 设置资源策略 (Set Resource Policy)](#27-设置资源策略-set-resource-policy) +- [2.8 获取资源策略 (Get Resource Policy)](#28-获取资源策略-get-resource-policy) +- [2.9 获取资源 (Get Resource)](#29-获取资源-get-resource) +- [2.10 设置资源 (Set Resource)](#210-设置资源-set-resource) +- [2.11 列出资源 (List Resources)](#211-列出资源-list-resources) +- [2.12 删除资源 (Delete Resource)](#212-删除资源-delete-resource) + +### [3. AS API (`/api/attestation-service`)](#as-api-apiattestation-service) +- [3.1 证明 (Attestation)](#31-证明-attestation-1) +- [3.2 挑战 (Challenge)](#32-挑战-challenge) +- [3.3 获取证书 (Get Certificate)](#33-获取证书-get-certificate) + +### [4. AS API (`/api/as`)](#as-api-apias) +- [4.1 证明 (Attestation)](#41-证明-attestation-2) +- [4.2 挑战 (Challenge)](#42-挑战-challenge-1) +- [4.3 获取证书 (Get Certificate)](#43-获取证书-get-certificate-1) + +### [5. RVPS API (`/api/rvps`)](#rvps-api-apirvps) +- [5.1 查询参考值 (Query Reference Value)](#51-查询参考值-query-reference-value) +- [5.2 注册参考值 (Register Reference Value)](#52-注册参考值-register-reference-value) +- [5.3 删除参考值 (Delete Reference Value)](#53-删除参考值-delete-reference-value) + +### [6. 审计 API (`/api/audit`)](#审计-api-apiaudit) +- [6.1 列出认证记录 (List Attestation Records)](#61-列出认证记录-list-attestation-records) +- [6.2 列出资源请求记录 (List Resource Requests)](#62-列出资源请求记录-list-resource-requests) + +### [7. 健康检查 API (`/api`)](#7-健康检查-api-api) +- [7.1 基本健康检查](#71-基本健康检查) +- [7.2 服务健康检查](#72-服务健康检查) + +### [8. 实例API (`/api/aa-instance`)](#8-实例api-apiaa-instance) +- [8.1 AA实例心跳](#81-aa实例心跳) +- [8.2 实例列表](#82-实例列表) ### [附录](#附录) - [KBS认证头生成方法](#kbs认证头生成方法) --- +### IAM API (`**/api/iam**`) + +Gateway 按原样代理所有 IAM 请求到 Trustee IAM 服务,接口语义与源服务完全一致。以下示例展示各端点的常见调用方式。 + +#### 1.1 创建账号 (Create Account) + +* **端点:** `POST /api/iam/accounts` +* **说明:** 创建新的租户账号,包含名称与可选标签。 +* **调用方法:** + ```bash + curl -X POST http://:8081/api/iam/accounts \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "example-account", + "labels": { "environment": "dev" } + }' + ``` +* **请求头:** `Content-Type: application/json` +* **请求体:** + ```json + { + "name": "string", + "labels": { "key": "value" } + } + ``` +* **响应:** 返回创建好的账号对象。 +* **返回码:** `201 Created`(成功) / `400 Bad Request`(字段缺失或非法) +* **返回示例:** + ```json + { + "account": { + "id": "acct-5f5c7a2b-...", + "name": "example-account", + "labels": { "environment": "dev" }, + "created_at": "2025-12-04T06:00:00Z" + } + } + ``` + +#### 1.2 创建主体 (Create Principal) + +* **端点:** `POST /api/iam/accounts/{account_id}/principals` +* **说明:** 在指定账号下新增主体(人、服务、运行时等)。 +* **调用方法:** + ```bash + curl -X POST http://:8081/api/iam/accounts/acct-123/principals \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "runtime-a", + "principal_type": "Runtime", + "attributes": { "values": { "cluster": "prod" } } + }' + ``` +* **请求头:** `Content-Type: application/json` +* **请求体:** + ```json + { + "name": "string", + "principal_type": "Human|Service|Runtime|External|Unknown", + "attributes": { + "values": { + "key": "value" + } + } + } + ``` +* **响应:** 返回创建成功的主体。 +* **返回码:** `201 Created` / `404 Not Found`(账号不存在) +* **返回示例:** + ```json + { + "principal": { + "id": "prn-123", + "account_id": "acct-123", + "name": "runtime-a", + "principal_type": "Runtime", + "attributes": { "values": { "cluster": "prod" } }, + "created_at": "2025-12-04T06:01:00Z" + } + } + ``` + +#### 1.3 注册资源 (Register Resource) + +* **端点:** `POST /api/iam/resources` +* **说明:** 按资源类型生成唯一 ARN,用于后续策略引用。 +* **调用方法:** + ```bash + curl -X POST http://:8081/api/iam/resources \ + -H 'Content-Type: application/json' \ + -d '{ + "owner_account_id": "acct-123", + "resource_type": "kbs/key", + "tags": { "tier": "gold" } + }' + ``` +* **请求头:** `Content-Type: application/json` +* **请求体:** + ```json + { + "owner_account_id": "acct-123", + "resource_type": "namespace/type", + "tags": { "key": "value" }, + "attributes": { "values": { "key": "value" } } + } + ``` +* **响应:** 包含生成的 `arn:trustee::...`。 +* **返回码:** `201 Created` / `404 Not Found`(账号不存在) +* **返回示例:** + ```json + { + "resource": { + "arn": "arn:trustee::acct-123:kbs/key/res-001", + "owner_account_id": "acct-123", + "resource_type": "kbs/key", + "tags": { "tier": "gold" }, + "created_at": "2025-12-04T06:02:00Z" + } + } + ``` + +#### 1.4 创建角色 (Create Role) + +* **端点:** `POST /api/iam/roles` +* **说明:** 同时写入信任策略与访问策略,供主体 `AssumeRole` 使用。 +* **调用方法:** + ```bash + curl -X POST http://:8081/api/iam/roles \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "trusted-runtime", + "trust_policy": { + "statements": [{ + "effect": "Allow", + "actions": ["sts:AssumeRole"], + "resources": ["role/*"], + "conditions": [{ + "operator": "StringEquals", + "key": "principal.accountId", + "values": ["acct-123"] + }] + }] + }, + "access_policy": { + "statements": [{ + "effect": "Allow", + "actions": ["kbs:GetKey"], + "resources": ["arn:trustee::acct-123:kbs/key/*"] + }] + } + }' + ``` +* **请求头:** `Content-Type: application/json` +* **请求体:** `name` + `trust_policy` + `access_policy`(PolicyDocument 结构) +* **响应:** 返回角色详情。 +* **返回码:** `201 Created` / `400 Bad Request` +* **返回示例:** + ```json + { + "role": { + "id": "role-abc", + "name": "trusted-runtime", + "created_at": "2025-12-04T06:03:00Z", + "trust_policy": { ... }, + "access_policy": { ... } + } + } + ``` + +#### 1.5 AssumeRole 获取会话令牌 + +* **端点:** `POST /api/iam/sts/assume-role` +* **说明:** 执行信任策略 + 可选 Attestation 校验,返回短期 STS Token。 +* **调用方法:** + ```bash + curl -X POST http://:8081/api/iam/sts/assume-role \ + -H 'Content-Type: application/json' \ + -d '{ + "principal_id": "prn-123", + "role_id": "role-abc", + "session_name": "runtime-session", + "attestation_token": "" + }' + ``` +* **请求头:** `Content-Type: application/json` +* **请求体:** `principal_id`、`role_id`、可选 `attestation_token`/`requested_duration_seconds`。 +* **响应:** 成功时返回 token 及过期时间。 +* **返回码:** `200 OK` / `401 Unauthorized` +* **返回示例:** + ```json + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "expires_at": "2025-12-04T08:00:00Z" + } + ``` + +#### 1.6 鉴权评估 (Access Evaluation) + +* **端点:** `POST /api/iam/authz/evaluate` +* **说明:** 校验 token 是否有效,并基于角色访问策略返回 Allow/Deny。 +* **调用方法:** + ```bash + curl -X POST http://:8081/api/iam/authz/evaluate \ + -H 'Content-Type: application/json' \ + -d '{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "action": "kbs:GetKey", + "resource": "arn:trustee::acct-123:kbs/key/key-001", + "context": { "ip": "10.0.0.5" } + }' + ``` +* **请求头:** `Content-Type: application/json` +* **请求体:** + ```json + { + "token": "session-token", + "action": "service:Verb", + "resource": "arn:trustee::acct-123:type/id", + "context": { "key": "value" } + } + ``` +* **响应:** `{ "allowed": true|false }` +* **返回码:** `200 OK` / `401 Unauthorized` +* **返回示例:** + ```json + { "allowed": true } + ``` + +--- + ### KBS API (`**/api/kbs/v0**`) 这部分 API 主要用于处理与 Key Broker Service (KBS) 相关的操作。Gateway 通常作为代理将请求转发给后端 KBS 服务,但在某些情况下(如策略和资源列表、审计日志)会直接与自己的数据库交互。