fix(auth): enforce legacy grant role ceiling

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
cryppadotta 2026-09-12 15:14:13 +00:00
parent 88c2dff1b0
commit 2b427e7ed3
2 changed files with 67 additions and 2 deletions

View File

@ -227,6 +227,57 @@ describeEmbeddedPostgres("authorization service", () => {
expect(decision.explanation).toContain("Allowed by explicit grant tasks:assign");
});
it("limits ambiguous legacy user grants to the active membership role", async () => {
const company = await createCompany(db, "LegacyGrantRoleCeiling");
const userId = `user-${randomUUID()}`;
await db.insert(companyMemberships).values({
companyId: company.id,
principalType: "user",
principalId: userId,
status: "active",
membershipRole: "admin",
});
await db.insert(principalPermissionGrants).values({
companyId: company.id,
principalType: "user",
principalId: userId,
permissionKey: "tools:admin",
grantOrigin: "legacy_unknown",
});
const service = authorizationService(db);
await expect(service.decidePrincipalGrant({
companyId: company.id,
principalType: "user",
principalId: userId,
action: "tools:admin",
permissionKey: "tools:admin",
})).resolves.toMatchObject({ allowed: true, reason: "allow_role_default" });
await db
.update(companyMemberships)
.set({ membershipRole: "operator" })
.where(and(
eq(companyMemberships.companyId, company.id),
eq(companyMemberships.principalType, "user"),
eq(companyMemberships.principalId, userId),
));
await expect(service.decidePrincipalGrant({
companyId: company.id,
principalType: "user",
principalId: userId,
action: "tools:admin",
permissionKey: "tools:admin",
})).resolves.toMatchObject({ allowed: false, reason: "deny_missing_grant" });
await expect(db.select().from(principalPermissionGrants).where(and(
eq(principalPermissionGrants.companyId, company.id),
eq(principalPermissionGrants.principalId, userId),
))).resolves.toEqual([
expect.objectContaining({ permissionKey: "tools:admin", grantOrigin: "legacy_unknown" }),
]);
});
it("allows suggest grants to read peer agent configuration", async () => {
const company = await createCompany(db, "AgentReadGrant");
const actorAgent = await createAgent(db, company.id);

View File

@ -697,6 +697,18 @@ export function authorizationService(db: Db | DbTransaction) {
});
}
const legacyGrantMatchesCurrentRole = grant.grantOrigin !== "legacy_unknown"
|| input.principalType !== "user"
|| grantsForHumanRole(normalizeHumanRole(membership.membershipRole, "operator"))
.some((defaultGrant) => defaultGrant.permissionKey === input.permissionKey);
if (!legacyGrantMatchesCurrentRole) {
return deny({
action: input.action,
reason: "deny_missing_grant",
explanation: `Missing permission: ${input.permissionKey}.`,
});
}
if (
!(await scopeAllows(db, input.companyId, grant.scope, input.scope, {
requireStructuredScope: input.permissionKey === "tasks:assign_scope",
@ -717,8 +729,10 @@ export function authorizationService(db: Db | DbTransaction) {
return allow({
action: input.action,
reason: "allow_explicit_grant",
explanation: `Allowed by explicit grant ${input.permissionKey}.`,
reason: grant.grantOrigin === "legacy_unknown" ? "allow_role_default" : "allow_explicit_grant",
explanation: grant.grantOrigin === "legacy_unknown"
? `Allowed by the ${membership.membershipRole ?? "operator"} membership role.`
: `Allowed by explicit grant ${input.permissionKey}.`,
grant: {
principalType: input.principalType,
principalId: input.principalId,