Skip to content

Commit fe4a1f3

Browse files
authored
Refactor Session.roles: scopeless set (#3441)
2 parents 3041062 + 249b14d commit fe4a1f3

File tree

16 files changed

+58
-116
lines changed

16 files changed

+58
-116
lines changed

src/components/authorization/dto/role.dto.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
import { type Role } from '~/common';
22

3-
export type ProjectScope = 'project';
4-
export type GlobalScope = 'global';
5-
6-
// Scope for roles. Does this role apply anywhere or only with project membership?
7-
export type AuthScope = GlobalScope | ProjectScope;
8-
9-
export type ProjectScopedRole = `${ProjectScope}:${Role}`;
10-
export type GlobalScopedRole = `${GlobalScope}:${Role}`;
3+
type AuthScope = 'global' | 'project';
114

125
export type ScopedRole = `${AuthScope}:${Role}` | 'member:true';
136

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { CanImpersonateEvent } from '~/core/authentication/events/can-impersonate.event';
22
import { EventsHandler } from '~/core/events';
3-
import { withoutScope } from '../dto';
43
import { AssignableRoles } from '../dto/assignable-roles.dto';
54
import { Privileges } from '../policy';
65

@@ -10,9 +9,9 @@ export class CanImpersonateHandler {
109

1110
handle(event: CanImpersonateEvent) {
1211
const p = this.privileges.for(AssignableRoles);
13-
const valid = event.session.roles.every((role) =>
14-
p.can('edit', withoutScope(role)),
15-
);
12+
const valid = event.session.roles
13+
.values()
14+
.every((role) => p.can('edit', role));
1615
event.allow.vote(valid);
1716
}
1817
}

src/components/authorization/policies/conditions/role.condition.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { inspect } from 'util';
22
import { type Role } from '~/common';
3-
import { withoutScope } from '../../dto';
43
import {
54
type AsEdgeQLParams,
65
type Condition,
@@ -13,7 +12,7 @@ export class RoleCondition implements Condition {
1312
constructor(readonly allowed: ReadonlySet<Role>) {}
1413

1514
isAllowed({ session }: IsAllowedParams<any>) {
16-
const given = session.roles.map(withoutScope);
15+
const given = session.roles.values();
1716
return given.some((role) => this.allowed.has(role));
1817
}
1918

src/components/authorization/policy/executor/policy-executor.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { forwardRef, Inject, Injectable } from '@nestjs/common';
22
import { CachedByArg } from '@seedcompany/common';
3-
import { identity, intersection } from 'lodash';
3+
import { identity } from 'lodash';
44
import { type EnhancedResource } from '~/common';
55
import { Identity, type Session } from '~/core/authentication';
66
import { type QueryFragment } from '~/core/database/query';
7-
import { withoutScope } from '../../dto';
87
import { RoleCondition } from '../../policies/conditions/role.condition';
98
import { type Permission } from '../builder/perm-granter';
109
import {
@@ -148,8 +147,8 @@ export class PolicyExecutor {
148147
}
149148

150149
const roleCondition =
151-
policy.roles && policy.roles.length > 0
152-
? new RoleCondition(new Set(policy.roles))
150+
policy.roles && policy.roles.size > 0
151+
? new RoleCondition(policy.roles)
153152
: undefined;
154153

155154
if (!roleCondition && condition === true) {
@@ -280,11 +279,10 @@ export class PolicyExecutor {
280279
if (!policy.roles) {
281280
return true; // policy doesn't limit roles
282281
}
283-
const rolesSpecifiedByPolicyThatUserHas = intersection(
284-
policy.roles,
285-
session.roles.map(withoutScope),
282+
const rolesSpecifiedByPolicyThatUserHas = policy.roles.intersection(
283+
session.roles,
286284
);
287-
return rolesSpecifiedByPolicyThatUserHas.length > 0;
285+
return rolesSpecifiedByPolicyThatUserHas.size > 0;
288286
});
289287
return policies;
290288
}

src/components/authorization/policy/policy.factory.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
DiscoveryService,
44
} from '@golevelup/nestjs-discovery';
55
import { Injectable, type OnModuleInit } from '@nestjs/common';
6-
import { entries, mapEntries, mapValues } from '@seedcompany/common';
6+
import { entries, mapEntries, mapValues, setOf } from '@seedcompany/common';
77
import { pick, startCase } from 'lodash';
88
import { type DeepWritable, type Writable } from 'ts-essentials';
99
import { type EnhancedResource, many, type Role } from '~/common';
@@ -35,7 +35,7 @@ export interface Policy {
3535
/* Policy Name */
3636
name: string;
3737
/* Only applies to these roles */
38-
roles?: readonly Role[];
38+
roles?: ReadonlySet<Role>;
3939
/* What the policy grants */
4040
grants: Grants;
4141
/* An optimization to determine Powers this policy contains */
@@ -103,7 +103,7 @@ export class PolicyFactory implements OnModuleInit {
103103
): PlainPolicy {
104104
const name = startCase(discoveredClass.name.replace(/Policy$/, ''));
105105

106-
const roles = meta.role === 'all' ? undefined : many(meta.role);
106+
const roles = meta.role === 'all' ? undefined : setOf(many(meta.role));
107107

108108
const grants: WritableGrants = new Map();
109109
const resultList = many(meta.def(resGranter)).flat();

src/components/engagement/engagement-status.resolver.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export class EngagementStatusResolver {
2020
}
2121
return await this.engagementRules.getAvailableTransitions(
2222
status.parentId,
23-
undefined,
2423
status.changeset,
2524
);
2625
}
@@ -31,7 +30,7 @@ export class EngagementStatusResolver {
3130
and change the status to any other status?
3231
`,
3332
})
34-
async canBypassTransitions(): Promise<boolean> {
35-
return await this.engagementRules.canBypassWorkflow();
33+
canBypassTransitions(): boolean {
34+
return this.engagementRules.canBypassWorkflow();
3635
}
3736
}

src/components/engagement/engagement.rules.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable no-case-declarations */
22
import { Injectable } from '@nestjs/common';
3+
import { setOf } from '@seedcompany/common';
34
import { node, relation } from 'cypher-query-builder';
45
import { first, intersection } from 'lodash';
56
import {
@@ -12,7 +13,6 @@ import { ILogger, Logger } from '~/core';
1213
import { Identity } from '~/core/authentication';
1314
import { DatabaseService } from '~/core/database';
1415
import { ACTIVE, INACTIVE } from '~/core/database/query';
15-
import { withoutScope } from '../authorization/dto';
1616
import { ProjectStep } from '../project/dto';
1717
import {
1818
EngagementStatus,
@@ -29,7 +29,7 @@ interface StatusRule {
2929
transitions: Transition[];
3030
}
3131

32-
const rolesThatCanBypassWorkflow: Role[] = [Role.Administrator];
32+
const rolesThatCanBypassWorkflow = setOf<Role>([Role.Administrator]);
3333

3434
@Injectable()
3535
export class EngagementRules {
@@ -314,7 +314,6 @@ export class EngagementRules {
314314

315315
async getAvailableTransitions(
316316
engagementId: ID,
317-
currentUserRoles?: Role[],
318317
changeset?: ID,
319318
): Promise<EngagementStatusTransition[]> {
320319
const session = this.identity.current;
@@ -330,8 +329,7 @@ export class EngagementRules {
330329
);
331330

332331
// If current user is not an approver (based on roles) then don't allow any transitions
333-
currentUserRoles ??= session.roles.map(withoutScope);
334-
if (intersection(approvers, currentUserRoles).length === 0) {
332+
if (session.roles.intersection(setOf(approvers)).size === 0) {
335333
return [];
336334
}
337335

@@ -356,28 +354,22 @@ export class EngagementRules {
356354
return availableTransitionsAccordingToProject;
357355
}
358356

359-
async canBypassWorkflow() {
360-
const session = this.identity.current;
361-
const roles = session.roles.map(withoutScope);
362-
return intersection(rolesThatCanBypassWorkflow, roles).length > 0;
357+
canBypassWorkflow() {
358+
const { roles } = this.identity.current;
359+
return roles.intersection(rolesThatCanBypassWorkflow).size > 0;
363360
}
364361

365362
async verifyStatusChange(
366363
engagementId: ID,
367364
nextStatus: EngagementStatus,
368365
changeset?: ID,
369366
) {
370-
// If current user's roles include a role that can bypass workflow
371-
// stop the check here.
372-
const session = this.identity.current;
373-
const currentUserRoles = session.roles.map(withoutScope);
374-
if (intersection(rolesThatCanBypassWorkflow, currentUserRoles).length > 0) {
367+
if (this.canBypassWorkflow()) {
375368
return;
376369
}
377370

378371
const transitions = await this.getAvailableTransitions(
379372
engagementId,
380-
currentUserRoles,
381373
changeset,
382374
);
383375

src/components/project/project.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { Identity } from '~/core/authentication';
2525
import { Transactional } from '~/core/database';
2626
import { type AnyChangesOf } from '~/core/database/changes';
2727
import { Privileges } from '../authorization';
28-
import { withoutScope } from '../authorization/dto';
2928
import { BudgetService } from '../budget';
3029
import { BudgetStatus, type SecuredBudget } from '../budget/dto';
3130
import { EngagementService } from '../engagement';
@@ -161,8 +160,9 @@ export class ProjectService {
161160
{
162161
userId: session.userId,
163162
roles: session.roles
164-
.map(withoutScope)
165-
.filter((role) => Role.applicableToProjectMembership.has(role)),
163+
.values()
164+
.filter((role) => Role.applicableToProjectMembership.has(role))
165+
.toArray(),
166166
projectId: project,
167167
},
168168
false,

src/core/authentication/authentication.gel.repository.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
22
import { IntegrityError } from 'gel';
33
import { type ID, type PublicOf, ServerException } from '~/common';
44
import { RootUserAlias } from '../config/root-user.config';
5-
import { DbTraceLayer, disableAccessPolicies, e, Gel, withScope } from '../gel';
5+
import { DbTraceLayer, disableAccessPolicies, e, Gel } from '../gel';
66
import type { AuthenticationRepository } from './authentication.repository';
77
import { type LoginInput } from './dto';
88
import { type Session } from './session/session.dto';
@@ -138,8 +138,8 @@ export class AuthenticationGelRepository
138138
return e.select(e.Auth.Session, (session) => ({
139139
filter_single: { token },
140140
userId: session.user.id,
141-
roles: withScope('global', session.user.roles),
142-
impersonateeRoles: withScope('global', impersonatee.roles),
141+
roles: session.user.roles,
142+
impersonateeRoles: impersonatee.roles,
143143
}));
144144
},
145145
);
@@ -151,7 +151,7 @@ export class AuthenticationGelRepository
151151
{ userId: e.uuid },
152152
({ userId }) => {
153153
const user = e.cast(e.User, userId);
154-
return withScope('global', user.roles);
154+
return user.roles;
155155
},
156156
);
157157

src/core/authentication/authentication.repository.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { Injectable } from '@nestjs/common';
22
import { node, relation } from 'cypher-query-builder';
33
import { DateTime } from 'luxon';
4-
import { type ID, ServerException } from '~/common';
5-
import { type ScopedRole } from '../../components/authorization/dto';
4+
import { type ID, type Role, ServerException } from '~/common';
65
import { DatabaseService, DbTraceLayer, OnIndex } from '../database';
76
import {
87
ACTIVE,
@@ -201,8 +200,8 @@ export class AuthenticationRepository {
201200
)
202201
.return<{
203202
userId: ID | null;
204-
roles: readonly ScopedRole[];
205-
impersonateeRoles: readonly ScopedRole[] | null;
203+
roles: readonly Role[];
204+
impersonateeRoles: readonly Role[] | null;
206205
}>([
207206
'user.id as userId',
208207
'roles',

0 commit comments

Comments
 (0)