mesh/delivery merged: source.main moved to novox/mesh-catalog@bf94af0a820f
mesh/check checking 36d59933239e on bf94af0a820f
104 lines
5.6 KiB
TypeScript
104 lines
5.6 KiB
TypeScript
// Who is in a forge team and what the team may do (issue 1563; novox/hq ADR 0466 §3, issue 971).
|
|
//
|
|
// The build keys sit on one forge user, nox-builder, in the team `builders`, which reads every repository and pushes
|
|
// nothing. The tools' account cannot read a team (the forge answers /orgs/novox/teams 403: it lacks read:organization),
|
|
// so the read goes with the site administrator's login, and only the read, as the branch protection read does (ADR 0375):
|
|
// GET /orgs/{org}/teams, GET /teams/{id}/members and, for a user's effective right on a repository, the repository as
|
|
// that user sees it (GiteaAdmin.repositoryPermission, which counts access through a team). Nothing is written.
|
|
|
|
/** A team as the forge's Team object answers it (GET /orgs/{org}/teams): the fields read here. */
|
|
export interface ForgeTeam {
|
|
id: number;
|
|
name: string;
|
|
description?: string;
|
|
/** The team's overall permission: none, read, write, admin, owner. */
|
|
permission?: string;
|
|
includes_all_repositories?: boolean;
|
|
can_create_org_repo?: boolean;
|
|
/** Per unit, the right the team has: units_map {"repo.code": "read", ...}. */
|
|
units_map?: Record<string, string>;
|
|
units?: string[];
|
|
}
|
|
|
|
/** What the module asks of the forge for this, with the site administrator's login (GiteaAdmin, client.ts). */
|
|
export interface TeamForge {
|
|
teams(org: string): Promise<ForgeTeam[]>;
|
|
teamMembers(teamId: number): Promise<string[]>;
|
|
accountExists(user: string): Promise<boolean>;
|
|
everyRepository(): Promise<{ full_name: string; owner: string; name: string }[]>;
|
|
repositoryPermission(owner: string, repo: string, user: string): Promise<string>;
|
|
}
|
|
|
|
export interface TeamRights {
|
|
team: string;
|
|
id: number;
|
|
description: string;
|
|
members: string[];
|
|
/** The team's overall permission. */
|
|
permission: string;
|
|
includes_all_repositories: boolean;
|
|
can_create_org_repo: boolean;
|
|
/** Per unit, read / write / admin / none. */
|
|
units: Record<string, string>;
|
|
/** True when no unit of the team is above read: the team pushes nothing, writes no issue, no pull request. */
|
|
read_only: boolean;
|
|
}
|
|
|
|
const RANK: Record<string, number> = { none: 0, read: 1, write: 2, admin: 3, owner: 4 };
|
|
|
|
/** One team as a person reads it. A team the forge gives no units_map is read from its `permission`, which is what the
|
|
* older forge versions answer. */
|
|
export function teamRights(t: ForgeTeam, members: string[]): TeamRights {
|
|
const units = t.units_map && Object.keys(t.units_map).length ? { ...t.units_map }
|
|
: Object.fromEntries((t.units ?? []).map((u) => [u, t.permission ?? "none"]));
|
|
const highest = Math.max(0, ...Object.values(units).map((r) => RANK[r] ?? 0));
|
|
return {
|
|
team: t.name, id: t.id, description: t.description ?? "", members: [...members].sort(), permission: t.permission ?? "none",
|
|
includes_all_repositories: t.includes_all_repositories === true, can_create_org_repo: t.can_create_org_repo === true,
|
|
units, read_only: highest <= RANK.read && (RANK[t.permission ?? "none"] ?? 0) <= RANK.read,
|
|
};
|
|
}
|
|
|
|
export interface UserRights {
|
|
user: string;
|
|
/** The teams of the organisation the user is a member of. */
|
|
teams: string[];
|
|
/** How many repositories the forge gives the user each right on, effective (through a team or as a collaborator). */
|
|
repositories: Record<string, number>;
|
|
/** The repositories where the user may do more than read: empty for a read-only user. */
|
|
writes: { repository: string; permission: string }[];
|
|
/** Every repository and the right on it; only when asked. */
|
|
every?: { repository: string; permission: string }[];
|
|
}
|
|
|
|
/** The teams of an organisation (or the one named), each with its members and rights; and for one user, the teams it is
|
|
* in and its effective right on every repository (or on those named), as the forge says it. Only reads. */
|
|
export async function readTeamRights(forge: TeamForge, org: string, opts: { team?: string; user?: string; repositories?: string[]; list?: boolean } = {}):
|
|
Promise<{ organisation: string; teams: TeamRights[]; user?: UserRights }> {
|
|
if (opts.user && !(await forge.accountExists(opts.user))) throw new Error(`no forge user ${opts.user}`);
|
|
const all = await forge.teams(org);
|
|
const named = opts.team ? all.filter((t) => t.name === opts.team) : all;
|
|
if (opts.team && !named.length) throw new Error(`no team "${opts.team}" in ${org}: it holds ${all.map((t) => t.name).join(", ") || "none"}`);
|
|
// Membership is read for every team when a user is asked, so the user's teams are all found.
|
|
const wanted = opts.user ? all : named;
|
|
const rights: TeamRights[] = [];
|
|
for (const t of wanted) rights.push(teamRights(t, await forge.teamMembers(t.id)));
|
|
const shown = opts.team ? rights.filter((r) => r.team === opts.team) : rights;
|
|
const out: { organisation: string; teams: TeamRights[]; user?: UserRights } = { organisation: org, teams: shown };
|
|
if (!opts.user) return out;
|
|
const user = opts.user;
|
|
const mine = rights.filter((r) => r.members.includes(user)).map((r) => r.team);
|
|
const repos = (await forge.everyRepository()).filter((r) => !opts.repositories?.length || opts.repositories.includes(r.full_name));
|
|
const counts: Record<string, number> = {};
|
|
const writes: { repository: string; permission: string }[] = [];
|
|
const every: { repository: string; permission: string }[] = [];
|
|
for (const r of repos) {
|
|
const permission = await forge.repositoryPermission(r.owner, r.name, user);
|
|
counts[permission] = (counts[permission] ?? 0) + 1;
|
|
if ((RANK[permission] ?? 0) > RANK.read) writes.push({ repository: r.full_name, permission });
|
|
every.push({ repository: r.full_name, permission });
|
|
}
|
|
out.user = { user, teams: mine, repositories: counts, writes, ...(opts.list ? { every } : {}) };
|
|
return out;
|
|
}
|