From d2d200257f723cc4b78596d88c0f9263afce787c Mon Sep 17 00:00:00 2001 From: mpabi Date: Sun, 26 Apr 2026 21:56:07 +0200 Subject: [PATCH] Add token skeleton command --- README.md | 1 + doc/rvctl.md | 52 ++++++++++++++++++++++++------- doc/tokens.md | 13 ++++++++ doc/tokens.schema.json | 3 +- rvctl.py | 71 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 123 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 269f52f..279d2bc 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ Podstawowe komendy tokenow: ```bash ./rvctl tokens scan +./rvctl tokens add t1 ./rvctl tokens read ./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03 ``` diff --git a/doc/rvctl.md b/doc/rvctl.md index 1f142b3..dd93a90 100644 --- a/doc/rvctl.md +++ b/doc/rvctl.md @@ -291,9 +291,9 @@ Przyklady: ## `tokens scan` -Skanuje remote URL-e w repo oraz lokalny `tokens.json`, laczy wpisy w pary po -endpoincie, userze i wartosci tokena, a potem pokazuje jeden logiczny wiersz -na token. +Czyta remote URL-e w repo oraz lokalny `tokens.json`, laczy wpisy w pary po +endpoincie, nazwie remota, token id, wartosci tokena, org i repo, a potem +pokazuje jeden logiczny wiersz na token. Komenda jest read-only. Przelaczniki: @@ -304,9 +304,9 @@ Typowy wynik: ```text tokens -item server proto host org repo user remote token_ref token valid scope org repo ----- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ----- awrop- oawrc- oawr-- -1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 t1 * e59cc...13be forever +---+ +++++ ++++ +item server proto host org repo user remote token_ref token valid scope org repo +---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr-- +1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 t1 * e59cc...13be forever -----w--- +++++ ++++ ``` `token_ref` jest komorka stalej szerokosci: nazwa tokena jest po lewej, a marker @@ -316,7 +316,8 @@ oraz uprawnienia zostaly wczytane. Marker `R` oznacza token tylko w remote, a Maski uprawnien: -- `scope` ma pozycje `awrop`: `all`, `write:repository`, `read:repository`, organization scope, `public-only` +- `scope` ma pozycje `aAimnopru`: activitypub, admin, issue, misc, notification, organization, package, repository, user +- w `scope`: `w` oznacza read/write, `r` read, `-` brak dostepu - `org` ma pozycje `oawrc`: owner, admin, write, read, create repo - `repo` ma pozycje `oawr`: owner, admin, write, read - `+` oznacza wlaczone, `-` wylaczone, `?` nie wczytano, `!` blad wczytania @@ -329,6 +330,35 @@ Przyklad: ./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03 ``` +## `tokens add TOKEN_ID` + +Dodaje pusty szkielet tokena do `tokens.json`. Pole `value` jest puste i trzeba +je uzupelnic recznie przed uzyciem tokena. + +Przelaczniki: + +- `--server ENDPOINT` + Endpoint serwera. Domyslnie `git.base_url` z `workspace.json`. +- `--value TOKEN` + Opcjonalna wartosc tokena. Domyslnie pusta. +- `--user NAME` + Opcjonalny opis usera API. +- `--remote NAME` + Opcjonalny remote powiazany z tokenem. +- `--org NAME` + Opcjonalna organizacja dla remota. +- `--repo NAME` + Opcjonalne repo dla remota. +- `--dry-run` + Pokazuje plan bez zapisu. + +Przyklady: + +```bash +./rvctl tokens add r6 +./rvctl tokens add t1 --remote r1 --org edu-tools --repo rv-launcher +``` + ## `tokens read` Pokazuje zawartosc `tokens/tokens.json` w podziale na endpointy serwerow. @@ -383,9 +413,9 @@ repo_root... token_path... tokens -item server proto host org repo user remote token_ref token valid scope org repo ----- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ----- awrop- oawrc- oawr-- -1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 t1 * e59cc...13be forever +---+ +++++ ++++ +item server proto host org repo user remote token_ref token valid scope org repo +---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr-- +1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 t1 * e59cc...13be forever -----w--- +++++ ++++ status itemvalue @@ -452,7 +482,7 @@ Przelaczniki: - `--replace` Nadpisuje inne auth przy `--from store`. - `--dry-run` - Pokazuje plan bez zapisu przy `--from store`. + Pokazuje plan bez zapisu. Przyklady: diff --git a/doc/tokens.md b/doc/tokens.md index 6711b0a..3d0a1b2 100644 --- a/doc/tokens.md +++ b/doc/tokens.md @@ -161,6 +161,19 @@ Pokazuje zawartosc `tokens.json`. ./rvctl tokens read --show-secrets ``` +### `tokens add` + +Dodaje pusty szkielet tokena do `tokens.json`. Pole `value` zostaje puste, zeby +mozna bylo recznie wkleic sekret. + +```bash +./rvctl tokens add t1 +./rvctl tokens add r6 --server http://77.90.8.171:3001 +./rvctl tokens add t1 --remote r1 --org edu-tools --repo rv-launcher +``` + +`tokens write` nie uzyje pustego tokena. Najpierw trzeba uzupelnic `value`. + ### `tokens stats` Pokazuje kontekst, tabele `tokens` i podsumowanie statusow endpointow. diff --git a/doc/tokens.schema.json b/doc/tokens.schema.json index 5c71bea..6245d48 100644 --- a/doc/tokens.schema.json +++ b/doc/tokens.schema.json @@ -77,8 +77,7 @@ "minLength": 1 }, "value": { - "type": "string", - "minLength": 1 + "type": "string" }, "server": { "$ref": "#/$defs/server" diff --git a/rvctl.py b/rvctl.py index 3d19acd..d3f573b 100644 --- a/rvctl.py +++ b/rvctl.py @@ -397,10 +397,12 @@ def normalize_v3_token(config: WorkspaceConfig, raw_token: dict) -> dict | None: return None token_id = raw_token.get("token_id") or raw_token.get("id") or raw_token.get("token_ref") - token_value = raw_token.get("value") or raw_token.get("token") + token_value = raw_token.get("value") + if token_value is None: + token_value = raw_token.get("token") if not isinstance(token_id, str) or not token_id: return None - if not isinstance(token_value, str) or not token_value: + if not isinstance(token_value, str): return None server_record = normalize_server_record(config, raw_token.get("server", {})) @@ -1154,7 +1156,7 @@ def token_store_rows(store_servers: dict[str, dict]) -> list[dict[str, str]]: for row in server_entry.get("tokens", []): token_value = row.get("token_value", "") token_id = row.get("token_id", "") - if token_value and token_id: + if token_id: rows.append( { "endpoint": endpoint, @@ -1620,6 +1622,48 @@ def run_tokens_stats(config: WorkspaceConfig, args: argparse.Namespace) -> None: print_status_counts(status_counts) +def run_tokens_add(config: WorkspaceConfig, args: argparse.Namespace) -> None: + server_url = (args.server or config.git.base_url).rstrip("/") + server_info = server_info_from_url(config, server_url) + if server_info is None: + raise SystemExit(f"Only http/https server URLs are supported: {server_url}") + + token_data = load_token_store(config) + existing_token = find_token_record(token_data, server_info["endpoint"], args.token_id) + if existing_token is not None: + raise SystemExit(f"Token already exists in tokens.json: {server_info['endpoint']}:{args.token_id}") + + token_record = { + "token_id": args.token_id, + "value": args.value or "", + "server": server_record_from_info(server_info), + "remotes": [], + } + if args.user: + token_record["user"] = args.user + if args.remote: + token_record["remotes"].append( + { + "name": args.remote, + "org": args.org or "", + "repo": args.repo_name or "", + } + ) + + print(f"token_path\t{config.token_path}") + print(f"server\t{server_info['endpoint']}") + print(f"token_id\t{args.token_id}") + print(f"value\t{'set' if token_record['value'] else 'empty'}") + print(f"remote\t{args.remote or ''}") + print(f"status\t{'dry-run' if args.dry_run else 'added'}") + + if args.dry_run: + return + + token_data.setdefault("tokens", []).append(token_record) + write_token_store(config, token_data) + + def run_tokens_write(config: WorkspaceConfig, args: argparse.Namespace) -> None: repo_path = resolve_repo_argument(args.repo) repo_root = git_repo_root(repo_path) @@ -1648,6 +1692,8 @@ def run_tokens_write(config: WorkspaceConfig, args: argparse.Namespace) -> None: server_endpoint = token_server_endpoint(token_record) token_id = str(token_record.get("token_id", "")) token_value = str(token_record.get("value", "")) + if not token_value: + raise SystemExit(f"Token value is empty in tokens.json: {server_endpoint}:{token_id}") final_url = credentialed_remote_url(target_url, token_id, token_value) status = "added" if existing_remote_url is None else "updated" @@ -1971,7 +2017,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|read|stats|write|update", "manage tokens.json and Git remote credentials"], + ["tokens", "scan|add|read|stats|write|update", "manage tokens.json and Git remote credentials"], ], ) print() @@ -2008,6 +2054,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"], + ["add", "TOKEN_ID", "tokens.json", "add an empty token skeleton for manual editing"], ["read", "[--server ENDPOINT]", "tokens.json", "show servers, token ids and remotes"], ["stats", "[--repo PATH]", "repo + tokens.json", "compare remote URLs with local token store"], ["write", "--remote R [--replace]", "tokens.json -> repo", "write selected token into a remote URL"], @@ -2126,6 +2173,19 @@ def build_parser() -> argparse.ArgumentParser: tokens_read_parser.add_argument("--server", help="Filter output to one server endpoint.") tokens_read_parser.add_argument("--show-secrets", action="store_true", help="Print full token values.") + tokens_add_parser = tokens_subparsers.add_parser( + "add", + help="Add an empty token skeleton to tokens.json.", + ) + tokens_add_parser.add_argument("token_id", help="Token id, for example 't1' or 'r6'.") + tokens_add_parser.add_argument("--server", help="Server endpoint. Default: git.base_url from workspace.json.") + tokens_add_parser.add_argument("--value", default="", help="Optional token value. Default: empty for manual editing.") + tokens_add_parser.add_argument("--user", help="Optional API user label.") + tokens_add_parser.add_argument("--remote", help="Optional remote name to attach to this token.") + tokens_add_parser.add_argument("--org", help="Optional remote organization.") + tokens_add_parser.add_argument("--repo", dest="repo_name", help="Optional remote repository.") + tokens_add_parser.add_argument("--dry-run", action="store_true", help="Print the planned token without writing it.") + tokens_stats_parser = tokens_subparsers.add_parser( "stats", help="Print per-server token statistics from tokens.json.", @@ -2197,6 +2257,9 @@ def main() -> int: if args.tokens_command == "scan": run_tokens_scan(config, args) return 0 + if args.tokens_command == "add": + run_tokens_add(config, args) + return 0 if args.tokens_command == "read": run_tokens_read(config, args) return 0