Browse Source

Add scopes to access

pull/7603/head
Thomas Kaul 2 weeks ago
parent
commit
7337084eb0
  1. 6
      apps/api/src/app/access/access.controller.ts
  2. 54
      libs/common/src/lib/scopes.spec.ts
  3. 31
      libs/common/src/lib/scopes.ts
  4. 4
      prisma/migrations/20260815120000_added_scopes_to_access/migration.sql

6
apps/api/src/app/access/access.controller.ts

@ -99,7 +99,10 @@ export class AccessController {
? { connect: { id: data.granteeUserId } }
: undefined,
permissions: data.permissions,
scopes: getScopesOfAccess({ permissions: data.permissions }),
scopes: getScopesOfAccess({
granteeUserId: data.granteeUserId,
permissions: data.permissions
}),
settings: this.accessService.buildSettings(data.filters),
user: { connect: { id: this.request.user.id } }
});
@ -170,6 +173,7 @@ export class AccessController {
: { disconnect: true },
permissions: data.permissions,
scopes: getScopesOfAccess({
granteeUserId: data.granteeUserId,
permissions: data.permissions ?? originalAccess.permissions
}),
settings: this.accessService.buildSettings(data.filters)

54
libs/common/src/lib/scopes.spec.ts

@ -11,6 +11,7 @@ describe('Scopes', () => {
it('Scopes take precedence over the permissions', () => {
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ'],
scopes: [scopes.portfolioRead]
})
@ -20,18 +21,65 @@ describe('Scopes', () => {
it('Derive from the permission to read', () => {
// An access created before the scopes have been introduced has no scopes
expect(
getScopesOfAccess({ permissions: ['READ'], scopes: [] })
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ'],
scopes: []
})
).toContain(scopes.portfolioReadValues);
});
it('Derive from the permission to read restricted', () => {
expect(
getScopesOfAccess({ permissions: ['READ_RESTRICTED'], scopes: [] })
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d',
permissions: ['READ_RESTRICTED'],
scopes: []
})
).not.toContain(scopes.portfolioReadValues);
});
it('Without permissions and scopes', () => {
expect(getScopesOfAccess({})).not.toContain(scopes.portfolioReadValues);
expect(
getScopesOfAccess({
granteeUserId: 'ffb08949-2f8a-4b6e-88fd-0f1e6b6b5f5d'
})
).not.toContain(scopes.portfolioReadValues);
});
});
describe('Get scopes of public access', () => {
it('Allows reading the portfolio', () => {
expect(getScopesOfAccess({ permissions: ['READ_RESTRICTED'] })).toContain(
scopes.portfolioRead
);
});
it('Excludes the accounts and the watchlist', () => {
const scopesOfAccess = getScopesOfAccess({
permissions: ['READ_RESTRICTED']
});
expect(scopesOfAccess).not.toContain(scopes.accountRead);
expect(scopesOfAccess).not.toContain(scopes.watchlistRead);
});
it('Cannot be widened by the scopes', () => {
expect(
getScopesOfAccess({
scopes: [
scopes.portfolioRead,
scopes.portfolioReadValues,
scopes.watchlistRead
]
})
).toEqual([scopes.portfolioRead]);
});
it('Cannot be widened by the permission to read', () => {
expect(getScopesOfAccess({ permissions: ['READ'] })).not.toContain(
scopes.portfolioReadValues
);
});
});

31
libs/common/src/lib/scopes.ts

@ -14,6 +14,11 @@ export const scopes = {
watchlistRead: 'watchlist:read'
} as const;
const SCOPES_OF_PUBLIC_ACCESS: string[] = [
scopes.activityRead,
scopes.portfolioRead
];
const SCOPES_OF_READ_RESTRICTED_ACCESS = [
scopes.accountRead,
scopes.activityRead,
@ -27,21 +32,33 @@ const SCOPES_OF_READ_ACCESS = [
];
export function getScopesOfAccess({
granteeUserId,
permissions,
scopes
}: {
granteeUserId?: string | null;
permissions?: AccessPermission[];
scopes?: string[];
}): string[] {
if (scopes?.length) {
return scopes;
let scopesOfAccess = scopes;
if (!scopesOfAccess?.length) {
// TODO: Remove the derivation from the permissions once they have been
// dropped from the access
scopesOfAccess = permissions?.includes('READ')
? SCOPES_OF_READ_ACCESS
: SCOPES_OF_READ_RESTRICTED_ACCESS;
}
if (granteeUserId) {
return scopesOfAccess;
}
// TODO: Remove the derivation from the permissions once they have been
// dropped from the access
return permissions?.includes('READ')
? SCOPES_OF_READ_ACCESS
: SCOPES_OF_READ_RESTRICTED_ACCESS;
// An access which has not been granted to a user is public, hence it is
// narrowed to the scopes exposed by the public endpoints
return scopesOfAccess.filter((scope) => {
return SCOPES_OF_PUBLIC_ACCESS.includes(scope);
});
}
/**

4
prisma/migrations/20260815120000_added_scopes_to_access/migration.sql

@ -4,6 +4,10 @@ ALTER TABLE "Access" ADD COLUMN "scopes" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- Derive the scopes from the permissions of the existing accesses
UPDATE "Access"
SET "scopes" = CASE
WHEN "granteeUserId" IS NULL THEN ARRAY[
'activity:read',
'portfolio:read'
]
WHEN 'READ' = ANY("permissions") THEN ARRAY[
'account:read',
'activity:read',

Loading…
Cancel
Save