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

77 lines
3.5 KiB
TypeScript
Raw Normal View History

/**
* 策略执行(任务 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 === 'expired') {
return DENY('policy.identity_expired', `${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', '平台侧策略允许');
},
};
}