fix(auth): enforce legacy grant role ceiling
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
88c2dff1b0
commit
2b427e7ed3
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue