aps-agent/server/core/policy/engine.ts

74 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 策略执行(任务 A4.2):在一次调用真正落到 Provider 之前,给出允许 / 拒绝 / 需审批。
*
* 三条不变量:
* 1. 只返回这三种结论,没有第四种「平台替业务系统放行」
* 2. **平台允许不能覆盖业务系统的拒绝**:本层只做「能不能提交」,业务系统说不行就是不行,
* 结果由 Provider 的回执决定,Core 不把 forbidden 改写成成功,也不自动重试写操作
* 3. 未绑定身份默认拒绝——不做「拿共享账号顶替」这种隐式降级(架构设计 4.1)
*
* 判定顺序固定(先可见性、再身份、再会话范围、最后动作分级),顺序变了结论就会变。
* 这一层是可替换的:接 OPA 之类通用策略引擎时实现同一个 PolicyEngine 接口即可。
*/
import type { Capability } from '@aps/provider-sdk';
import type { AccountBinding } from '../identity/context.js';
export type PolicyDecisionKind = 'allow' | 'deny' | 'require-approval';
export interface PolicyDecision {
decision: PolicyDecisionKind;
/** 机器可读的原因码:界面按它选提示,日志与审计按它聚合 */
code: string;
/** 给人看的一句话原因 */
reason: string;
}
export interface PolicyInput {
subject: { userId: string; tenantId: string };
capability: Capability;
providerId: string;
/** 平台用户在该系统的账号绑定;未绑定为 undefined */
binding: AccountBinding | undefined;
/** 能力可见性结果(A3.5 的白名单与只读模式) */
visible: boolean;
/** 该系统是否需要平台用户绑定账号(来自 Provider 声明);不需要时跳过绑定检查 */
requiresIdentity: boolean;
/** 会话所属项目;个人会话为 null(ADR-0003:写操作必须在项目会话内) */
projectId: string | null;
}
export interface PolicyEngine {
decide(input: PolicyInput): PolicyDecision;
}
const ALLOW = (code: string, reason: string): PolicyDecision => ({ decision: 'allow', code, reason });
const DENY = (code: string, reason: string): PolicyDecision => ({ decision: 'deny', code, reason });
/** 首版规则:可见性 → 身份绑定 → 会话范围 → 动作分级 */
export function createRulePolicy(): PolicyEngine {
return {
decide(input: PolicyInput): PolicyDecision {
if (!input.visible) {
return DENY('policy.capability_not_visible', `当前身份看不到能力 ${input.capability.id}`);
}
if (input.requiresIdentity && !input.binding) {
return DENY('policy.identity_unbound', `还没有绑定 ${input.providerId} 的系统账号,先绑定再调用`);
}
if (input.binding?.status === 'disabled') {
return DENY('policy.identity_disabled', `${input.providerId} 的系统账号已停用`);
}
if (input.capability.effect === 'write' && input.projectId === null) {
return DENY('policy.write_requires_project', '写操作必须在项目会话里发起(个人会话只读)');
}
if (input.capability.effect === 'write') {
return {
decision: 'require-approval',
code: 'policy.write_requires_confirmation',
reason: `写操作 ${input.capability.id} 需要用户确认后才执行`,
};
}
return ALLOW(input.capability.effect === 'draft' ? 'policy.draft_allowed' : 'policy.allowed', '平台侧策略允许');
},
};
}