From 7920b6a20f0fac893260062f8e3799c136b90942 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Tue, 18 Aug 2026 12:10:17 +0200 Subject: [PATCH 1/2] Token-Rechte vollstaendig dokumentiert Die Angaben stammten aus der Zeit, als Nebula nur gelesen hat. Seit v1.7.0 legt es Issues an und schliesst sie - wer sich an README oder den Hinweis im Verbindungsdialog hielt, bekam dabei einen 403. Erhoben wurde es nicht aus dem Gedaechtnis, sondern aus dem Code: jeder ausgehende Aufruf in lib/git/gitea.ts, github.ts und gitlab.ts, abgebildet auf das noetige Recht. Aufgeschluesselt nach Funktion statt nach System, damit erkennbar ist, was sich weglassen laesst: der Abgleich allein kommt rein lesend aus, nur die Issue-Anbindung schreibt. **GitLab ist der unangenehme Fall.** Es kennt keinen schmalen Schreib-Scope fuer Issues - dort geht nur "api", also alles, was das Konto darf. Das steht jetzt ausdruecklich da, samt der Alternative: bei read_api bleiben und auf die Issue-Anbindung verzichten. Der Abgleich laeuft dann vollstaendig, und geschlossene Issues werden weiterhin erkannt, nur nicht zurueckgeschrieben. Ebenfalls festgehalten, was Nebula *nicht* braucht: Schreibrechte auf Code, Adminrechte, Rechte zum Anlegen von Webhooks. Letzteres ist der Grund, warum der Haken von Hand eingetragen wird - so bleibt der Token davon frei. Der Hinweis in lib/git/meta.ts steht direkt am Eingabefeld und ist die Stelle, die tatsaechlich gelesen wird; er ist deshalb mitgezogen. Dazu ein Waechter in meta.test.ts: jeder Provider muss in GitClient createIssue und setIssueState koennen, folglich muss auch jeder Hinweis sagen, was das an Rechten kostet. Genau diese Kopplung war hier gerissen. Geprueft: 267 Unit-Tests (6 neue), 36 E2E-Tests, Hinweistext je System gegen die laufende Anwendung gelesen, axe-core auf 375px und 1280px ohne Beanstandung. Refs #10 --- FEATURES.md | 24 ++++++++++++++++ README.md | 51 ++++++++++++++++++++++++++++------ src/lib/git/meta.test.ts | 60 ++++++++++++++++++++++++++++++++++++++++ src/lib/git/meta.ts | 11 ++++++-- 4 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 src/lib/git/meta.test.ts diff --git a/FEATURES.md b/FEATURES.md index b957c34..fcf90e7 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -485,6 +485,30 @@ Firmen-Gitea nebeneinander. Kurzbeschreibung. - **Löschen** trennt nur die Verknüpfung; die Projekte selbst bleiben erhalten. +#### Nötige Token-Rechte + +Nebula liest fast alles nur. Geschrieben wird ausschließlich beim Anlegen und Schließen +von Issues (4.6) — wer darauf verzichtet, kommt mit einem rein lesenden Token aus. + +| System | Nur Abgleich | Zusätzlich für die Issue-Anbindung | +| --- | --- | --- | +| GitHub | *Metadata*, *Contents*, *Issues*, *Pull requests*, *Actions* — je lesend | *Issues: Read and write* | +| GitLab | `read_api` | `api` | +| Gitea / Forgejo | `read:repository`, `read:issue`, `read:user` | `write:issue` | + +**GitLab kennt keinen schmalen Schreib-Scope.** `api` erlaubt alles, was das Konto darf, +Code eingeschlossen. Wem das zu weit geht, bleibt bei `read_api`: der Abgleich läuft +vollständig, geschlossene Issues werden weiterhin erkannt — nur schreibt Nebula dann +nicht zurück. + +**Nicht verlangt** werden Schreibrechte auf Code, Adminrechte und Rechte zum Anlegen von +Webhooks. Der Haken wird von Hand im Git-System eingetragen (4.7); das erspart dem Token +genau die Administrationsrechte, die er sonst bräuchte. + +Ein fehlendes Recht endet in einem `403` mit „Token hat nicht die noetigen Scopes", +sichtbar am Repository auf der Projektseite. Klein anfangen und gezielt nachlegen ist +deshalb ohne Risiko. + ### 4.3 Was beim Sync geholt wird | Bereich | Details | diff --git a/README.md b/README.md index 4e7b4ab..a8bec2e 100644 --- a/README.md +++ b/README.md @@ -173,14 +173,8 @@ Nebula auffindbare Credentials nutzt. ## Git-Verbindung einrichten -1. Im Git-System einen **Personal Access Token** mit Leserechten erzeugen: - - | System | Scopes | - | --- | --- | - | GitHub | Fine-grained Token: *Contents: Read*, *Metadata: Read*, *Issues: Read*, *Pull requests: Read* | - | GitLab | `read_api` (alternativ `read_repository`) | - | Gitea / Forgejo | `repository:read`, `issue:read`, `user:read` | - +1. Im Git-System einen **Personal Access Token** erzeugen — mit den Rechten aus dem + Abschnitt [Welche Rechte der Token braucht](#welche-rechte-der-token-braucht). 2. In Nebula unter **Git-Verbindungen → Verbindung hinzufügen** eintragen: System wählen, Basis-URL (z. B. `https://git.meine-firma.de`, ohne `/api`) und Token. 3. Mit **Testen** prüfen — Nebula meldet den erkannten Benutzer zurück. @@ -192,6 +186,47 @@ Nebula auffindbare Credentials nutzt. Der Snapshot bleibt in der Datenbank, das Dashboard funktioniert also auch offline. Ohne Token gehen nur öffentliche Repositories, mit strengem Rate-Limit. +### Welche Rechte der Token braucht + +Nebula liest fast alles nur — geschrieben wird ausschließlich, wenn du eine Aufgabe als +Issue anlegst oder abhakst. Deshalb lohnt es, zwei Fälle zu unterscheiden: + +**Fall A — nur Abgleich.** Branches, Tags, Commits, Releases, CI-Zustand, README, +Heatmap, offene Issues und PRs anzeigen. Rein lesend. + +**Fall B — zusätzlich die Issue-Anbindung.** Eine Aufgabe als Issue anlegen; wird das +Issue geschlossen, hakt Nebula die Aufgabe ab und schließt sie umgekehrt mit. + +| System | Fall A (nur lesen) | Fall B zusätzlich | +| --- | --- | --- | +| **GitHub** | Fine-grained Token: *Metadata: Read*, *Contents: Read*, *Issues: Read*, *Pull requests: Read*, *Actions: Read* | *Issues: **Read and write*** | +| **GitLab** | `read_api` | `api` — **es gibt nichts dazwischen** | +| **Gitea / Forgejo** | `read:repository`, `read:issue`, `read:user` | `write:issue` | + +Wozu die einzelnen Rechte gut sind: + +| Recht | Wofür | +| --- | --- | +| Repository lesen (`Contents`, `read:repository`) | Branches, Tags, Commits, Releases, Sprachen, README, Commit-Heatmap | +| Issues lesen | Offene Issues auf der Projektseite, „Issues als Aufgaben übernehmen" | +| Pull Requests lesen | Offene PRs auf der Projektseite | +| Actions lesen (nur GitHub) | CI-Zustand des Default-Branch. Bei GitLab kommt er aus den Pipelines (`read_api`), bei Gitea aus dem Commit-Status (`read:repository`) | +| Benutzer lesen | Nur für den Verbindungstest — Nebula meldet zurück, als wer der Token gilt | +| Issues schreiben | Aufgabe als Issue anlegen, schließen, wieder öffnen | + +> **GitLab ist der unangenehme Fall.** Es kennt keinen schmalen Schreib-Scope für Issues: +> `api` erlaubt alles, was dein Konto darf — auch Code ändern. Wenn dir das zu weit geht, +> nimm `read_api` und verzichte auf die Issue-Anbindung. Der Abgleich funktioniert +> vollständig, und geschlossene Issues werden trotzdem erkannt — nur eben nur gelesen. + +**Was Nebula *nicht* braucht:** Schreibrechte auf Code, Adminrechte, Rechte zum Anlegen +von Webhooks. Den Haken für Webhooks trägst du selbst im Git-System ein — genau deshalb +verlangt Nebula dafür kein zusätzliches Recht. + +**Im Zweifel klein anfangen.** Ein fehlendes Recht führt zu einem `403` mit dem Hinweis +„Token hat nicht die noetigen Scopes"; die Meldung steht am Repository auf der +Projektseite. Dann gezielt nachlegen, statt vorsorglich alles zu vergeben. + ### Automatisch statt auf Knopfdruck Unter **Admin-Einstellungen → Automatischer Abgleich** lässt sich der Sync in den diff --git a/src/lib/git/meta.test.ts b/src/lib/git/meta.test.ts new file mode 100644 index 0000000..045cc15 --- /dev/null +++ b/src/lib/git/meta.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { PROVIDER_META, guessProvider } from "./meta"; + +const PROVIDER = ["GITHUB", "GITLAB", "GITEA"] as const; + +describe("PROVIDER_META", () => { + it("kennt jedes unterstuetzte System", () => { + for (const provider of PROVIDER) { + expect(PROVIDER_META[provider], provider).toBeDefined(); + } + // Und nichts darueber hinaus – ein Eintrag ohne Gegenstueck im Schema + // waere ein Ueberbleibsel. + expect(Object.keys(PROVIDER_META).sort()).toEqual([...PROVIDER].sort()); + }); + + it("hat zu jedem System Beschriftung, Basis-URL und Dokuverweis", () => { + for (const provider of PROVIDER) { + const meta = PROVIDER_META[provider]; + expect(meta.label.length, provider).toBeGreaterThan(0); + expect(meta.defaultBaseUrl, provider).toMatch(/^https:\/\//); + expect(meta.docsUrl, provider).toMatch(/^https:\/\//); + } + }); + + /** + * Der eigentliche Waechter. + * + * Der Hinweis am Eingabefeld war eine Zeit lang rein lesend formuliert, + * obwohl Nebula seit v1.7.0 Issues anlegt und schliesst – wer sich daran + * hielt, bekam einen 403. Jeder Provider in GitClient muss createIssue und + * setIssueState koennen; folglich muss auch jeder Hinweis sagen, was das + * an Rechten kostet. + */ + it("sagt bei jedem System etwas ueber die Rechte fuer Issues", () => { + for (const provider of PROVIDER) { + expect(PROVIDER_META[provider].tokenHelp, provider).toMatch(/issue/i); + } + }); + + it("nennt einen konkreten Scope statt nur 'Leserechte'", () => { + // GitLab nennt Scopes in Anfuehrungszeichen, GitHub und Gitea + // Berechtigungsnamen – gemeinsam ist, dass etwas Nachschlagbares dasteht. + expect(PROVIDER_META.GITLAB.tokenHelp).toMatch(/read_api/); + expect(PROVIDER_META.GITEA.tokenHelp).toMatch(/read:repository/); + expect(PROVIDER_META.GITHUB.tokenHelp).toMatch(/Metadata/); + }); +}); + +describe("guessProvider", () => { + it("erkennt die oeffentlichen Instanzen am Hostnamen", () => { + expect(guessProvider("github.com")).toBe("GITHUB"); + expect(guessProvider("gitlab.com")).toBe("GITLAB"); + }); + + it("faellt bei allem anderen auf Gitea zurueck", () => { + // Selbst gehostet ist hier der haeufigste Fall, und Gitea die + // wahrscheinlichste Wahl dafuer. + expect(guessProvider("git.wilde-server.de")).toBe("GITEA"); + }); +}); diff --git a/src/lib/git/meta.ts b/src/lib/git/meta.ts index 0fa0bed..26cd4b7 100644 --- a/src/lib/git/meta.ts +++ b/src/lib/git/meta.ts @@ -12,19 +12,24 @@ export const PROVIDER_META: Record< label: "GitHub", defaultBaseUrl: "https://github.com", tokenHelp: - "Fine-grained Token mit Lesezugriff auf Contents, Metadata, Issues und Pull Requests.", + "Fine-grained Token: Metadata, Contents und Pull requests lesen, Issues lesen. " + + "Nur wenn Aufgaben als Issue angelegt werden sollen, Issues zusaetzlich schreiben.", docsUrl: "https://github.com/settings/tokens", }, GITLAB: { label: "GitLab", defaultBaseUrl: "https://gitlab.com", - tokenHelp: "Personal Access Token mit Scope 'read_api' (bzw. 'read_repository').", + tokenHelp: + "Scope 'read_api' genuegt fuer den Abgleich. Aufgaben als Issue anzulegen verlangt " + + "'api' – GitLab kennt keinen schmaleren Schreibzugriff.", docsUrl: "https://gitlab.com/-/user_settings/personal_access_tokens", }, GITEA: { label: "Gitea / Forgejo", defaultBaseUrl: "https://git.example.com", - tokenHelp: "Access Token mit Leserechten auf repository, issue und user.", + tokenHelp: + "read:repository, read:issue und read:user. Nur wenn Aufgaben als Issue angelegt " + + "werden sollen, zusaetzlich write:issue.", docsUrl: "https://docs.gitea.com/development/api-usage", }, }; From e9f01ba8fa9f667702be05807beff7fa60afb610 Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Tue, 18 Aug 2026 12:57:02 +0200 Subject: [PATCH 2/2] docs(git): jeden ausgehenden Aufruf einzeln mit seinem Recht belegen Die Faelle A und B beantworteten die Frage auf Ebene der Scopes, nicht auf Ebene der Aufrufe. Wer nachpruefen wollte, ob ein Recht wirklich noetig ist, musste die drei Clients selbst lesen. Jetzt steht in der README je System eine vollstaendige Tabelle: Aufruf, Zweck, benoetigtes Recht. Die Zuordnung bei GitHub folgt der offiziellen Berechtigungsuebersicht - dass /tags und /stats/commit_activity unter Metadata fallen und nicht unter Contents, ist nicht zu erraten. Fuer GitLab ist mit Quellenangabe belegt, dass es unterhalb von 'api' keinen Schreibzugriff auf Issues gibt. Ausserdem war offen, woher die Kommentare an den Issues stammen: Nebula schreibt keine. Der Schreibzugriff besteht aus genau zwei Aufrufen. Ein Test klopft dafuer die Clients ab - kaeme ein dritter dazu, wuerde die Empfehlung 'nimm ein lesendes Token' still falsch, ohne dass jemand einen Fehler saehe. Der Hinweis am Eingabefeld nannte Actions nicht, obwohl der CI-Zustand bei GitHub genau daran haengt. Refs #10 --- FEATURES.md | 14 ++- README.md | 127 +++++++++++++++++++++++----- src/lib/git/meta.ts | 5 +- src/lib/git/schreibzugriffe.test.ts | 60 +++++++++++++ 4 files changed, 181 insertions(+), 25 deletions(-) create mode 100644 src/lib/git/schreibzugriffe.test.ts diff --git a/FEATURES.md b/FEATURES.md index fcf90e7..447d41a 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -487,8 +487,9 @@ Firmen-Gitea nebeneinander. #### Nötige Token-Rechte -Nebula liest fast alles nur. Geschrieben wird ausschließlich beim Anlegen und Schließen -von Issues (4.6) — wer darauf verzichtet, kommt mit einem rein lesenden Token aus. +Nebula liest fast alles nur. Geschrieben wird an **genau zwei Stellen**: `POST …/issues` +beim Anlegen und `PATCH`/`PUT …/issues/{nr}` beim Schließen oder Wiederöffnen (4.6). Wer +darauf verzichtet, kommt mit einem rein lesenden Token aus. | System | Nur Abgleich | Zusätzlich für die Issue-Anbindung | | --- | --- | --- | @@ -496,6 +497,10 @@ von Issues (4.6) — wer darauf verzichtet, kommt mit einem rein lesenden Token | GitLab | `read_api` | `api` | | Gitea / Forgejo | `read:repository`, `read:issue`, `read:user` | `write:issue` | +Welcher Aufruf welches Recht verlangt, steht Zeile für Zeile in der +[README](README.md#welche-rechte-der-token-braucht) — dort ist jeder ausgehende Aufruf +der drei Clients aufgeführt. + **GitLab kennt keinen schmalen Schreib-Scope.** `api` erlaubt alles, was das Konto darf, Code eingeschlossen. Wem das zu weit geht, bleibt bei `read_api`: der Abgleich läuft vollständig, geschlossene Issues werden weiterhin erkannt — nur schreibt Nebula dann @@ -505,6 +510,11 @@ nicht zurück. Webhooks. Der Haken wird von Hand im Git-System eingetragen (4.7); das erspart dem Token genau die Administrationsrechte, die er sonst bräuchte. +**Kommentare schreibt Nebula nie** — weder beim Anlegen noch beim Schließen. In den +Rumpf des angelegten Issues wandert die Aufgabenbeschreibung plus eine Zeile Rückverweis +auf das Projekt; danach bleibt der Text unangetastet. Was sonst an Kommentaren und +Labels an einem Issue hängt, kommt aus der Weboberfläche oder von einem anderen Werkzeug. + Ein fehlendes Recht endet in einem `403` mit „Token hat nicht die noetigen Scopes", sichtbar am Repository auf der Projektseite. Klein anfangen und gezielt nachlegen ist deshalb ohne Risiko. diff --git a/README.md b/README.md index a8bec2e..190d9f2 100644 --- a/README.md +++ b/README.md @@ -188,8 +188,10 @@ Ohne Token gehen nur öffentliche Repositories, mit strengem Rate-Limit. ### Welche Rechte der Token braucht -Nebula liest fast alles nur — geschrieben wird ausschließlich, wenn du eine Aufgabe als -Issue anlegst oder abhakst. Deshalb lohnt es, zwei Fälle zu unterscheiden: +Nebula liest fast alles nur. Geschrieben wird an **genau zwei Stellen**: wenn du eine +Aufgabe als Issue anlegst, und wenn eine Aufgabe auf `DONE` wandert und Nebula das +verknüpfte Issue schließt. Alles andere sind `GET`-Aufrufe. Deshalb lohnt es, zwei Fälle +zu unterscheiden: **Fall A — nur Abgleich.** Branches, Tags, Commits, Releases, CI-Zustand, README, Heatmap, offene Issues und PRs anzeigen. Rein lesend. @@ -203,29 +205,112 @@ Issue geschlossen, hakt Nebula die Aufgabe ab und schließt sie umgekehrt mit. | **GitLab** | `read_api` | `api` — **es gibt nichts dazwischen** | | **Gitea / Forgejo** | `read:repository`, `read:issue`, `read:user` | `write:issue` | -Wozu die einzelnen Rechte gut sind: +#### Jeder Aufruf einzeln -| Recht | Wofür | -| --- | --- | -| Repository lesen (`Contents`, `read:repository`) | Branches, Tags, Commits, Releases, Sprachen, README, Commit-Heatmap | -| Issues lesen | Offene Issues auf der Projektseite, „Issues als Aufgaben übernehmen" | -| Pull Requests lesen | Offene PRs auf der Projektseite | -| Actions lesen (nur GitHub) | CI-Zustand des Default-Branch. Bei GitLab kommt er aus den Pipelines (`read_api`), bei Gitea aus dem Commit-Status (`read:repository`) | -| Benutzer lesen | Nur für den Verbindungstest — Nebula meldet zurück, als wer der Token gilt | -| Issues schreiben | Aufgabe als Issue anlegen, schließen, wieder öffnen | +Die folgenden Tabellen sind vollständig: mehr als das ruft Nebula nicht nach draußen. +Sie sind aus `src/lib/git/github.ts`, `gitlab.ts` und `gitea.ts` erhoben — wer prüfen +will, ob hier etwas fehlt, greppt dort nach `getJson`, `getJsonSafe` und `sendJson`. -> **GitLab ist der unangenehme Fall.** Es kennt keinen schmalen Schreib-Scope für Issues: -> `api` erlaubt alles, was dein Konto darf — auch Code ändern. Wenn dir das zu weit geht, -> nimm `read_api` und verzichte auf die Issue-Anbindung. Der Abgleich funktioniert -> vollständig, und geschlossene Issues werden trotzdem erkannt — nur eben nur gelesen. +**GitHub** — `https://api.github.com`, Header `Authorization: Bearer …` -**Was Nebula *nicht* braucht:** Schreibrechte auf Code, Adminrechte, Rechte zum Anlegen -von Webhooks. Den Haken für Webhooks trägst du selbst im Git-System ein — genau deshalb -verlangt Nebula dafür kein zusätzliches Recht. +| Aufruf | Wofür | Benötigtes Recht | +| --- | --- | --- | +| `GET /rate_limit` | Verbindungstest | keins | +| `GET /user` | Kontoname beim Testen zurückmelden | keins | +| `GET /user/repos` | Auswahlliste hinter **Repos laden** | Metadata: Read | +| `GET /repos/{o}/{r}` | Stammdaten, Default-Branch, Sichtbarkeit | Metadata: Read | +| `GET …/languages` | Sprachverteilung | Metadata: Read | +| `GET …/tags` | Tags | Metadata: Read | +| `GET …/stats/commit_activity` | Commit-Heatmap über ein Jahr | Metadata: Read | +| `GET …/branches` | Branch-Liste | Contents: Read | +| `GET …/commits` | die letzten 100 Commits | Contents: Read | +| `GET …/releases` | Releases | Contents: Read | +| `GET …/readme` | README-Vorschau | Contents: Read | +| `GET …/pulls?state=open` | offene Pull Requests | Pull requests: Read | +| `GET …/issues?state=open` | offene Issues | Issues: Read | +| `GET …/issues/{nr}` | Abgleich: ist das verknüpfte Issue noch offen? | Issues: Read | +| `GET …/actions/runs` | CI-Zustand des Default-Branch | Actions: Read | +| `POST …/issues` | **Aufgabe als Issue anlegen** | Issues: Read and write | +| `PATCH …/issues/{nr}` | **Issue schließen oder wieder öffnen** | Issues: Read and write | -**Im Zweifel klein anfangen.** Ein fehlendes Recht führt zu einem `403` mit dem Hinweis -„Token hat nicht die noetigen Scopes"; die Meldung steht am Repository auf der -Projektseite. Dann gezielt nachlegen, statt vorsorglich alles zu vergeben. +Dass `/tags` und `/stats/commit_activity` unter *Metadata* fallen und nicht unter +*Contents*, ist nicht zu erraten — es steht so in +[GitHubs Berechtigungsübersicht](https://docs.github.com/en/rest/authentication/permissions-required-for-fine-grained-personal-access-tokens). + +**GitLab** — `{Basis-URL}/api/v4`, Header `PRIVATE-TOKEN: …` + +| Aufruf | Wofür | Benötigtes Recht | +| --- | --- | --- | +| `GET /projects?per_page=1` | Verbindungstest | `read_api` | +| `GET /user` | Kontoname beim Testen zurückmelden | `read_user` (in `read_api` enthalten) | +| `GET /projects?membership=true` | Auswahlliste hinter **Repos laden** | `read_api` | +| `GET /projects/{id}?statistics=true` | Stammdaten, Default-Branch, Sichtbarkeit | `read_api` | +| `GET …/repository/branches` · `/tags` · `/commits` | Snapshot und Heatmap | `read_api` | +| `GET …/repository/tree` · `/repository/files/{datei}` | README (zwei Aufrufe, GitLab hat keinen README-Endpunkt) | `read_api` | +| `GET …/releases` · `/languages` | Releases, Sprachverteilung | `read_api` | +| `GET …/merge_requests?state=opened` | offene Merge Requests | `read_api` | +| `GET …/issues?state=opened` | offene Issues | `read_api` | +| `GET …/issues/{nr}` | Abgleich: ist das verknüpfte Issue noch offen? | `read_api` | +| `GET …/pipelines` | CI-Zustand des Default-Branch | `read_api` | +| `POST …/issues` | **Aufgabe als Issue anlegen** | `api` | +| `PUT …/issues/{nr}` (`state_event`) | **Issue schließen oder wieder öffnen** | `api` | + +**Gitea / Forgejo** — `{Basis-URL}/api/v1`, Header `Authorization: token …` + +| Aufruf | Wofür | Benötigtes Recht | +| --- | --- | --- | +| `GET /version` | Verbindungstest | keins — der Aufruf darf fehlschlagen, Nebula wertet ihn nur ergänzend aus | +| `GET /user` | Kontoname beim Testen zurückmelden | `read:user` | +| `GET /user/repos` bzw. `GET /repos/search` | Auswahlliste hinter **Repos laden** | `read:repository` | +| `GET /repos/{o}/{r}` | Stammdaten, Default-Branch, Sichtbarkeit | `read:repository` | +| `GET …/branches` · `/tags` · `/commits` · `/releases` · `/languages` | Snapshot und Heatmap | `read:repository` | +| `GET …/contents` · `/contents/{datei}` | README (zwei Aufrufe, wie bei GitLab) | `read:repository` | +| `GET …/commits/{branch}/status` | CI-Zustand des Default-Branch | `read:repository` | +| `GET …/pulls?state=open` | offene Pull Requests | `read:repository` — Gitea zählt PRs zum Repository-Scope, nicht zu den Issues | +| `GET …/issues?state=open&type=issues` | offene Issues | `read:issue` | +| `GET …/issues/{nr}` | Abgleich: ist das verknüpfte Issue noch offen? | `read:issue` | +| `POST …/issues` | **Aufgabe als Issue anlegen** | `write:issue` | +| `PATCH …/issues/{nr}` | **Issue schließen oder wieder öffnen** | `write:issue` | + +`write:issue` schließt `read:issue` mit ein — Gitea versteht `write:` als „lesen und +schreiben". Beide anzuhaken schadet nicht, nötig ist es nicht. + +#### Was Nebula nicht tut + +- **Keine Kommentare.** Nebula schreibt nie einen Issue-Kommentar, weder beim Anlegen + noch beim Schließen. Was im Rumpf des angelegten Issues steht, ist die + Aufgabenbeschreibung plus eine Zeile Rückverweis — danach fasst Nebula den Text + nicht mehr an. +- **Kein Code.** Nichts wird committet, gepusht, gemergt oder getaggt. Nebula ist eine + Übersicht, keine Entwicklungsumgebung. +- **Keine Webhooks anlegen.** Den Haken trägst du selbst im Git-System ein — genau + deshalb verlangt Nebula dafür keine Administrationsrechte. +- **Keine Labels, keine Meilensteine, keine Zuweisungen.** +- **Keine Adminrechte**, keine Organisations- oder Benutzerverwaltung. + +Kommentare und Labels, die an einem Issue hängen, stammen also entweder aus der +Weboberfläche deines Git-Systems oder von einem anderen Werkzeug mit eigenem Token — +nicht von Nebula. + +#### Der GitLab-Sonderfall + +GitLab kennt keinen schmalen Schreib-Scope für Issues. Laut +[Access token scopes](https://docs.gitlab.com/security/tokens/access_token_scopes/) +gibt es nur `api` — „complete read and write access to the API" — und darunter direkt +`read_api`. Wer bei GitLab Aufgaben als Issue anlegen will, vergibt damit ein Token, das +auch Code ändern darf. + +Wenn dir das zu weit geht: bleib bei `read_api` und verzichte auf die Issue-Anbindung. +Der Abgleich läuft vollständig, und geschlossene Issues werden weiterhin erkannt und +haken ihre Aufgabe ab — nur der Rückweg von Nebula ins Issue fällt weg. Ein eigenes +Dienstkonto, das nur Zugriff auf die betroffenen Projekte hat, ist der andere Ausweg. + +#### Im Zweifel klein anfangen + +Ein fehlendes Recht führt zu einem `403` mit dem Hinweis „Token hat nicht die noetigen +Scopes"; die Meldung steht am Repository auf der Projektseite. Fang mit Fall A an und +leg gezielt nach, wenn du die Issue-Anbindung wirklich benutzt — statt vorsorglich +alles zu vergeben. ### Automatisch statt auf Knopfdruck diff --git a/src/lib/git/meta.ts b/src/lib/git/meta.ts index 26cd4b7..eb1fd48 100644 --- a/src/lib/git/meta.ts +++ b/src/lib/git/meta.ts @@ -12,8 +12,9 @@ export const PROVIDER_META: Record< label: "GitHub", defaultBaseUrl: "https://github.com", tokenHelp: - "Fine-grained Token: Metadata, Contents und Pull requests lesen, Issues lesen. " + - "Nur wenn Aufgaben als Issue angelegt werden sollen, Issues zusaetzlich schreiben.", + "Fine-grained Token, alle nur lesend: Metadata, Contents, Issues, Pull requests " + + "und Actions. Nur wenn Aufgaben als Issue angelegt werden sollen, Issues " + + "zusaetzlich schreiben.", docsUrl: "https://github.com/settings/tokens", }, GITLAB: { diff --git a/src/lib/git/schreibzugriffe.test.ts b/src/lib/git/schreibzugriffe.test.ts new file mode 100644 index 0000000..f8193d9 --- /dev/null +++ b/src/lib/git/schreibzugriffe.test.ts @@ -0,0 +1,60 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +/** + * Waechter ueber eine Zusage in der Dokumentation. + * + * README und FEATURES sagen zu, dass Nebula an genau zwei Stellen nach + * draussen schreibt: ein Issue anlegen und seinen Zustand umsetzen. Auf + * dieser Zusage steht die Empfehlung, ein rein lesendes Token zu vergeben, + * solange die Issue-Anbindung nicht gebraucht wird. + * + * Kaeme ein dritter Schreibaufruf dazu, wuerde die Empfehlung still falsch: + * niemand bekaeme einen Fehler, nur ein 403 an einer Stelle, die laut + * Dokumentation gar nicht schreibt. Deshalb wird hier der Quelltext selbst + * abgeklopft – sendJson ist der einzige Weg aus lib/git heraus, der eine + * andere Methode als GET benutzt (siehe http.ts). + */ +const CLIENTS = ["gitea.ts", "github.ts", "gitlab.ts"] as const; + +/** Die Zeile ueber dem Methoden-Argument nennt den Endpunkt. */ +function schreibaufrufe(quelltext: string): string[] { + const zeilen = quelltext.split("\n"); + const gefunden: string[] = []; + zeilen.forEach((zeile, i) => { + if (!/\bsendJson[<(]/.test(zeile)) return; + // sendJson(url, METHODE, …) – Endpunkt und Methode stehen darunter. + const block = zeilen.slice(i, i + 4).join(" "); + const methode = block.match(/"(POST|PATCH|PUT)"/)?.[1]; + const pfad = block.match(/\/issues[^`"']*/)?.[0] ?? "?"; + gefunden.push(`${methode ?? "?"} ${pfad}`); + }); + return gefunden; +} + +describe("Schreibzugriffe nach draussen", () => { + it("beschraenken sich auf Issues anlegen und Zustand setzen", () => { + for (const datei of CLIENTS) { + const quelltext = readFileSync(join(__dirname, datei), "utf8"); + const aufrufe = schreibaufrufe(quelltext); + + expect(aufrufe.length, `${datei}: Anzahl Schreibaufrufe`).toBe(2); + for (const aufruf of aufrufe) { + expect(aufruf, `${datei}: ${aufruf}`).toMatch(/^(POST|PATCH|PUT) \/issues/); + } + // Genau einer legt an, genau einer setzt um. + expect(aufrufe.filter((a) => a.startsWith("POST")).length, datei).toBe(1); + expect(aufrufe.filter((a) => /^(PATCH|PUT)/.test(a)).length, datei).toBe(1); + } + }); + + it("laufen ueber sendJson, nicht an http.ts vorbei", () => { + // Ein direkter fetch in einem Client haette weder SSRF-Schutz noch + // Zeitbegrenzung – und wuerde am Waechter oben vorbeilaufen. + for (const datei of CLIENTS) { + const quelltext = readFileSync(join(__dirname, datei), "utf8"); + expect(quelltext, datei).not.toMatch(/\bfetch\s*\(/); + } + }); +});