pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/lib/access.server.ts

101 lines4,024 bytesCodeBlame
1import { data } from "react-router";
2
3import {
4 type Capability,
5 type Repo,
6 type Result,
7 type Viewer,
8 abilities,
9 can,
10 granted,
11 httpStatus,
12 needs,
13 permission,
14} from "@g1t/contracts";
15
16import type { ViewerAccess } from "./access";
17import { repos } from "./services.server";
18import { getViewer } from "./session.server";
19
20type Context = Parameters<typeof getViewer>[0];
21type RepoParams = { owner?: string; repo?: string };
22
23/**
24 * One lookup of a repository per request: the repository's layout and the
25 * page under it load at the same time and both need it.
26 */
27const lookups = new WeakMap<object, Map<string, Promise<Result<Repo>>>>();
28
29export function repoFor(context: Context, params: RepoParams): Promise<Result<Repo>> {
30 const key = `${params.owner}/${params.repo}`.toLowerCase();
31 let seen = lookups.get(context);
32 if (!seen) lookups.set(context, (seen = new Map()));
33 let found = seen.get(key);
34 if (!found) {
35 found = repos.get({ namespace: params.owner ?? "", name: params.repo ?? "" }, getViewer(context));
36 seen.set(key, found);
37 }
38 return found;
39}
40
41/** The viewer's role on a repository and what it lets them do. */
42export function accessFor(viewer: Viewer, repo: Repo): ViewerAccess {
43 return {
44 role: permission(viewer, repo),
45 // A role of their own, not only because the repository is public.
46 insider: viewer ? granted(viewer, repo) != null : false,
47 can: abilities(viewer, repo),
48 };
49}
50
51/**
52 * The repository and the viewer's access to it, refusing with a 404 when
53 * they cannot read it and a 403 that says which role is needed when they
54 * can read it but not do `capability`.
55 */
56export async function requireRepo(context: Context, params: RepoParams, capability: Capability = "read") {
57 const viewer = getViewer(context);
58 const found = await repoFor(context, params);
59 if (!found.ok) {
60 if (found.error.code === "not_found" || found.error.code === "forbidden") throw data(null, { status: 404 });
61 throw data(found.error.message, { status: httpStatus(found.error) });
62 }
63 const access = accessFor(viewer, found.value);
64 if (!access.can.read) throw data(null, { status: 404 });
65 if (!access.can[capability]) throw data(needs(capability), { status: 403 });
66 return { viewer, repo: found.value, access };
67}
68
69/**
70 * Pages only people with a role of their own see (plans, memory,
71 * deployments, security, settings): a 404 for anyone else, as before.
72 */
73export async function requireInsider(context: Context, params: RepoParams, capability: Capability = "read") {
74 const found = await requireRepo(context, params, "read");
75 if (!found.access.insider) throw data(null, { status: 404 });
76 if (!found.access.can[capability]) throw data(needs(capability), { status: 403 });
77 return found;
78}
79
80/** The viewer's access, or none when the repository cannot be read. */
81export async function accessTo(context: Context, params: RepoParams): Promise<ViewerAccess> {
82 const found = await repoFor(context, params);
83 return found.ok ? accessFor(getViewer(context), found.value) : { role: null, insider: false, can: abilities(null, { id: "", namespace: "", isPrivate: true }) };
84}
85
86/**
87 * For an action: why the viewer may not do `capability` here, or null when
88 * they may. Not cached, since the action may change the repository.
89 */
90export async function refusal(context: Context, params: RepoParams, capability: Capability): Promise<string | null> {
91 const viewer = getViewer(context);
92 const found = await repos.get({ namespace: params.owner ?? "", name: params.repo ?? "" }, viewer);
93 if (!found.ok) return found.error.message;
94 return can(viewer, found.value, capability) ? null : needs(capability);
95}
96
97/** For an action whose page only people with `capability` see: a 403 that says why, otherwise nothing. */
98export async function requireCapability(context: Context, params: RepoParams, capability: Capability): Promise<void> {
99 const refused = await refusal(context, params, capability);
100 if (refused) throw data(refused, { status: 403 });
101}