gitea: the token needs read:user, not just write:repository and write:issue
Deployed #49 and the watcher immediately broke: GET /user/repos answered 403, 'required=[read:user]' — confirmed live against the running forge (1.27.3). That route sits under gitea's user scope category despite listing repositories, not repository as assumed. Also gives the fake forge real scope enforcement on /user/repos, which is why the original PR's test suite didn't catch this: it only checked the token's value was valid, never that it carried the required scope.
This commit is contained in:
@@ -38,8 +38,12 @@ function fakeForge(): Promise<Forge> {
|
||||
mints: 0,
|
||||
lastScopes: null as string[] | null,
|
||||
tokens: new Map<string, string>(), // name -> value
|
||||
scopesOf: new Map<string, string[]>(), // value -> scopes, so a route can enforce them like gitea does
|
||||
admins: new Map([[ADMIN, PASSWORD]]),
|
||||
};
|
||||
// write:X implies read:X — gitea's own rule (models/auth/access_token_scope.go).
|
||||
const covers = (scopes: string[], required: string): boolean =>
|
||||
scopes.includes(required) || scopes.includes(`write:${required.split(":")[1]}`);
|
||||
const json = (res: ServerResponse, status: number, body: unknown): void => {
|
||||
res.writeHead(status, { "Content-Type": "application/json" });
|
||||
res.end(body === null ? "" : JSON.stringify(body));
|
||||
@@ -70,6 +74,7 @@ function fakeForge(): Promise<Forge> {
|
||||
forge.lastScopes = scopes;
|
||||
const sha1 = `minted-${forge.mints}-${Math.random().toString(36).slice(2)}`;
|
||||
forge.tokens.set(name, sha1);
|
||||
forge.scopesOf.set(sha1, scopes);
|
||||
return json(res, 201, { id: forge.mints, name, sha1, scopes, token_last_eight: sha1.slice(-8) });
|
||||
}
|
||||
if (req.method === "DELETE" && tokens[2]) {
|
||||
@@ -84,6 +89,14 @@ function fakeForge(): Promise<Forge> {
|
||||
const h = req.headers.authorization ?? "";
|
||||
const value = h.startsWith("token ") ? h.slice(6) : "";
|
||||
if (![...forge.tokens.values()].includes(value)) return json(res, 401, { message: "token is required" });
|
||||
// gitea 1.27.3: GET /user/repos sits under the `user` scope category, not `repository` —
|
||||
// confirmed against the live forge. A token without read:user (or write:user) is refused here.
|
||||
const scopes = forge.scopesOf.get(value) ?? [];
|
||||
if (!covers(scopes, "read:user")) {
|
||||
return json(res, 403, {
|
||||
message: `token does not have at least one of required scope(s), required=[read:user]`,
|
||||
});
|
||||
}
|
||||
return json(res, 200, [
|
||||
{ full_name: "novox/hq", name: "hq", owner: { login: "novox" }, private: true, html_url: "http://fake/novox/hq" },
|
||||
]);
|
||||
@@ -146,7 +159,7 @@ test("first start: mints with the admin account, keeps the token at 0600, asks f
|
||||
|
||||
assert.equal(repos[0]?.full_name, "novox/hq");
|
||||
assert.equal(forge.mints, 1);
|
||||
assert.deepEqual(forge.lastScopes, ["write:repository", "write:issue"]);
|
||||
assert.deepEqual(forge.lastScopes, ["write:repository", "write:issue", "read:user"]);
|
||||
assert.deepEqual(forge.lastScopes, [...TOKEN_SCOPES]);
|
||||
const token = forge.tokens.get("mesh-tools")!;
|
||||
assert.equal(await readFile(file, "utf8"), token + "\n");
|
||||
|
||||
Reference in New Issue
Block a user