Skip to content

Commit 60f96a7

Browse files
committed
refactor(hosted-access): split HTTP authorization helpers
1 parent d2867df commit 60f96a7

4 files changed

Lines changed: 137 additions & 56 deletions

File tree

src/features/hosted-access/main/adapters/input/http/HostedAuthHttpController.ts

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
HOSTED_AUTH_HEADERS,
33
HOSTED_AUTH_ROUTES,
4-
type HostedAuthStatus,
54
type HostedPrincipal,
65
parseOidcLoginAttemptId,
76
parseOpaqueAuthoritySecret,
@@ -36,8 +35,14 @@ import {
3635
SESSION_COOKIE,
3736
} from '../../../../core/domain';
3837

38+
import { projectHostedAuthStatus } from './HostedAuthStatusProjection';
3939
import { applyHostedCapabilityAdvertisements } from './HostedCapabilityAdvertisement';
40+
import { setHostedCredentialCookies as setCookies } from './HostedCredentialCookies';
4041
import { HostedEventStreamRequestFenceRegistry } from './HostedEventStreamRequestFence';
42+
import {
43+
isHostedTeamWorkspaceAuthorized,
44+
isHostedTeamWorkspaceEventAuthorized,
45+
} from './HostedTeamWorkspaceEventAuthorization';
4146
import {
4247
captureHostedTeamWorkspaceGrantFence,
4348
type HostedRequestGrantFence,
@@ -188,41 +193,32 @@ export class HostedAuthHttpController {
188193
return context !== null && roleAllows(context.principal.role, 'hosted.query');
189194
}
190195
async isTeamWorkspaceAuthorized(request: unknown, teamId: TeamId): Promise<boolean> {
191-
const context = await this.liveRequestContext(request as HostedHttpRequest);
192-
if (!context) return false;
193-
return this.workspaceAccess
194-
.hasTeamWorkspaceGrant(
195-
context.principal.userId,
196-
teamId,
197-
this.dependencies.resolveTeamWorkspaceId
198-
)
199-
.catch(() => false);
196+
return isHostedTeamWorkspaceAuthorized({
197+
request: request as HostedHttpRequest,
198+
teamId,
199+
workspaceAccess: this.workspaceAccess,
200+
...(this.dependencies.resolveTeamWorkspaceId === undefined
201+
? {}
202+
: { resolveTeamWorkspaceId: this.dependencies.resolveTeamWorkspaceId }),
203+
liveRequestContext: (candidate) => this.liveRequestContext(candidate),
204+
});
200205
}
201206
async isTeamWorkspaceEventAuthorized(
202207
request: unknown,
203208
teamId: TeamId,
204209
runtimeWorkspaceId: string
205210
): Promise<boolean> {
206211
const hostedRequest = request as HostedHttpRequest;
207-
const context = await this.liveRequestContext(hostedRequest);
208-
if (context === null || !roleAllows(context.principal.role, 'hosted.events')) return false;
209-
try {
210-
const fence = await this.workspaceAccess.captureTeamWorkspaceGrantFence(
211-
context.principal.userId,
212-
teamId,
213-
this.dependencies.resolveTeamWorkspaceId
214-
);
215-
return (
216-
fence !== null &&
217-
fence.runtimeWorkspaceId === runtimeWorkspaceId &&
218-
(await this.workspaceAccess.revalidateTeamWorkspaceGrantFence(
219-
fence,
220-
this.dependencies.resolveTeamWorkspaceId
221-
))
222-
);
223-
} catch {
224-
return false;
225-
}
212+
return isHostedTeamWorkspaceEventAuthorized({
213+
request: hostedRequest,
214+
teamId,
215+
runtimeWorkspaceId,
216+
workspaceAccess: this.workspaceAccess,
217+
...(this.dependencies.resolveTeamWorkspaceId === undefined
218+
? {}
219+
: { resolveTeamWorkspaceId: this.dependencies.resolveTeamWorkspaceId }),
220+
liveRequestContext: (candidate) => this.liveRequestContext(candidate),
221+
});
226222
}
227223
async isHostedTaskMutationAuthorized(request: unknown, teamId?: TeamId): Promise<boolean> {
228224
const hostedRequest = request as HostedHttpRequest;
@@ -301,7 +297,7 @@ export class HostedAuthHttpController {
301297
'auth.personal.pair',
302298
'success'
303299
);
304-
this.setCredentialCookies(reply, result.value.sessionSecret, result.value.deviceSecret);
300+
setCookies(reply, result.value.sessionSecret, result.value.deviceSecret, this.dependencies);
305301
return this.status(result.value.principal, result.value.csrfToken);
306302
} catch (error) {
307303
const storageUnavailable =
@@ -687,10 +683,11 @@ export class HostedAuthHttpController {
687683
'auth.personal.renew',
688684
'success'
689685
);
690-
this.setCredentialCookies(
686+
setCookies(
691687
reply,
692688
result.context.sessionSecret,
693-
result.replacementDeviceSecret
689+
result.replacementDeviceSecret,
690+
this.dependencies
694691
);
695692
}
696693
const context = result.context;
@@ -757,34 +754,14 @@ export class HostedAuthHttpController {
757754
(fetchSite === undefined || fetchSite === 'same-origin' || fetchSite === 'same-site')
758755
);
759756
}
760-
private setCredentialCookies(
761-
reply: HostedHttpReply,
762-
sessionSecret: string,
763-
deviceSecret: string
764-
): void {
765-
reply.header('set-cookie', [
766-
cookie(SESSION_COOKIE, sessionSecret, {
767-
maxAge: this.dependencies.sessionMaxAgeSeconds,
768-
secure: this.dependencies.secureCookies,
769-
sameSite: 'Strict',
770-
}),
771-
cookie(DEVICE_COOKIE, deviceSecret, {
772-
maxAge: this.dependencies.deviceMaxAgeSeconds,
773-
secure: this.dependencies.secureCookies,
774-
sameSite: 'Strict',
775-
}),
776-
]);
777-
}
778-
private status(principal: HostedPrincipal | null, csrf: string | null): HostedAuthStatus {
779-
return Object.freeze({
757+
private status(principal: HostedPrincipal | null, csrfToken: string | null) {
758+
return projectHostedAuthStatus({
780759
mode: this.dependencies.mode,
781-
authenticated: principal !== null,
782760
principal,
783-
csrfToken: csrf,
761+
csrfToken,
784762
oidcProviderName:
785763
this.dependencies.oidc === null ? null : this.dependencies.authentication.displayName,
786-
deploymentId: (principal && this.dependencies.runtimeIdentity?.deploymentId) ?? null,
787-
bootId: (principal && this.dependencies.runtimeIdentity?.bootId) ?? null,
764+
runtimeIdentity: this.dependencies.runtimeIdentity,
788765
});
789766
}
790767
private admitOidcLogin(source: string): boolean {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { HostedAuthMode, HostedAuthStatus, HostedPrincipal } from '../../../../contracts';
2+
3+
export function projectHostedAuthStatus(options: {
4+
readonly mode: HostedAuthMode;
5+
readonly principal: HostedPrincipal | null;
6+
readonly csrfToken: string | null;
7+
readonly oidcProviderName: string | null;
8+
readonly runtimeIdentity?: { readonly deploymentId: string; readonly bootId: string } | null;
9+
}): HostedAuthStatus {
10+
return Object.freeze({
11+
mode: options.mode,
12+
authenticated: options.principal !== null,
13+
principal: options.principal,
14+
csrfToken: options.csrfToken,
15+
oidcProviderName: options.oidcProviderName,
16+
deploymentId: (options.principal && options.runtimeIdentity?.deploymentId) ?? null,
17+
bootId: (options.principal && options.runtimeIdentity?.bootId) ?? null,
18+
});
19+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
cookie,
3+
DEVICE_COOKIE,
4+
type HostedHttpReply,
5+
SESSION_COOKIE,
6+
} from '../../../../core/domain';
7+
8+
export function setHostedCredentialCookies(
9+
reply: HostedHttpReply,
10+
sessionSecret: string,
11+
deviceSecret: string,
12+
options: {
13+
readonly sessionMaxAgeSeconds: number;
14+
readonly deviceMaxAgeSeconds: number;
15+
readonly secureCookies: boolean;
16+
}
17+
): void {
18+
reply.header('set-cookie', [
19+
cookie(SESSION_COOKIE, sessionSecret, {
20+
maxAge: options.sessionMaxAgeSeconds,
21+
secure: options.secureCookies,
22+
sameSite: 'Strict',
23+
}),
24+
cookie(DEVICE_COOKIE, deviceSecret, {
25+
maxAge: options.deviceMaxAgeSeconds,
26+
secure: options.secureCookies,
27+
sameSite: 'Strict',
28+
}),
29+
]);
30+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { type HostedHttpRequest, roleAllows } from '../../../../core/domain';
2+
3+
import type {
4+
HostedAuthenticationContext,
5+
HostedTeamWorkspaceAttribution,
6+
HostedWorkspaceAccessService,
7+
} from '../../../../core/application';
8+
import type { TeamId } from '@shared/contracts/hosted';
9+
10+
export async function isHostedTeamWorkspaceEventAuthorized(options: {
11+
readonly request: HostedHttpRequest;
12+
readonly teamId: TeamId;
13+
readonly runtimeWorkspaceId: string;
14+
readonly workspaceAccess: HostedWorkspaceAccessService;
15+
readonly resolveTeamWorkspaceId?: (teamId: TeamId) => Promise<HostedTeamWorkspaceAttribution>;
16+
readonly liveRequestContext: (
17+
request: HostedHttpRequest
18+
) => Promise<HostedAuthenticationContext | null>;
19+
}): Promise<boolean> {
20+
const context = await options.liveRequestContext(options.request);
21+
if (context === null || !roleAllows(context.principal.role, 'hosted.events')) return false;
22+
try {
23+
const fence = await options.workspaceAccess.captureTeamWorkspaceGrantFence(
24+
context.principal.userId,
25+
options.teamId,
26+
options.resolveTeamWorkspaceId
27+
);
28+
return (
29+
fence !== null &&
30+
fence.runtimeWorkspaceId === options.runtimeWorkspaceId &&
31+
(await options.workspaceAccess.revalidateTeamWorkspaceGrantFence(
32+
fence,
33+
options.resolveTeamWorkspaceId
34+
))
35+
);
36+
} catch {
37+
return false;
38+
}
39+
}
40+
41+
export async function isHostedTeamWorkspaceAuthorized(options: {
42+
readonly request: HostedHttpRequest;
43+
readonly teamId: TeamId;
44+
readonly workspaceAccess: HostedWorkspaceAccessService;
45+
readonly resolveTeamWorkspaceId?: (teamId: TeamId) => Promise<HostedTeamWorkspaceAttribution>;
46+
readonly liveRequestContext: (
47+
request: HostedHttpRequest
48+
) => Promise<HostedAuthenticationContext | null>;
49+
}): Promise<boolean> {
50+
const context = await options.liveRequestContext(options.request);
51+
if (context === null) return false;
52+
return options.workspaceAccess
53+
.hasTeamWorkspaceGrant(context.principal.userId, options.teamId, options.resolveTeamWorkspaceId)
54+
.catch(() => false);
55+
}

0 commit comments

Comments
 (0)