gitea: say that a repository made private later is granted only at the next start or by hand, and let one unreadable permission refuse its repository, not the whole pass
mesh/delivery merged: source.main moved to novox/mesh-catalog@a4f5df797b68
mesh/check being carried to b4a7bb9dea50 on a4f5df797b68
mesh/delivery merged: source.main moved to novox/mesh-catalog@a4f5df797b68
mesh/check being carried to b4a7bb9dea50 on a4f5df797b68
This commit is contained in:
@@ -284,8 +284,12 @@ repository it cannot read: one repository at a time, no organisation membership,
|
||||
|
||||
- **When:** one pass over every repository of every user and organisation at the start (a forge not answering yet is
|
||||
asked again a minute apart until one pass ran), and on the forge's system webhook: `forge.repository.created` and
|
||||
`forge.fork` at once, and the first event of any repository this process has not seen readable (a repository made
|
||||
private later sends no creation). Nothing polls for it.
|
||||
`forge.fork` at once, and the first event of any repository this process has not seen readable (the forge may not
|
||||
send a creation for every owner). Nothing polls for it.
|
||||
- **Not caught:** a repository made private after it was seen readable. The forge sends no event for a change of
|
||||
visibility, and the start's pass already saw it readable, so its events ask nothing. It is granted at the module's
|
||||
next start, or now by `gitea_grant_read`; `gitea_unreadable_repos` shows it meanwhile. A repository whose permission
|
||||
the forge will not answer is named as refused (or, in the listing, with permission `unknown`) and the pass goes on.
|
||||
- **With what:** the site administrator's login (`MESH_GITEA_ADMIN_PASSWORD_FILE`), because adding a collaborator asks a
|
||||
repository's administration, which on another user's repository only the site administrator holds. It is used for
|
||||
four calls alone: list every repository, read one, read the account's permission on one, add the account as a read
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
// - **at the start**, one pass over every repository of every user and organisation;
|
||||
// - **on the forge's own event** (the system webhook, webhook.ts): `forge.repository.created` and `forge.fork` at
|
||||
// once, and the first event of any repository this process has not yet seen readable, since the forge may not
|
||||
// send a repository's creation for every owner, and a repository made private later sends no creation at all.
|
||||
// Nothing polls for it.
|
||||
// send a repository's creation for every owner. Nothing polls for it.
|
||||
//
|
||||
// **Not caught: a repository made private later.** The forge sends no event for a change of visibility, and a public
|
||||
// repository is already seen readable by the start's pass, so its events ask nothing. Such a repository is granted at
|
||||
// the module's next start, or now by `gitea_grant_read`; `gitea_unreadable_repos` shows it meanwhile.
|
||||
//
|
||||
// **The grant needs the site administrator's login.** Adding a collaborator asks the repository's administration,
|
||||
// which on another user's repository only the site administrator holds; the module holds that login for setting the
|
||||
@@ -76,8 +79,14 @@ export class ReadAll {
|
||||
const repos = await this.forge.everyRepository();
|
||||
const out: Unreadable[] = [];
|
||||
for (const repo of repos) {
|
||||
const u = await this.judge(repo);
|
||||
if (u) out.push(u);
|
||||
try {
|
||||
const u = await this.judge(repo);
|
||||
if (u) out.push(u);
|
||||
} catch (err) {
|
||||
// A permission the forge would not answer is said for that repository; the rest are still looked at.
|
||||
out.push({ repository: repo.full_name, permission: "unknown", private: repo.private, archived: repo.archived,
|
||||
why: `the forge did not answer ${this.account}'s permission on it — ${err instanceof Error ? err.message : String(err)}` });
|
||||
}
|
||||
}
|
||||
return { checked: repos.length, unreadable: out };
|
||||
}
|
||||
@@ -97,7 +106,14 @@ export class ReadAll {
|
||||
} else repos = await this.forge.everyRepository();
|
||||
const result: Granted = { checked: repos.length, granted: [], refused: [] };
|
||||
for (const repo of repos) {
|
||||
const u = await this.judge(repo);
|
||||
let u: Unreadable | null;
|
||||
try {
|
||||
u = await this.judge(repo);
|
||||
} catch (err) {
|
||||
// One repository whose permission cannot be read is a refusal of its own, never the end of the pass.
|
||||
result.refused.push({ repository: repo.full_name, why: `its permission could not be read — ${err instanceof Error ? err.message : String(err)}` });
|
||||
continue;
|
||||
}
|
||||
if (!u) continue;
|
||||
try {
|
||||
await this.forge.addReadCollaborator(repo.owner, repo.name, this.account);
|
||||
@@ -106,7 +122,13 @@ export class ReadAll {
|
||||
continue;
|
||||
}
|
||||
// Read back: the forge's own word that the account reads it now, never the grant's answer alone.
|
||||
const after = await this.forge.repositoryPermission(repo.owner, repo.name, this.account);
|
||||
let after: string;
|
||||
try {
|
||||
after = await this.forge.repositoryPermission(repo.owner, repo.name, this.account);
|
||||
} catch (err) {
|
||||
result.refused.push({ repository: repo.full_name, why: `granted, but its permission could not be read back — ${err instanceof Error ? err.message : String(err)}` });
|
||||
continue;
|
||||
}
|
||||
if (READS.has(after)) {
|
||||
this.readable.add(repo.full_name);
|
||||
result.granted.push(repo.full_name);
|
||||
@@ -136,6 +158,8 @@ export class ReadAll {
|
||||
return;
|
||||
}
|
||||
const made = event.name === "forge.repository.created" || event.name === "forge.fork";
|
||||
// Seen readable once, its events ask nothing: so a repository made private after that is not caught here (the
|
||||
// forge says no change of visibility), only at the next start or by gitea_grant_read.
|
||||
if (!made && this.readable.has(full)) return;
|
||||
const r = await this.grant([full]);
|
||||
if (r.granted.length) this.log(`${this.account} was given read on ${full}, on the forge's ${event.name}`);
|
||||
|
||||
@@ -28,7 +28,7 @@ interface Team { id: number; name: string; units: any; members: Set<string> }
|
||||
|
||||
interface ForgeRepo { owner: string; name: string; full: string; private: boolean; archived?: boolean; ownerVisibility: string; collaborators: Map<string, string> }
|
||||
const forgeRepos: ForgeRepo[] = [];
|
||||
const repoFaults = { refuse: new Set<string>() };
|
||||
const repoFaults = { refuse: new Set<string>(), permission: new Set<string>() };
|
||||
function addRepo(full: string, isPrivate: boolean, ownerVisibility = "public"): ForgeRepo {
|
||||
const [owner, name] = full.split("/");
|
||||
const r = { owner, name, full, private: isPrivate, ownerVisibility, collaborators: new Map<string, string>() };
|
||||
@@ -209,6 +209,7 @@ function fakeForge() {
|
||||
if (req.method === "GET" && !m[3]) return json(res, 200, repoJSON(repo));
|
||||
const user = decodeURIComponent(m[3] ?? "");
|
||||
if (req.method === "GET" && m[4]) {
|
||||
if (repoFaults.permission.has(repo.full)) return json(res, 500, { message: "the forge is not well" });
|
||||
if (!isAdmin(me) && me !== user) return json(res, 403, { message: "only the user or an administrator" });
|
||||
return json(res, 200, { permission: permissionOf(user, repo) });
|
||||
}
|
||||
@@ -511,16 +512,18 @@ test("on the forge's events: a repository made is granted read at once, a known
|
||||
const readAll = new ReadAll(GiteaAdmin.fromEnv(env), TOOLS_ACCOUNT, (l: string) => lines.push(l));
|
||||
await readAll.atStart();
|
||||
|
||||
const cases: { name: string; event: string; repo: string; make?: boolean; refuse?: boolean; asks: boolean; reads: string }[] = [
|
||||
const cases: { name: string; event: string; repo: string; make?: boolean; refuse?: boolean; unanswered?: boolean; asks: boolean; reads: string }[] = [
|
||||
{ name: "a repository made", event: "forge.repository.created", repo: "jschoubben/new-private", make: true, asks: true, reads: "read" },
|
||||
{ name: "a fork made", event: "forge.fork", repo: "someone/fork", make: true, asks: true, reads: "read" },
|
||||
{ name: "a push to a repository seen readable", event: "forge.push", repo: "jschoubben/txt-game", asks: false, reads: "read" },
|
||||
{ name: "a push to a private repository never seen", event: "forge.push", repo: "jschoubben/quiet", make: true, asks: true, reads: "read" },
|
||||
{ name: "a repository the forge refuses the grant on", event: "forge.repository.created", repo: "jschoubben/stuck", make: true, refuse: true, asks: true, reads: "none" },
|
||||
{ name: "a repository whose permission the forge will not answer", event: "forge.repository.created", repo: "jschoubben/mute", make: true, unanswered: true, asks: true, reads: "none" },
|
||||
];
|
||||
for (const c of cases) {
|
||||
if (c.make) addRepo(c.repo, true);
|
||||
if (c.refuse) repoFaults.refuse.add(c.repo);
|
||||
if (c.unanswered) repoFaults.permission.add(c.repo);
|
||||
const from = forge.calls.length;
|
||||
const [owner, repo] = c.repo.split("/");
|
||||
await readAll.onEvent({ name: c.event, body: { owner, repo } });
|
||||
@@ -531,5 +534,14 @@ test("on the forge's events: a repository made is granted read at once, a known
|
||||
const named = await (readAllTools(() => readAll)[1].run({ repositories: ["jschoubben/stuck"] })) as any;
|
||||
assert.deepEqual(named.refused.map((x: any) => x.repository), ["jschoubben/stuck"]);
|
||||
await assert.rejects(readAllTools(() => readAll)[1].run({ repositories: ["no-slash"] }), /not a repository/);
|
||||
assert.match(lines.join("\n"), /could not be given read on jschoubben\/mute — its permission could not be read — .*500/);
|
||||
// A whole pass goes past the repository it cannot judge: every other one is still granted, and the listing names it.
|
||||
addRepo("jschoubben/after-mute", true);
|
||||
const pass = await readAll.grant();
|
||||
assert.ok(pass.granted.includes("jschoubben/after-mute"), "the pass did not stop at the repository it could not judge");
|
||||
assert.ok(pass.refused.some((x: any) => x.repository === "jschoubben/mute" && /permission could not be read/.test(x.why)));
|
||||
const listed = await readAllTools(() => readAll)[0].run({}) as any;
|
||||
assert.equal(listed.unreadable.find((u: any) => u.repository === "jschoubben/mute").permission, "unknown");
|
||||
repoFaults.refuse.clear();
|
||||
repoFaults.permission.clear();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user