gitea: the token needs read:user, not just write:repository and write:issue #51
@@ -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");
|
||||
|
||||
@@ -28,12 +28,15 @@ export const TOKEN_NAME = "mesh-tools";
|
||||
|
||||
/**
|
||||
* The least the fifteen tools and the watcher need (gitea's route groups, 1.20+ scoped tokens):
|
||||
* write:repository — list/create/delete repositories, pull requests (list/get/open/merge), and
|
||||
* the watcher's /user/repos poll;
|
||||
* write:issue — issues, comments, labels.
|
||||
* Nothing under /admin, /orgs or /users — the escape-hatch tool reaches only what these two cover.
|
||||
* write:repository — create/delete repositories, pull requests (list/get/open/merge);
|
||||
* write:issue — issues, comments, labels;
|
||||
* read:user — GET /user/repos, which the watcher's poll and gitea_list_repos both call.
|
||||
* It sits under the `user` category despite listing repositories, not `repository`
|
||||
* — confirmed against the running forge (1.27.3), which answered
|
||||
* `required=[read:user]` to a token carrying only the other two.
|
||||
* Nothing under /admin, /orgs or write:user — the escape-hatch tool reaches only what these three cover.
|
||||
*/
|
||||
export const TOKEN_SCOPES: readonly string[] = ["write:repository", "write:issue"];
|
||||
export const TOKEN_SCOPES: readonly string[] = ["write:repository", "write:issue", "read:user"];
|
||||
|
||||
/** Where a client's token comes from, and what to do when the forge says it is wrong. */
|
||||
export interface TokenSource {
|
||||
|
||||
Reference in New Issue
Block a user