diff --git a/README.md b/README.md index 4e96087..98afb98 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ Podstawowe komendy tokenow: ```bash ./rvctl tokens scan +./rvctl tokens list store +./rvctl tokens list remote ./rvctl tokens sync remote r1 ./rvctl tokens update r1 ./rvctl tokens sync store r1 @@ -168,6 +170,7 @@ Podstawowe komendy tokenow: `tokens scan` wypisuje tabele `tokens` i niczego nie zapisuje: jeden wiersz na logiczny remote tokena. +`tokens list store|remote|both` wypisuje jedno zrodlo bez porownywania. `tokens.json` synchronizujemy z repo `rv-launcher`; remotes kart pracy i odpowiedzi sa generowane jako pochodne tego ustawienia. `token_ref` laczy nazwe tokena z markerem po prawej stronie: `*` oznacza, ze diff --git a/doc/rvctl.md b/doc/rvctl.md index 6c15079..9bb8694 100644 --- a/doc/rvctl.md +++ b/doc/rvctl.md @@ -331,6 +331,37 @@ Przyklad: ./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03 ``` +## `tokens list store|remote|both` + +Wypisuje jedno zrodlo bez porownywania go z drugim. `list` jest read-only: +pokazuje co jest w `tokens.json`, co jest w git remote albo oba zrodla jako +osobne wiersze. `scan` sluzy do porownania zgodnosci. + +Przelaczniki: + +- `--repo PATH` + Repo, z ktorego listowane sa git remotes. Domyslnie repo zawierajace `rvctl`. +- `--server ENDPOINT` + Ogranicza wynik do jednego endpointu. + +Typowy wynik: + +```text +tokens +item source kind server proto host org repo user remote token valid scope org repo +---- ------ ----- ------ ----- ------------------ --------- ----------- ---- ------ ------------ ------------------- aAimnopru oawrc- oawr-- +1 store auth gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 e59cc...13be forever --------- +++++ ++++ +2 remote auth gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 e59cc...13be +``` + +Przyklady: + +```bash +./rvctl tokens list store +./rvctl tokens list remote +./rvctl tokens list both +``` + ## `tokens add REMOTE_ID` Dodaje pusty szkielet tokena do `tokens.json`. `REMOTE_ID` musi byc taki sam diff --git a/doc/tokens.md b/doc/tokens.md index 9c56a59..baa47fb 100644 --- a/doc/tokens.md +++ b/doc/tokens.md @@ -1,31 +1,41 @@ # Tokens -Plik opisuje model pracy z tokenami w launcherze. +Ten dokument opisuje model tokenow uzywany przez `rvctl`. -## Zrodlo prawdy +## Zasada -Sa dwa miejsca, w ktorych moga byc zapisane dane dostepowe: +`tokens.json` synchronizujemy z repo `rv-launcher`. Remoty kart pracy i repo +odpowiedzi sa generowane jako pochodne konfiguracji launchera, a nie jako osobne +zrodla prawdy dla tokenow. -- remote URL w repo, na przyklad `http://u1:SECRET@host/org/repo.git` +W praktyce oznacza to: + +- `r1` w repo `rv-launcher` jest miejscem startowym do pobrania tokena z remote +- `tokens.json` jest lokalnym store sekretow i metadanych tokenow +- karty pracy dostaja remote na podstawie ustawien launchera i komend typu `submission` +- `tokens scan` jest read-only i niczego nie zapisuje + +## Zrodlo Prawdy + +Sa dwa miejsca, ktore `rvctl` potrafi porownac: + +- git remote w repo `rv-launcher`, np. `http://u1:SECRET@host/org/repo.git` - lokalny plik `~/dev/workspace/rv/tokens/tokens.json` -`tokens scan` tylko porownuje oba miejsca i niczego nie zapisuje. -`tokens sync remote r1` czyta remote `r1` i zapisuje jego dane w -`tokens.json`. -`tokens sync store r1` zapisuje rekord `r1` z `tokens.json` do git remote -`r1`. -`tokens update r1` nie synchronizuje sekretu; tylko pobiera z API metadane -tokenu: `valid`, `scope`, `org_perm` i `repo_perm`. +Kierunek jest jawny: -W podstawowym flow `tokens.json` synchronizujemy z repo `rv-launcher`. Remoty -kart pracy i repo odpowiedzi sa generowane jako pochodne konfiguracji launchera, -nie jako osobne zrodla prawdy dla tokenow. +- `tokens list store` pokazuje tylko rekordy w `tokens.json` +- `tokens list remote` pokazuje tylko git remotes +- `tokens sync remote r1` czyta git remote `r1` i zapisuje rekord do `tokens.json` +- `tokens sync store r1` czyta `tokens.json` i zapisuje auth do git remote `r1` +- `tokens update r1` nie synchronizuje sekretu, tylko pobiera metadane z API +- `tokens remove ... r1` usuwa rekord ze store, git remote albo oba miejsca ## Format `tokens.json` -Aktualny format to `version: 3`. Glownym rekordem jest remote-token. Pole -`id` jest obowiazkowe i musi byc takie samo jak nazwa git remote, np. `r1` -albo `r1a`. +Aktualny format to `version: 3`. Glownym rekordem jest jeden remote-token. +Pole `id` jest obowiazkowe i musi byc takie samo jak nazwa git remote, np. +`r1` albo `r1a`. ```json { @@ -74,30 +84,30 @@ albo `r1a`. } ``` -Schemat JSON jest w pliku `doc/tokens.schema.json`. +Pelny schemat jest w `doc/tokens.schema.json`. ## Pola -Pola synchronizowane z remote URL-a: +Pola synchronizowane z git remote: -- `id` - nazwa git remote, np. `r1`; to jest klucz rekordu w `tokens.json` -- `server.endpoint` - endpoint serwera, na przyklad `http://77.90.8.171:3001` -- `server.type` - typ serwera, na przyklad `gitea`, `github`, `gitlab`, `unknown` +- `id` - nazwa git remote, np. `r1`; to jest klucz rekordu +- `server.endpoint` - endpoint serwera, np. `http://77.90.8.171:3001` +- `server.type` - typ serwera, np. `gitea`, `github`, `gitlab`, `unknown` - `user` - login z URL-a, czyli lewa strona `http://u1:SECRET@...` - `value` - sekret tokena - `org` - organizacja z URL-a - `repo` - repo z URL-a -Pola wzbogacane przez API przy `tokens update r1`: +Pola pobierane z API przez `tokens update r1`: -- `valid` - `forever`, data `expires_at`, `invalid`, `?` albo `!` +- `valid` - `forever`, data wygasniecia, `invalid`, `?` albo `!` - `scope` - mapa scope tokena -- `org_perm` - mapa praw w organizacji -- `repo_perm` - mapa praw w repo +- `org_perm` - mapa praw uzytkownika w organizacji +- `repo_perm` - mapa praw uzytkownika w repo ## Porownanie -Porownanie z `git remote -v` jest robione po: +`tokens scan` porownuje git remote i `tokens.json` po: ```text server.endpoint + id + user + value + org + repo @@ -106,17 +116,17 @@ server.endpoint + id + user + value + org + repo Znacznik w kolumnie `token_ref`: - `*` - remote i `tokens.json` sa zgodne -- `R` - wpis istnieje tylko w remote URL-u +- `R` - wpis istnieje tylko w git remote - `S` - wpis istnieje tylko w `tokens.json` -- `!` - remote i `tokens.json` sa zgodne, ale zapisany token jest `invalid` albo ma blad walidacji +- `!` - wpis jest sparowany, ale token jest `invalid` albo ma blad sprawdzania -Bez zgodnego wpisu kolumny `valid`, `scope`, `org` i `repo` w raporcie maja -wartosc `?`, bo launcher nie pokazuje metadanych API dla niesparowanych wpisow. +Dla niesparowanych wpisow `rvctl` pokazuje `?` w kolumnach `valid`, `scope`, +`org` i `repo`, bo metadane API sa wiarygodne dopiero dla rekordu ze store. ## Uprawnienia W `tokens.json` uprawnienia sa zapisane jako mapy klucz-wartosc. W tabeli CLI sa -pokazywane skrocone maski. +pokazywane jako zwarte maski. Naglowki masek: @@ -136,7 +146,7 @@ Kategorie `scope`: - `r` - repository - `u` - user -Znaki w `scope`: +Wartosci w `scope`: - `w` - read/write - `r` - read @@ -159,25 +169,80 @@ Kategorie `repo_perm`: - `w` - write - `r` - read -Znaki w `org_perm` i `repo_perm`: +Wartosci w `org_perm` i `repo_perm`: - `+` - flaga wlaczona - `-` - flaga wylaczona - `?` - nie wczytano - `!` - blad wczytania +## Typowy Flow + +W repo `rv-launcher`: + +```bash +git remote add r1 http://u1:TOKEN@77.90.8.171:3001/edu-tools/rv-launcher.git +./rvctl tokens scan +./rvctl tokens sync remote r1 +./rvctl tokens update r1 +./rvctl tokens scan +``` + +Po `sync remote` sekret jest w `tokens.json`. Po `update` `rvctl` dopisuje +`valid`, `scope`, `org_perm` i `repo_perm`. + +Jesli chcesz tylko zobaczyc jedno zrodlo, bez porownywania: + +```bash +./rvctl tokens list remote +./rvctl tokens list store +./rvctl tokens list both +``` + +Jesli remote ma byc bez sekretu, a token ma zostac tylko w `tokens.json`: + +```bash +./rvctl tokens remove remote r1 +git remote add r1 http://77.90.8.171:3001/edu-tools/rv-launcher.git +``` + +Jesli trzeba ponownie wpisac sekret ze store do remota: + +```bash +./rvctl tokens sync store r1 +``` + ## Komendy ### `tokens scan` -Read-only. Czyta remote URL-e i `tokens.json`, a potem wypisuje tabele -`tokens`. Nie tworzy i nie modyfikuje `tokens.json`. +Read-only. Czyta git remote i `tokens.json`, laczy wpisy w pary i wypisuje +tabele `tokens`. Nie tworzy i nie modyfikuje `tokens.json`. Bez `--repo` +czyta repo zawierajace `rvctl`. ```bash ./rvctl tokens scan -./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens scan --repo ~/dev/workspace/rv/tools/rv-launcher ``` +### `tokens list store|remote|both` + +Read-only. Wypisuje jedno zrodlo bez porownywania go z drugim. To odroznia +`list` od `scan`: `list` odpowiada na pytanie "co jest zapisane tutaj", a +`scan` odpowiada na pytanie "czy store i remote sa zgodne". + +```bash +./rvctl tokens list store +./rvctl tokens list remote +./rvctl tokens list both +./rvctl tokens list remote --repo ~/dev/workspace/rv/tools/rv-launcher +``` + +Przelaczniki: + +- `--repo PATH` - repo, z ktorego listowane sa git remotes; bez tej opcji uzywane jest repo zawierajace `rvctl` +- `--server ENDPOINT` - ogranicza wynik do jednego endpointu + ### `tokens sync remote REMOTE_ID` Kopiuje dane dostepowe z git remote do `tokens.json`. Nie pobiera metadanych z @@ -190,7 +255,7 @@ API. Bez `--repo` czyta repo zawierajace `rvctl`. ### `tokens update REMOTE_ID` -Pobiera z API metadane dla rekordu `REMOTE_ID` juz zapisanego w `tokens.json`. +Pobiera z API metadane dla rekordu `REMOTE_ID` zapisanego w `tokens.json`. Nie zmienia git remote URL-a i nie kopiuje sekretu z remote. ```bash @@ -204,21 +269,19 @@ Zapisuje dane z rekordu `REMOTE_ID` w `tokens.json` do git remote o tej samej nazwie. Bez `--repo` zapisuje do repo zawierajacego `rvctl`. ```bash -./rvctl tokens sync store r1 --repo ~/dev/workspace/rv/series/inf/03 -./rvctl tokens sync store r1 --repo PATH --replace +./rvctl tokens sync store r1 +./rvctl tokens sync store r1 --replace ``` ### `tokens remove store|remote|both REMOTE_ID` -Usuwa rekord `REMOTE_ID` z `tokens.json`, git remote albo oba miejsca. -Domyslnym repo dla `remote` i `both` jest repo, w ktorym lezy `rvctl`. Inne -repo mozna wskazac przez `--repo PATH`. +Usuwa rekord ze store, git remote albo oba miejsca. Bez `--repo` operacje +`remote` i `both` dzialaja na repo zawierajacym `rvctl`. ```bash ./rvctl tokens remove store r1 ./rvctl tokens remove remote r1 ./rvctl tokens remove both r1 -./rvctl tokens remove remote r1 --repo ~/dev/workspace/rv/series/inf/03 ``` Przelaczniki: @@ -252,15 +315,18 @@ nazwie git remote. ### `tokens stats` -Pokazuje kontekst, tabele `tokens` i podsumowanie statusow endpointow. +Pokazuje kontekst, tabele `tokens` i podsumowanie statusow endpointow. Bez +`--repo` czyta repo zawierajace `rvctl`. ```bash -./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03 +./rvctl tokens stats +./rvctl tokens stats --repo ~/dev/workspace/rv/tools/rv-launcher ``` ### Komendy zgodnosci -Stare komendy nadal dzialaja, ale sa mniej jednoznaczne: +Stare komendy nadal dzialaja, ale w nowych instrukcjach preferujemy jawne +`sync remote`, `sync store` i `update REMOTE_ID`. ```bash ./rvctl tokens update --from remotes --repo PATH diff --git a/rvctl.py b/rvctl.py index 9cb0f3c..88f6ef0 100644 --- a/rvctl.py +++ b/rvctl.py @@ -1277,6 +1277,24 @@ TOKEN_COLUMNS = [ ("repo_mask", "repo", 6), ] +TOKEN_LIST_COLUMNS = [ + ("item", "item", 4), + ("source", "source", 6), + ("kind", "kind", 5), + ("server", "server", 6), + ("proto", "proto", 5), + ("host", "host", 18), + ("owner", "org", 9), + ("repo_name", "repo", 11), + ("user", "user", 4), + ("remote", "remote", 6), + ("token", "token", 12), + ("valid", "valid", 19), + ("scope_mask", "scope", 9), + ("org_mask", "org", 6), + ("repo_mask", "repo", 6), +] + TOKEN_SEPARATOR_OVERRIDES = { "scope_mask": "aAimnopru", "org_mask": "oawrc-", @@ -1409,6 +1427,94 @@ def repo_token_rows( return rows +def list_store_rows(store_servers: dict[str, dict], server_filter: str | None = None) -> list[dict[str, str]]: + rows: list[dict[str, str]] = [] + endpoints = sorted(store_servers) + for endpoint in endpoints: + if server_filter and endpoint != server_filter: + continue + server_entry = store_servers[endpoint] + endpoint_values = endpoint_column_values(endpoint, None, server_entry) + for row in sorted(server_entry.get("tokens", []), key=lambda item: item.get("token_id", "")): + token_value = str(row.get("token_value", "")) + rows.append( + { + "item": str(len(rows) + 1), + "source": "store", + "kind": "auth" if token_value else "empty", + "server": endpoint_values["server"], + "proto": endpoint_values["scheme"], + "host": endpoint_values["host"], + "owner": str(row.get("owner", "")), + "repo_name": str(row.get("repo_name", "")), + "user": str(row.get("user", "")), + "remote": str(row.get("remote", "")), + "token": short_secret(token_value) if token_value else "", + "valid": str(row.get("valid", "")), + "scope_mask": str(row.get("scope_mask", "")), + "org_mask": str(row.get("org_mask", "")), + "repo_mask": str(row.get("repo_mask", "")), + } + ) + return rows + + +def list_remote_rows(repo_servers: dict[str, dict], server_filter: str | None = None) -> list[dict[str, str]]: + rows: list[dict[str, str]] = [] + endpoints = sorted(repo_servers) + for endpoint in endpoints: + if server_filter and endpoint != server_filter: + continue + server_entry = repo_servers[endpoint] + endpoint_values = endpoint_column_values(endpoint, server_entry, None) + remote_rows = [] + for row in server_entry.get("tokens", []): + remote_rows.append( + { + "kind": "auth", + "remote": row.get("remote", ""), + "owner": row.get("org", ""), + "repo_name": row.get("repo", ""), + "user": row.get("user", ""), + "token_value": row.get("token_value", ""), + } + ) + for row in server_entry.get("plain_remotes", []): + remote_rows.append( + { + "kind": "plain", + "remote": row.get("remote", ""), + "owner": row.get("org", ""), + "repo_name": row.get("repo", ""), + "user": "", + "token_value": "", + } + ) + + for row in sorted(remote_rows, key=lambda item: (item.get("remote", ""), item.get("kind", ""))): + token_value = str(row.get("token_value", "")) + rows.append( + { + "item": str(len(rows) + 1), + "source": "remote", + "kind": str(row.get("kind", "")), + "server": endpoint_values["server"], + "proto": endpoint_values["scheme"], + "host": endpoint_values["host"], + "owner": str(row.get("owner", "")), + "repo_name": str(row.get("repo_name", "")), + "user": str(row.get("user", "")), + "remote": str(row.get("remote", "")), + "token": short_secret(token_value) if token_value else "", + "valid": "", + "scope_mask": "", + "org_mask": "", + "repo_mask": "", + } + ) + return rows + + def fallback_project_by_endpoint( endpoint_names: list[str], repo_servers: dict[str, dict], @@ -1750,7 +1856,7 @@ def safe_remote_url_label(remote_url: str | None) -> str: def run_tokens_scan(config: WorkspaceConfig, args: argparse.Namespace) -> None: - repo_path = resolve_repo_argument(args.repo) + repo_path = resolve_launcher_repo_argument(args.repo) token_data = load_token_store(config, write_normalized=False) store_servers = store_servers_from_tokens(token_data) report = scan_repo_remotes(config, repo_path) @@ -1759,6 +1865,24 @@ def run_tokens_scan(config: WorkspaceConfig, args: argparse.Namespace) -> None: print_token_item_rows(endpoint_names, repo_servers, store_servers, report["remote_rows"]) +def run_tokens_list(config: WorkspaceConfig, args: argparse.Namespace) -> None: + rows: list[dict[str, str]] = [] + if args.target in {"store", "both"}: + token_data = load_token_store(config, write_normalized=False) + store_servers = store_servers_from_tokens(token_data) + rows.extend(list_store_rows(store_servers, args.server)) + + if args.target in {"remote", "both"}: + repo_root, repo_servers = collect_repo_server_entries(config, resolve_launcher_repo_argument(args.repo)) + if repo_root is None: + raise SystemExit(f"Missing git repo at path: {resolve_launcher_repo_argument(args.repo)}") + rows.extend(list_remote_rows(repo_servers, args.server)) + + for index, row in enumerate(rows, start=1): + row["item"] = str(index) + print_fixed_table("tokens", TOKEN_LIST_COLUMNS, rows) + + def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None: token_data = load_token_store(config, write_normalized=False) servers = token_store_servers(token_data) @@ -1804,7 +1928,7 @@ def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None: def run_tokens_stats(config: WorkspaceConfig, args: argparse.Namespace) -> None: - repo_root, repo_servers = collect_repo_server_entries(config, resolve_repo_argument(args.repo)) + repo_root, repo_servers = collect_repo_server_entries(config, resolve_launcher_repo_argument(args.repo)) token_data = load_token_store(config, write_normalized=False) store_servers = store_servers_from_tokens(token_data) endpoint_names = sorted(set(repo_servers) | set(store_servers)) @@ -2345,7 +2469,7 @@ def print_main_overview(config_path: Path) -> None: ["list-cards", "[series]", "list cards in a selected series"], ["tmux-container", "[series] [card]", "start tmux with container in pane 0"], ["submission", "[series] [card] --class K --nick N", "prepare answer repo and student branch"], - ["tokens", "scan|sync|update|remove|add|read|stats|write", "manage tokens.json and Git remote credentials"], + ["tokens", "scan|list|sync|update|remove|add|read|stats|write", "manage tokens.json and Git remote credentials"], ], ) print() @@ -2382,6 +2506,7 @@ def print_tokens_overview() -> None: ["command", "common options", "direction", "purpose"], [ ["scan", "[--repo PATH]", "read-only", "print token table with remote/store marker and auth masks"], + ["list", "store|remote|both", "read-only", "list one source without comparing it"], ["add", "REMOTE_ID", "tokens.json", "add an empty token skeleton for manual editing"], ["read", "[--server ENDPOINT]", "tokens.json", "show servers, remote ids and tokens"], ["stats", "[--repo PATH]", "repo + tokens.json", "compare remote URLs with local token store"], @@ -2397,7 +2522,9 @@ def print_tokens_overview() -> None: print_table( ["case", "command"], [ - ["scan current repo", "./rvctl tokens scan"], + ["scan launcher repo", "./rvctl tokens scan"], + ["list token store", "./rvctl tokens list store"], + ["list launcher remotes", "./rvctl tokens list remote"], ["read remote r1 into store", "./rvctl tokens sync remote r1"], ["refresh r1 metadata", "./rvctl tokens update r1"], ["write store r1 to remote", "./rvctl tokens sync store r1 --repo PATH"], @@ -2500,7 +2627,21 @@ def build_parser() -> argparse.ArgumentParser: "scan", help="Read repo remotes and tokens.json, then print paired token rows.", ) - add_repo_option(tokens_scan_parser) + tokens_scan_parser.add_argument( + "--repo", + help="Path inside a target git repo. Default: repo containing rvctl.", + ) + + tokens_list_parser = tokens_subparsers.add_parser( + "list", + help="List token store records, Git remotes, or both without comparing them.", + ) + tokens_list_parser.add_argument("target", choices=["store", "remote", "both"], help="Source to list.") + tokens_list_parser.add_argument( + "--repo", + help="Path inside a target git repo for target remote/both. Default: repo containing rvctl.", + ) + tokens_list_parser.add_argument("--server", help="Filter output to one server endpoint.") tokens_read_parser = tokens_subparsers.add_parser( "read", @@ -2526,7 +2667,10 @@ def build_parser() -> argparse.ArgumentParser: "stats", help="Print per-server token statistics from tokens.json.", ) - add_repo_option(tokens_stats_parser) + tokens_stats_parser.add_argument( + "--repo", + help="Path inside a target git repo. Default: repo containing rvctl.", + ) tokens_stats_parser.add_argument("--server", help="Filter output to one server endpoint.") tokens_sync_parser = tokens_subparsers.add_parser( @@ -2621,6 +2765,9 @@ def main() -> int: if args.tokens_command == "scan": run_tokens_scan(config, args) return 0 + if args.tokens_command == "list": + run_tokens_list(config, args) + return 0 if args.tokens_command == "add": run_tokens_add(config, args) return 0