Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BAUeCFpDkRD6tU5wGsc1C
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import { PlanFeature, planHasFeature } from '../value-objects/plan-feature.vo';
|
|
import { SubscriptionPlan, SubscriptionPlanType } from '../value-objects/subscription-plan.vo';
|
|
import { effectivePlan } from './subscription-access';
|
|
|
|
/**
|
|
* Politique d'acces aux capacites exposees par l'assistant et par le serveur MCP.
|
|
*
|
|
* Une capacite est une action du produit rendue appelable par un agent. Elle
|
|
* n'est pas decrite dans un prompt : elle est declaree ici avec ce qu'elle
|
|
* exige, et le controle a lieu dans le processus, sur l'identite authentifiee.
|
|
* Un modele peut se tromper de mot, il ne peut pas se donner un role.
|
|
*
|
|
* Deux conditions, verifiees dans cet ordre :
|
|
*
|
|
* 1. **Le role** — qui a le droit d'agir (`ADMIN`, `MANAGER`, `USER`...).
|
|
* 2. **L'offre** — ce que l'abonnement de l'organisation ouvre, via les memes
|
|
* `PLAN_FEATURES` que le reste du produit.
|
|
*
|
|
* L'offre effective passe par `effectivePlan` : un compte ADMIN dispose de
|
|
* Platinium, exactement comme dans l'apercu d'abonnement et dans l'assistant.
|
|
*/
|
|
|
|
/** `read` n'ecrit rien ; `write` modifie l'etat du produit. */
|
|
export type CapabilityScope = 'read' | 'write';
|
|
|
|
export interface CapabilityPolicy {
|
|
/** Identifiant stable, expose tel quel aux clients MCP. */
|
|
name: string;
|
|
scope: CapabilityScope;
|
|
/** Roles autorises. Absent : tout compte authentifie. */
|
|
roles?: readonly string[];
|
|
/** Fonctionnalite d'offre requise. Absent : aucune condition d'abonnement. */
|
|
feature?: PlanFeature;
|
|
}
|
|
|
|
export interface CapabilityActor {
|
|
id: string;
|
|
organizationId: string;
|
|
role?: string;
|
|
/** Adresse de l'appelant, reportee telle quelle dans le journal d'audit. */
|
|
email?: string;
|
|
/** Offre de l'organisation. Inconnue ou absente : Bronze. */
|
|
plan?: string;
|
|
}
|
|
|
|
/**
|
|
* Offre effective de l'appelant.
|
|
*
|
|
* Une valeur inconnue retombe sur Bronze, l'offre la plus restrictive, plutot
|
|
* que de faire echouer l'appel ou — pire — de l'autoriser par defaut.
|
|
*/
|
|
export function actorPlan(actor: CapabilityActor): SubscriptionPlanType {
|
|
let declared: SubscriptionPlan | null = null;
|
|
try {
|
|
if (actor.plan) declared = SubscriptionPlan.fromString(actor.plan);
|
|
} catch {
|
|
declared = null;
|
|
}
|
|
return effectivePlan(actor.role, declared).value;
|
|
}
|
|
|
|
export function canInvoke(actor: CapabilityActor, policy: CapabilityPolicy): boolean {
|
|
if (
|
|
policy.roles &&
|
|
!policy.roles.some(role => role.toLowerCase() === actor.role?.toLowerCase())
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (policy.feature && !planHasFeature(actorPlan(actor), policy.feature)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Filtre un catalogue pour un appelant.
|
|
*
|
|
* Une capacite hors de ses droits n'est pas seulement refusee a l'appel : elle
|
|
* n'apparait pas dans la liste. Un agent ne peut pas proposer, ni meme
|
|
* mentionner, une action que la personne n'a pas le droit de declencher.
|
|
*/
|
|
export function grantedCapabilities<T extends { policy: CapabilityPolicy }>(
|
|
actor: CapabilityActor,
|
|
capabilities: readonly T[]
|
|
): T[] {
|
|
return capabilities.filter(capability => canInvoke(actor, capability.policy));
|
|
}
|
|
|
|
/** Raison du refus, destinee au message d'erreur rendu a l'agent. */
|
|
export function denialReason(
|
|
actor: CapabilityActor,
|
|
policy: CapabilityPolicy
|
|
): 'role' | 'plan' | null {
|
|
if (canInvoke(actor, policy)) return null;
|
|
if (policy.roles && !policy.roles.some(r => r.toLowerCase() === actor.role?.toLowerCase())) {
|
|
return 'role';
|
|
}
|
|
return 'plan';
|
|
}
|