Restructure token store by remote id
This commit is contained in:
@@ -86,7 +86,8 @@ Masz dwie drogi.
|
|||||||
|
|
||||||
To jest wariant dydaktyczny, jesli uczen ma cwiczyc reczne dodawanie remota z
|
To jest wariant dydaktyczny, jesli uczen ma cwiczyc reczne dodawanie remota z
|
||||||
tokenem do konkretnego zdalnego endpointu. Jesli launcher zobaczy URL w formacie
|
tokenem do konkretnego zdalnego endpointu. Jesli launcher zobaczy URL w formacie
|
||||||
`http://LOGIN:TOKEN@...`, zapisze ten token lokalnie do `tokens/tokens.json`.
|
`http://LOGIN:TOKEN@...`, mozesz zapisac ten token lokalnie komenda
|
||||||
|
`tokens sync remote r1`.
|
||||||
|
|
||||||
Przyklad:
|
Przyklad:
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ Minimalny format:
|
|||||||
"version": 3,
|
"version": 3,
|
||||||
"tokens": [
|
"tokens": [
|
||||||
{
|
{
|
||||||
"token_id": "t1",
|
"id": "r1",
|
||||||
"value": "TU_WSTAW_TOKEN",
|
"value": "TU_WSTAW_TOKEN",
|
||||||
"server": {
|
"server": {
|
||||||
"type": "gitea",
|
"type": "gitea",
|
||||||
@@ -124,15 +125,11 @@ Minimalny format:
|
|||||||
"host": "77.90.8.171",
|
"host": "77.90.8.171",
|
||||||
"port": 3001
|
"port": 3001
|
||||||
},
|
},
|
||||||
"remotes": [
|
"user": "u1",
|
||||||
{
|
|
||||||
"name": "r1",
|
|
||||||
"org": "edu-tools",
|
"org": "edu-tools",
|
||||||
"repo": "rv-launcher"
|
"repo": "rv-launcher"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -159,7 +156,10 @@ Podstawowe komendy tokenow:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens scan
|
./rvctl tokens scan
|
||||||
./rvctl tokens add t1
|
./rvctl tokens sync remote r1
|
||||||
|
./rvctl tokens update r1
|
||||||
|
./rvctl tokens sync store r1
|
||||||
|
./rvctl tokens add r1
|
||||||
./rvctl tokens read
|
./rvctl tokens read
|
||||||
./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03
|
./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03
|
||||||
```
|
```
|
||||||
@@ -167,12 +167,12 @@ Podstawowe komendy tokenow:
|
|||||||
`tokens scan` wypisuje tabele `tokens` i niczego nie zapisuje: jeden wiersz na
|
`tokens scan` wypisuje tabele `tokens` i niczego nie zapisuje: jeden wiersz na
|
||||||
logiczny remote tokena.
|
logiczny remote tokena.
|
||||||
`token_ref` laczy nazwe tokena z markerem po prawej stronie: `*` oznacza, ze
|
`token_ref` laczy nazwe tokena z markerem po prawej stronie: `*` oznacza, ze
|
||||||
remote i `tokens.json` sa zgodne oraz uprawnienia zostaly wczytane, `R` oznacza
|
remote i `tokens.json` sa zgodne, `R` oznacza token tylko w remote, a `S`
|
||||||
token tylko w remote, a `S` token tylko w `tokens.json`. Kolumny `scope`, `org`
|
token tylko w `tokens.json`. Kolumny `scope`, `org` i `repo` sa maskami
|
||||||
i `repo` sa maskami uprawnien; bez `*` maja wartosc `?????????`, `?????`
|
uprawnien; bez zgodnego wpisu maja wartosc `?????????`, `?????` albo `????`.
|
||||||
albo `????`.
|
Zapis z remote do `tokens.json` robi `tokens sync remote r1`.
|
||||||
Zapis z remote do `tokens.json` oraz odczyt `valid` i uprawnien z API robi
|
Odczyt `valid` i uprawnien z API robi `tokens update r1`.
|
||||||
dopiero `tokens update --from remotes`.
|
Zapis z `tokens.json` do remota robi `tokens sync store r1`.
|
||||||
Kolumna `valid` oznacza, czy token zostal zaakceptowany przez API teraz.
|
Kolumna `valid` oznacza, czy token zostal zaakceptowany przez API teraz.
|
||||||
Jesli przy tokenie w `tokens.json` zapiszesz `expires_at`, `valid` moze pokazac
|
Jesli przy tokenie w `tokens.json` zapiszesz `expires_at`, `valid` moze pokazac
|
||||||
date wygasniecia; bez daty poprawny token pokazuje `forever`.
|
date wygasniecia; bez daty poprawny token pokazuje `forever`.
|
||||||
|
|||||||
+95
-33
@@ -103,22 +103,23 @@ Minimalny format pliku:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 2,
|
"version": 3,
|
||||||
"servers": {
|
"tokens": [
|
||||||
"http://77.90.8.171:3001": {
|
{
|
||||||
|
"id": "r1",
|
||||||
|
"value": "TU_WSTAW_TOKEN",
|
||||||
|
"server": {
|
||||||
"type": "gitea",
|
"type": "gitea",
|
||||||
|
"endpoint": "http://77.90.8.171:3001",
|
||||||
"scheme": "http",
|
"scheme": "http",
|
||||||
"host": "77.90.8.171",
|
"host": "77.90.8.171",
|
||||||
"port": 3001,
|
"port": 3001
|
||||||
"users": {
|
},
|
||||||
"u1": {
|
"user": "u1",
|
||||||
"tokens": {
|
"org": "edu-tools",
|
||||||
"t1": "TU_WSTAW_TOKEN"
|
"repo": "rv-launcher"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -306,13 +307,13 @@ Typowy wynik:
|
|||||||
tokens
|
tokens
|
||||||
item server proto host org repo user remote token_ref token valid scope org repo
|
item server proto host org repo user remote token_ref token valid scope org repo
|
||||||
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
||||||
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 t1 * e59cc...13be forever -----w--- +++++ ++++
|
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 * e59cc...13be forever -----w--- +++++ ++++
|
||||||
```
|
```
|
||||||
|
|
||||||
`token_ref` jest komorka stalej szerokosci: nazwa tokena jest po lewej, a marker
|
`token_ref` jest komorka stalej szerokosci: nazwa tokena jest po lewej, a marker
|
||||||
po prawej. Marker `*` oznacza, ze token w remote i `tokens.json` jest zgodny
|
po prawej. Nazwa tokena jest taka sama jak nazwa git remote, np. `r1`. Marker
|
||||||
oraz uprawnienia zostaly wczytane. Marker `R` oznacza token tylko w remote, a
|
`*` oznacza, ze remote i `tokens.json` sa zgodne. Marker `R` oznacza token tylko
|
||||||
`S` token tylko w `tokens.json`.
|
w remote, a `S` token tylko w `tokens.json`.
|
||||||
|
|
||||||
Maski uprawnien:
|
Maski uprawnien:
|
||||||
|
|
||||||
@@ -330,10 +331,11 @@ Przyklad:
|
|||||||
./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03
|
./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03
|
||||||
```
|
```
|
||||||
|
|
||||||
## `tokens add TOKEN_ID`
|
## `tokens add REMOTE_ID`
|
||||||
|
|
||||||
Dodaje pusty szkielet tokena do `tokens.json`. Pole `value` jest puste i trzeba
|
Dodaje pusty szkielet tokena do `tokens.json`. `REMOTE_ID` musi byc taki sam
|
||||||
je uzupelnic recznie przed uzyciem tokena.
|
jak nazwa git remote, np. `r1`. Pole `value` jest puste i trzeba je uzupelnic
|
||||||
|
recznie przed uzyciem tokena.
|
||||||
|
|
||||||
Przelaczniki:
|
Przelaczniki:
|
||||||
|
|
||||||
@@ -342,9 +344,9 @@ Przelaczniki:
|
|||||||
- `--value TOKEN`
|
- `--value TOKEN`
|
||||||
Opcjonalna wartosc tokena. Domyslnie pusta.
|
Opcjonalna wartosc tokena. Domyslnie pusta.
|
||||||
- `--user NAME`
|
- `--user NAME`
|
||||||
Opcjonalny opis usera API.
|
Login uzywany w URL-u auth, np. `u1`.
|
||||||
- `--remote NAME`
|
- `--remote NAME`
|
||||||
Opcjonalny remote powiazany z tokenem.
|
Alias zgodnosci. Jesli podany, musi byc taki sam jak `REMOTE_ID`.
|
||||||
- `--org NAME`
|
- `--org NAME`
|
||||||
Opcjonalna organizacja dla remota.
|
Opcjonalna organizacja dla remota.
|
||||||
- `--repo NAME`
|
- `--repo NAME`
|
||||||
@@ -355,8 +357,51 @@ Przelaczniki:
|
|||||||
Przyklady:
|
Przyklady:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens add r6
|
./rvctl tokens add r1
|
||||||
./rvctl tokens add t1 --remote r1 --org edu-tools --repo rv-launcher
|
./rvctl tokens add r1 --user u1 --org edu-tools --repo rv-launcher
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tokens sync remote REMOTE_ID`
|
||||||
|
|
||||||
|
Czyta dane auth z git remote `REMOTE_ID` i zapisuje je do `tokens.json`. Nie
|
||||||
|
pobiera metadanych z API.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--repo PATH`
|
||||||
|
Sciezka wewnatrz docelowego repo. Domyslnie biezacy katalog.
|
||||||
|
- `--dry-run`
|
||||||
|
Pokazuje plan bez zapisu.
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl tokens sync remote r1
|
||||||
|
./rvctl tokens sync remote r1 --repo ~/dev/workspace/rv/tools/rv-launcher
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tokens sync store REMOTE_ID`
|
||||||
|
|
||||||
|
Zapisuje dane auth z rekordu `REMOTE_ID` w `tokens.json` do git remote o tej
|
||||||
|
samej nazwie.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--repo PATH`
|
||||||
|
Sciezka wewnatrz docelowego repo. Domyslnie biezacy katalog.
|
||||||
|
- `--url URL`
|
||||||
|
URL remota, jesli remote jeszcze nie istnieje.
|
||||||
|
- `--server ENDPOINT`
|
||||||
|
Endpoint serwera z `tokens.json`.
|
||||||
|
- `--replace`
|
||||||
|
Nadpisuje inne dane auth juz wpisane w remote URL.
|
||||||
|
- `--dry-run`
|
||||||
|
Pokazuje plan bez zapisu.
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl tokens sync store r1 --repo ~/dev/workspace/rv/series/inf/03
|
||||||
```
|
```
|
||||||
|
|
||||||
## `tokens read`
|
## `tokens read`
|
||||||
@@ -379,10 +424,9 @@ type<TAB>gitea
|
|||||||
scheme<TAB>http
|
scheme<TAB>http
|
||||||
host<TAB>77.90.8.171
|
host<TAB>77.90.8.171
|
||||||
port<TAB>3001
|
port<TAB>3001
|
||||||
users<TAB>1
|
|
||||||
tokens<TAB>1
|
tokens<TAB>1
|
||||||
user<TAB>u1
|
id<TAB>r1<TAB>SE****23
|
||||||
token<TAB>t1<TAB>SE****23
|
user<TAB>r1<TAB>u1
|
||||||
```
|
```
|
||||||
|
|
||||||
Przyklad:
|
Przyklad:
|
||||||
@@ -415,7 +459,7 @@ token_path<TAB>...
|
|||||||
tokens
|
tokens
|
||||||
item server proto host org repo user remote token_ref token valid scope org repo
|
item server proto host org repo user remote token_ref token valid scope org repo
|
||||||
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
---- ------ ----- ------------------ --------- ----------- ---- ------ --------- ------------ ------------------- aAimnopru oawrc- oawr--
|
||||||
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 t1 * e59cc...13be forever -----w--- +++++ ++++
|
1 gitea http 77.90.8.171:3001 edu-tools rv-launcher u1 r1 r1 * e59cc...13be forever -----w--- +++++ ++++
|
||||||
|
|
||||||
status
|
status
|
||||||
item<TAB>value
|
item<TAB>value
|
||||||
@@ -445,7 +489,7 @@ Przelaczniki:
|
|||||||
- `--user NAME`
|
- `--user NAME`
|
||||||
Uzytkownik z wybranego endpointu.
|
Uzytkownik z wybranego endpointu.
|
||||||
- `--token-name NAME`
|
- `--token-name NAME`
|
||||||
Nazwa tokena, na przyklad `t1`.
|
Remote id w `tokens.json`, na przyklad `r1`. Domyslnie wartosc `--remote`.
|
||||||
- `--replace`
|
- `--replace`
|
||||||
Nadpisuje inne dane auth juz wpisane w remote URL.
|
Nadpisuje inne dane auth juz wpisane w remote URL.
|
||||||
- `--dry-run`
|
- `--dry-run`
|
||||||
@@ -454,12 +498,30 @@ Przelaczniki:
|
|||||||
Przyklad:
|
Przyklad:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens write --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001 --user u1 --token-name t1
|
./rvctl tokens write --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001
|
||||||
```
|
```
|
||||||
|
|
||||||
## `tokens update`
|
## `tokens update REMOTE_ID`
|
||||||
|
|
||||||
Uruchamia synchronizacje w zadanym kierunku.
|
Pobiera z API metadane dla rekordu `REMOTE_ID` zapisanego w `tokens.json`.
|
||||||
|
Nie synchronizuje sekretu z git remote.
|
||||||
|
|
||||||
|
Przelaczniki:
|
||||||
|
|
||||||
|
- `--server ENDPOINT`
|
||||||
|
Opcjonalny wybor endpointu, jesli ten sam `REMOTE_ID` istnieje dla wielu serwerow.
|
||||||
|
- `--dry-run`
|
||||||
|
Pokazuje plan bez zapisu.
|
||||||
|
|
||||||
|
Przyklad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl tokens update r1
|
||||||
|
```
|
||||||
|
|
||||||
|
## `tokens update --from ...`
|
||||||
|
|
||||||
|
Komendy zgodnosci dla starego modelu kierunkowego.
|
||||||
|
|
||||||
Przelaczniki:
|
Przelaczniki:
|
||||||
|
|
||||||
@@ -478,7 +540,7 @@ Przelaczniki:
|
|||||||
- `--user NAME`
|
- `--user NAME`
|
||||||
Opcjonalny wybor usera dla `--from store`.
|
Opcjonalny wybor usera dla `--from store`.
|
||||||
- `--token-name NAME`
|
- `--token-name NAME`
|
||||||
Opcjonalny wybor tokena dla `--from store`.
|
Opcjonalny wybor remote id dla `--from store`.
|
||||||
- `--replace`
|
- `--replace`
|
||||||
Nadpisuje inne auth przy `--from store`.
|
Nadpisuje inne auth przy `--from store`.
|
||||||
- `--dry-run`
|
- `--dry-run`
|
||||||
@@ -488,7 +550,7 @@ Przyklady:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens update --from remotes --repo ~/dev/workspace/rv/series/inf/03
|
./rvctl tokens update --from remotes --repo ~/dev/workspace/rv/series/inf/03
|
||||||
./rvctl tokens update --from store --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001 --user u1 --token-name t1
|
./rvctl tokens update --from store --repo ~/dev/workspace/rv/series/inf/03 --remote r1 --server http://77.90.8.171:3001
|
||||||
```
|
```
|
||||||
|
|
||||||
## `submission [series] [card]`
|
## `submission [series] [card]`
|
||||||
|
|||||||
+104
-62
@@ -4,28 +4,31 @@ Plik opisuje model pracy z tokenami w launcherze.
|
|||||||
|
|
||||||
## Zrodlo prawdy
|
## Zrodlo prawdy
|
||||||
|
|
||||||
Sa dwa miejsca, w ktorych moga byc zapisane tokeny:
|
Sa dwa miejsca, w ktorych moga byc zapisane dane dostepowe:
|
||||||
|
|
||||||
- remote URL-e w repo, na przyklad `http://t1:SECRET@host/org/repo.git`
|
- remote URL w repo, na przyklad `http://u1:SECRET@host/org/repo.git`
|
||||||
- lokalny plik `~/dev/workspace/rv/tokens/tokens.json`
|
- lokalny plik `~/dev/workspace/rv/tokens/tokens.json`
|
||||||
|
|
||||||
`tokens scan` tylko porownuje oba zrodla i niczego nie zapisuje.
|
`tokens scan` tylko porownuje oba miejsca i niczego nie zapisuje.
|
||||||
`tokens update --from remotes` czyta remote URL-e, zapisuje tokeny do
|
`tokens sync remote r1` czyta remote `r1` i zapisuje jego dane w
|
||||||
`tokens.json` i wzbogaca je danymi z API, jezeli token dziala.
|
`tokens.json`.
|
||||||
`tokens update --from store` zapisuje wybrany token z `tokens.json` do remote
|
`tokens sync store r1` zapisuje rekord `r1` z `tokens.json` do git remote
|
||||||
URL-a repo.
|
`r1`.
|
||||||
|
`tokens update r1` nie synchronizuje sekretu; tylko pobiera z API metadane
|
||||||
|
tokenu: `valid`, `scope`, `org_perm` i `repo_perm`.
|
||||||
|
|
||||||
## Format `tokens.json`
|
## Format `tokens.json`
|
||||||
|
|
||||||
Aktualny format to `version: 3`. Glownym rekordem jest token. `server`, `user`,
|
Aktualny format to `version: 3`. Glownym rekordem jest remote-token. Pole
|
||||||
`valid`, `scope` i `remotes` sa atrybutami tego tokena.
|
`id` jest obowiazkowe i musi byc takie samo jak nazwa git remote, np. `r1`
|
||||||
|
albo `r1a`.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"tokens": [
|
"tokens": [
|
||||||
{
|
{
|
||||||
"token_id": "t1",
|
"id": "r1",
|
||||||
"value": "SECRET",
|
"value": "SECRET",
|
||||||
"server": {
|
"server": {
|
||||||
"type": "gitea",
|
"type": "gitea",
|
||||||
@@ -35,17 +38,33 @@ Aktualny format to `version: 3`. Glownym rekordem jest token. `server`, `user`,
|
|||||||
"port": 3001
|
"port": 3001
|
||||||
},
|
},
|
||||||
"user": "u1",
|
"user": "u1",
|
||||||
"valid": "forever",
|
|
||||||
"scope": "-----w---",
|
|
||||||
"remotes": [
|
|
||||||
{
|
|
||||||
"name": "r1",
|
|
||||||
"org": "edu-tools",
|
"org": "edu-tools",
|
||||||
"repo": "rv-launcher",
|
"repo": "rv-launcher",
|
||||||
"org_perm": "+++++",
|
"valid": "forever",
|
||||||
"repo_perm": "++++"
|
"scope": {
|
||||||
|
"a": "-",
|
||||||
|
"A": "-",
|
||||||
|
"i": "-",
|
||||||
|
"m": "-",
|
||||||
|
"n": "-",
|
||||||
|
"o": "-",
|
||||||
|
"p": "-",
|
||||||
|
"r": "w",
|
||||||
|
"u": "-"
|
||||||
|
},
|
||||||
|
"org_perm": {
|
||||||
|
"o": "+",
|
||||||
|
"a": "+",
|
||||||
|
"w": "+",
|
||||||
|
"r": "+",
|
||||||
|
"c": "+"
|
||||||
|
},
|
||||||
|
"repo_perm": {
|
||||||
|
"o": "+",
|
||||||
|
"a": "+",
|
||||||
|
"w": "+",
|
||||||
|
"r": "+"
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -57,28 +76,27 @@ Schemat JSON jest w pliku `doc/tokens.schema.json`.
|
|||||||
|
|
||||||
Pola synchronizowane z remote URL-a:
|
Pola synchronizowane z remote URL-a:
|
||||||
|
|
||||||
|
- `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.endpoint` - endpoint serwera, na przyklad `http://77.90.8.171:3001`
|
||||||
- `server.type` - typ serwera, na przyklad `gitea`, `github`, `gitlab`, `unknown`
|
- `server.type` - typ serwera, na przyklad `gitea`, `github`, `gitlab`, `unknown`
|
||||||
- `token_id` - identyfikator z lewej strony URL-a, na przyklad `t1` w `http://t1:SECRET@...`
|
- `user` - login z URL-a, czyli lewa strona `http://u1:SECRET@...`
|
||||||
- `value` - sekret tokena
|
- `value` - sekret tokena
|
||||||
- `remotes[].name` - nazwa remota, na przyklad `r1`
|
- `org` - organizacja z URL-a
|
||||||
- `remotes[].org` - organizacja z URL-a
|
- `repo` - repo z URL-a
|
||||||
- `remotes[].repo` - repo z URL-a
|
|
||||||
|
|
||||||
Pola wzbogacane przez API przy `tokens update --from remotes`:
|
Pola wzbogacane przez API przy `tokens update r1`:
|
||||||
|
|
||||||
- `user` - login wlasciciela tokena odczytany z API
|
|
||||||
- `valid` - `forever`, data `expires_at`, `invalid`, `?` albo `!`
|
- `valid` - `forever`, data `expires_at`, `invalid`, `?` albo `!`
|
||||||
- `scope` - maska scope tokena w kolejnosci `aAimnopru`
|
- `scope` - mapa scope tokena
|
||||||
- `remotes[].org_perm` - maska praw w organizacji
|
- `org_perm` - mapa praw w organizacji
|
||||||
- `remotes[].repo_perm` - maska praw w repo
|
- `repo_perm` - mapa praw w repo
|
||||||
|
|
||||||
## Porownanie
|
## Porownanie
|
||||||
|
|
||||||
Porownanie z `git remote -v` jest robione po:
|
Porownanie z `git remote -v` jest robione po:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
server.endpoint + remote.name + token_id + value + org + repo
|
server.endpoint + id + user + value + org + repo
|
||||||
```
|
```
|
||||||
|
|
||||||
Znacznik w kolumnie `token_ref`:
|
Znacznik w kolumnie `token_ref`:
|
||||||
@@ -88,10 +106,13 @@ Znacznik w kolumnie `token_ref`:
|
|||||||
- `S` - wpis istnieje tylko w `tokens.json`
|
- `S` - wpis istnieje tylko w `tokens.json`
|
||||||
- `!` - remote i `tokens.json` sa zgodne, ale zapisany token jest `invalid` albo ma blad walidacji
|
- `!` - remote i `tokens.json` sa zgodne, ale zapisany token jest `invalid` albo ma blad walidacji
|
||||||
|
|
||||||
Bez `*` albo `!` kolumny `valid`, `scope`, `org` i `repo` w raporcie maja
|
Bez zgodnego wpisu kolumny `valid`, `scope`, `org` i `repo` w raporcie maja
|
||||||
wartosc `?`, bo launcher nie pokazuje metadanych API dla niesparowanych wpisow.
|
wartosc `?`, bo launcher nie pokazuje metadanych API dla niesparowanych wpisow.
|
||||||
|
|
||||||
## Maski uprawnien
|
## Uprawnienia
|
||||||
|
|
||||||
|
W `tokens.json` uprawnienia sa zapisane jako mapy klucz-wartosc. W tabeli CLI sa
|
||||||
|
pokazywane skrocone maski.
|
||||||
|
|
||||||
Naglowki masek:
|
Naglowki masek:
|
||||||
|
|
||||||
@@ -119,7 +140,22 @@ Znaki w `scope`:
|
|||||||
- `?` - nie wczytano
|
- `?` - nie wczytano
|
||||||
- `!` - blad wczytania
|
- `!` - blad wczytania
|
||||||
|
|
||||||
Znaki w `org` i `repo`:
|
Kategorie `org_perm`:
|
||||||
|
|
||||||
|
- `o` - owner
|
||||||
|
- `a` - admin
|
||||||
|
- `w` - write
|
||||||
|
- `r` - read
|
||||||
|
- `c` - create repository
|
||||||
|
|
||||||
|
Kategorie `repo_perm`:
|
||||||
|
|
||||||
|
- `o` - owner
|
||||||
|
- `a` - admin
|
||||||
|
- `w` - write
|
||||||
|
- `r` - read
|
||||||
|
|
||||||
|
Znaki w `org_perm` i `repo_perm`:
|
||||||
|
|
||||||
- `+` - flaga wlaczona
|
- `+` - flaga wlaczona
|
||||||
- `-` - flaga wylaczona
|
- `-` - flaga wylaczona
|
||||||
@@ -130,26 +166,43 @@ Znaki w `org` i `repo`:
|
|||||||
|
|
||||||
### `tokens scan`
|
### `tokens scan`
|
||||||
|
|
||||||
Read-only. Czyta remote URL-e i `tokens.json`, a potem wypisuje tabele `tokens`.
|
Read-only. Czyta remote URL-e i `tokens.json`, a potem wypisuje tabele
|
||||||
Nie tworzy i nie modyfikuje `tokens.json`.
|
`tokens`. Nie tworzy i nie modyfikuje `tokens.json`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens scan
|
./rvctl tokens scan
|
||||||
./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03
|
./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03
|
||||||
```
|
```
|
||||||
|
|
||||||
### `tokens update --from remotes`
|
### `tokens sync remote REMOTE_ID`
|
||||||
|
|
||||||
Kopiuje tokeny z remote URL-i repo do `tokens.json`, zapisuje rekordy w formacie
|
Kopiuje dane dostepowe z git remote do `tokens.json`. Nie pobiera metadanych z
|
||||||
v3 i probuje pobrac pola API: `user`, `valid`, `scope`, `org_perm`,
|
API.
|
||||||
`repo_perm`.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens update --from remotes --repo ~/dev/workspace/rv/tools/rv-launcher
|
./rvctl tokens sync remote r1
|
||||||
./rvctl tokens update --from remotes --dry-run
|
./rvctl tokens sync remote r1 --repo ~/dev/workspace/rv/tools/rv-launcher
|
||||||
```
|
```
|
||||||
|
|
||||||
`--dry-run` dziala jak read-only raport i niczego nie zapisuje.
|
### `tokens update REMOTE_ID`
|
||||||
|
|
||||||
|
Pobiera z API metadane dla rekordu `REMOTE_ID` juz zapisanego w `tokens.json`.
|
||||||
|
Nie zmienia git remote URL-a i nie kopiuje sekretu z remote.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl tokens update r1
|
||||||
|
./rvctl tokens update r1 --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
### `tokens sync store REMOTE_ID`
|
||||||
|
|
||||||
|
Zapisuje dane z rekordu `REMOTE_ID` w `tokens.json` do git remote o tej samej
|
||||||
|
nazwie.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./rvctl tokens sync store r1 --repo ~/dev/workspace/rv/series/inf/03
|
||||||
|
./rvctl tokens sync store r1 --repo PATH --replace
|
||||||
|
```
|
||||||
|
|
||||||
### `tokens read`
|
### `tokens read`
|
||||||
|
|
||||||
@@ -163,16 +216,16 @@ Pokazuje zawartosc `tokens.json`.
|
|||||||
|
|
||||||
### `tokens add`
|
### `tokens add`
|
||||||
|
|
||||||
Dodaje pusty szkielet tokena do `tokens.json`. Pole `value` zostaje puste, zeby
|
Dodaje pusty szkielet rekordu do `tokens.json`. `REMOTE_ID` musi odpowiadac
|
||||||
mozna bylo recznie wkleic sekret.
|
nazwie git remote.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens add t1
|
./rvctl tokens add r1
|
||||||
./rvctl tokens add r6 --server http://77.90.8.171:3001
|
./rvctl tokens add r1 --server http://77.90.8.171:3001 --user u1 --org edu-tools --repo rv-launcher
|
||||||
./rvctl tokens add t1 --remote r1 --org edu-tools --repo rv-launcher
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`tokens write` nie uzyje pustego tokena. Najpierw trzeba uzupelnic `value`.
|
`tokens sync store r1` nie uzyje pustego tokena. Najpierw trzeba uzupelnic
|
||||||
|
`value` i `user`.
|
||||||
|
|
||||||
### `tokens stats`
|
### `tokens stats`
|
||||||
|
|
||||||
@@ -182,23 +235,12 @@ Pokazuje kontekst, tabele `tokens` i podsumowanie statusow endpointow.
|
|||||||
./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03
|
./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03
|
||||||
```
|
```
|
||||||
|
|
||||||
### `tokens write`
|
### Komendy zgodnosci
|
||||||
|
|
||||||
Zapisuje wybrany token z `tokens.json` do remote URL-a repo.
|
Stare komendy nadal dzialaja, ale sa mniej jednoznaczne:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./rvctl tokens write \
|
./rvctl tokens update --from remotes --repo PATH
|
||||||
--repo ~/dev/workspace/rv/series/inf/03 \
|
./rvctl tokens update --from store --repo PATH --remote r1 --server http://77.90.8.171:3001
|
||||||
--remote r1 \
|
./rvctl tokens write --repo PATH --remote r1 --server http://77.90.8.171:3001
|
||||||
--server http://77.90.8.171:3001 \
|
|
||||||
--token-name t1
|
|
||||||
```
|
|
||||||
|
|
||||||
### `tokens update --from store`
|
|
||||||
|
|
||||||
Alias kierunkowy na zapis store -> remote. Uzywa tych samych opcji co
|
|
||||||
`tokens write`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./rvctl tokens update --from store --repo PATH --remote r1 --server http://77.90.8.171:3001 --token-name t1
|
|
||||||
```
|
```
|
||||||
|
|||||||
+123
-41
@@ -42,14 +42,125 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"remote": {
|
"scope_permissions": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["name", "org", "repo"],
|
"required": ["a", "A", "i", "m", "n", "o", "p", "r", "u"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"a": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "activitypub"
|
||||||
|
},
|
||||||
|
"A": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "admin"
|
||||||
|
},
|
||||||
|
"i": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "issue"
|
||||||
|
},
|
||||||
|
"m": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "misc"
|
||||||
|
},
|
||||||
|
"n": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "notification"
|
||||||
|
},
|
||||||
|
"o": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "organization"
|
||||||
|
},
|
||||||
|
"p": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "package"
|
||||||
|
},
|
||||||
|
"r": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "repository"
|
||||||
|
},
|
||||||
|
"u": {
|
||||||
|
"$ref": "#/$defs/scope_value",
|
||||||
|
"description": "user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org_permissions": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["o", "a", "w", "r", "c"],
|
||||||
|
"properties": {
|
||||||
|
"o": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "owner"
|
||||||
|
},
|
||||||
|
"a": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "admin"
|
||||||
|
},
|
||||||
|
"w": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "write"
|
||||||
|
},
|
||||||
|
"r": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "read"
|
||||||
|
},
|
||||||
|
"c": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "create repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"repo_permissions": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["o", "a", "w", "r"],
|
||||||
|
"properties": {
|
||||||
|
"o": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "owner"
|
||||||
|
},
|
||||||
|
"a": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "admin"
|
||||||
|
},
|
||||||
|
"w": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "write"
|
||||||
|
},
|
||||||
|
"r": {
|
||||||
|
"$ref": "#/$defs/flag_value",
|
||||||
|
"description": "read"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scope_value": {
|
||||||
|
"enum": ["w", "r", "-", "?", "!"]
|
||||||
|
},
|
||||||
|
"flag_value": {
|
||||||
|
"enum": ["+", "-", "?", "!"]
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["id", "value", "server", "org", "repo"],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1,
|
||||||
|
"description": "Git remote name, for example r1 or r1a."
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Token secret. May be empty only in a manual skeleton."
|
||||||
|
},
|
||||||
|
"server": {
|
||||||
|
"$ref": "#/$defs/server"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "User/login used in the authenticated remote URL."
|
||||||
},
|
},
|
||||||
"org": {
|
"org": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -57,49 +168,20 @@
|
|||||||
"repo": {
|
"repo": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"org_perm": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[+?!-]{5}$"
|
|
||||||
},
|
|
||||||
"repo_perm": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[+?!-]{4}$"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": ["token_id", "value", "server", "remotes"],
|
|
||||||
"properties": {
|
|
||||||
"token_id": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"server": {
|
|
||||||
"$ref": "#/$defs/server"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"valid": {
|
"valid": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"scope": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[rw?!-]{9}$"
|
|
||||||
},
|
|
||||||
"expires_at": {
|
"expires_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"remotes": {
|
"scope": {
|
||||||
"type": "array",
|
"$ref": "#/$defs/scope_permissions"
|
||||||
"items": {
|
},
|
||||||
"$ref": "#/$defs/remote"
|
"org_perm": {
|
||||||
}
|
"$ref": "#/$defs/org_permissions"
|
||||||
|
},
|
||||||
|
"repo_perm": {
|
||||||
|
"$ref": "#/$defs/repo_permissions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,34 @@ class WorkspaceConfig:
|
|||||||
git: GitConfig
|
git: GitConfig
|
||||||
|
|
||||||
|
|
||||||
|
SCOPE_FIELDS = [
|
||||||
|
("a", "activitypub"),
|
||||||
|
("A", "admin"),
|
||||||
|
("i", "issue"),
|
||||||
|
("m", "misc"),
|
||||||
|
("n", "notification"),
|
||||||
|
("o", "organization"),
|
||||||
|
("p", "package"),
|
||||||
|
("r", "repository"),
|
||||||
|
("u", "user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ORG_PERMISSION_FIELDS = [
|
||||||
|
("o", "owner"),
|
||||||
|
("a", "admin"),
|
||||||
|
("w", "write"),
|
||||||
|
("r", "read"),
|
||||||
|
("c", "create"),
|
||||||
|
]
|
||||||
|
|
||||||
|
REPO_PERMISSION_FIELDS = [
|
||||||
|
("o", "owner"),
|
||||||
|
("a", "admin"),
|
||||||
|
("w", "write"),
|
||||||
|
("r", "read"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path: Path) -> WorkspaceConfig:
|
def load_config(config_path: Path) -> WorkspaceConfig:
|
||||||
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
base_dir = config_path.parent
|
base_dir = config_path.parent
|
||||||
@@ -381,10 +409,17 @@ def normalize_remote_record(raw_remote: dict) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
remote_record = {"name": remote_name, "org": "", "repo": ""}
|
remote_record = {"name": remote_name, "org": "", "repo": ""}
|
||||||
for field_name in ["org", "repo", "org_perm", "repo_perm"]:
|
for field_name in ["org", "repo"]:
|
||||||
field_value = raw_remote.get(field_name)
|
field_value = raw_remote.get(field_name)
|
||||||
if isinstance(field_value, str) and field_value:
|
if isinstance(field_value, str) and field_value:
|
||||||
remote_record[field_name] = field_value
|
remote_record[field_name] = field_value
|
||||||
|
|
||||||
|
org_perm = normalize_permission_map(raw_remote.get("org_perm"), ORG_PERMISSION_FIELDS, "+?!-")
|
||||||
|
if org_perm is not None:
|
||||||
|
remote_record["org_perm"] = org_perm
|
||||||
|
repo_perm = normalize_permission_map(raw_remote.get("repo_perm"), REPO_PERMISSION_FIELDS, "+?!-")
|
||||||
|
if repo_perm is not None:
|
||||||
|
remote_record["repo_perm"] = repo_perm
|
||||||
return remote_record
|
return remote_record
|
||||||
|
|
||||||
|
|
||||||
@@ -392,36 +427,128 @@ def valid_mask(raw_value: str, allowed: str, width: int) -> bool:
|
|||||||
return len(raw_value) == width and all(char in allowed for char in raw_value)
|
return len(raw_value) == width and all(char in allowed for char in raw_value)
|
||||||
|
|
||||||
|
|
||||||
def normalize_v3_token(config: WorkspaceConfig, raw_token: dict) -> dict | None:
|
def permission_map_from_mask(
|
||||||
if not isinstance(raw_token, dict):
|
raw_value: str,
|
||||||
|
fields: list[tuple[str, str]],
|
||||||
|
allowed: str,
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
if not valid_mask(raw_value, allowed, len(fields)):
|
||||||
|
return None
|
||||||
|
return {key: value for (key, _), value in zip(fields, raw_value)}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_permission_map(
|
||||||
|
raw_value,
|
||||||
|
fields: list[tuple[str, str]],
|
||||||
|
allowed: str,
|
||||||
|
unknown: str = "?",
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
if isinstance(raw_value, str):
|
||||||
|
return permission_map_from_mask(raw_value, fields, allowed)
|
||||||
|
if not isinstance(raw_value, dict):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
token_id = raw_token.get("token_id") or raw_token.get("id") or raw_token.get("token_ref")
|
normalized: dict[str, str] = {}
|
||||||
token_value = raw_token.get("value")
|
found = False
|
||||||
if token_value is None:
|
for key, long_name in fields:
|
||||||
token_value = raw_token.get("token")
|
field_value = raw_value.get(key, raw_value.get(long_name))
|
||||||
if not isinstance(token_id, str) or not token_id:
|
if isinstance(field_value, str) and len(field_value) == 1 and field_value in allowed:
|
||||||
return None
|
normalized[key] = field_value
|
||||||
if not isinstance(token_value, str):
|
found = True
|
||||||
return None
|
else:
|
||||||
|
normalized[key] = unknown
|
||||||
|
return normalized if found else None
|
||||||
|
|
||||||
|
|
||||||
|
def permission_map_to_mask(
|
||||||
|
raw_value,
|
||||||
|
fields: list[tuple[str, str]],
|
||||||
|
allowed: str,
|
||||||
|
unknown: str = "?",
|
||||||
|
) -> str:
|
||||||
|
if isinstance(raw_value, str) and valid_mask(raw_value, allowed, len(fields)):
|
||||||
|
return raw_value
|
||||||
|
if not isinstance(raw_value, dict):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
values = []
|
||||||
|
for key, long_name in fields:
|
||||||
|
field_value = raw_value.get(key, raw_value.get(long_name))
|
||||||
|
if isinstance(field_value, str) and len(field_value) == 1 and field_value in allowed:
|
||||||
|
values.append(field_value)
|
||||||
|
else:
|
||||||
|
values.append(unknown)
|
||||||
|
return "".join(values)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_flat_token(
|
||||||
|
config: WorkspaceConfig,
|
||||||
|
raw_token: dict,
|
||||||
|
remote_id: str,
|
||||||
|
token_value: str,
|
||||||
|
user_name: str,
|
||||||
|
org_name: str,
|
||||||
|
repo_name: str,
|
||||||
|
org_perm=None,
|
||||||
|
repo_perm=None,
|
||||||
|
) -> dict | None:
|
||||||
server_record = normalize_server_record(config, raw_token.get("server", {}))
|
server_record = normalize_server_record(config, raw_token.get("server", {}))
|
||||||
if server_record is None:
|
if server_record is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
token_record = {
|
token_record = {
|
||||||
"token_id": token_id,
|
"id": remote_id,
|
||||||
"value": token_value,
|
"value": token_value,
|
||||||
"server": server_record,
|
"server": server_record,
|
||||||
"remotes": [],
|
"org": org_name,
|
||||||
|
"repo": repo_name,
|
||||||
}
|
}
|
||||||
for field_name in ["user", "valid", "expires_at"]:
|
if user_name:
|
||||||
|
token_record["user"] = user_name
|
||||||
|
for field_name in ["valid", "expires_at"]:
|
||||||
field_value = raw_token.get(field_name)
|
field_value = raw_token.get(field_name)
|
||||||
if isinstance(field_value, str) and field_value:
|
if isinstance(field_value, str) and field_value:
|
||||||
token_record[field_name] = field_value
|
token_record[field_name] = field_value
|
||||||
scope_value = raw_token.get("scope")
|
|
||||||
if isinstance(scope_value, str) and valid_mask(scope_value, "rw?!-", 9):
|
scope_record = normalize_permission_map(raw_token.get("scope"), SCOPE_FIELDS, "rw?!-")
|
||||||
token_record["scope"] = scope_value
|
if scope_record is not None:
|
||||||
|
token_record["scope"] = scope_record
|
||||||
|
|
||||||
|
org_record = normalize_permission_map(org_perm if org_perm is not None else raw_token.get("org_perm"), ORG_PERMISSION_FIELDS, "+?!-")
|
||||||
|
if org_record is not None:
|
||||||
|
token_record["org_perm"] = org_record
|
||||||
|
repo_record = normalize_permission_map(repo_perm if repo_perm is not None else raw_token.get("repo_perm"), REPO_PERMISSION_FIELDS, "+?!-")
|
||||||
|
if repo_record is not None:
|
||||||
|
token_record["repo_perm"] = repo_record
|
||||||
|
return token_record
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_v3_tokens(config: WorkspaceConfig, raw_token: dict) -> list[dict]:
|
||||||
|
if not isinstance(raw_token, dict):
|
||||||
|
return []
|
||||||
|
|
||||||
|
remote_id = raw_token.get("id") or raw_token.get("remote")
|
||||||
|
legacy_token_id = raw_token.get("token_id") or raw_token.get("token_ref")
|
||||||
|
token_value = raw_token.get("value")
|
||||||
|
if token_value is None:
|
||||||
|
token_value = raw_token.get("token")
|
||||||
|
if not isinstance(token_value, str):
|
||||||
|
return []
|
||||||
|
user_name = raw_token.get("user")
|
||||||
|
if not isinstance(user_name, str):
|
||||||
|
user_name = ""
|
||||||
|
|
||||||
|
if isinstance(remote_id, str) and remote_id:
|
||||||
|
token_record = normalize_flat_token(
|
||||||
|
config,
|
||||||
|
raw_token,
|
||||||
|
remote_id,
|
||||||
|
token_value,
|
||||||
|
user_name,
|
||||||
|
str(raw_token.get("org", "")),
|
||||||
|
str(raw_token.get("repo", "")),
|
||||||
|
)
|
||||||
|
return [token_record] if token_record is not None else []
|
||||||
|
|
||||||
raw_remotes = raw_token.get("remotes", [])
|
raw_remotes = raw_token.get("remotes", [])
|
||||||
if isinstance(raw_remotes, dict):
|
if isinstance(raw_remotes, dict):
|
||||||
@@ -430,34 +557,60 @@ def normalize_v3_token(config: WorkspaceConfig, raw_token: dict) -> dict | None:
|
|||||||
for remote_name, remote_value in raw_remotes.items()
|
for remote_name, remote_value in raw_remotes.items()
|
||||||
if isinstance(remote_value, dict)
|
if isinstance(remote_value, dict)
|
||||||
]
|
]
|
||||||
|
if not isinstance(raw_remotes, list):
|
||||||
|
raw_remotes = []
|
||||||
|
|
||||||
|
normalized_records: list[dict] = []
|
||||||
if isinstance(raw_remotes, list):
|
if isinstance(raw_remotes, list):
|
||||||
for raw_remote in raw_remotes:
|
for raw_remote in raw_remotes:
|
||||||
remote_record = normalize_remote_record(raw_remote)
|
remote_record = normalize_remote_record(raw_remote)
|
||||||
if remote_record is not None:
|
if remote_record is None:
|
||||||
token_record["remotes"].append(remote_record)
|
continue
|
||||||
|
legacy_user_name = user_name
|
||||||
|
if not legacy_user_name and isinstance(legacy_token_id, str):
|
||||||
|
legacy_user_name = legacy_token_id
|
||||||
|
token_record = normalize_flat_token(
|
||||||
|
config,
|
||||||
|
raw_token,
|
||||||
|
remote_record["name"],
|
||||||
|
token_value,
|
||||||
|
legacy_user_name,
|
||||||
|
remote_record.get("org", ""),
|
||||||
|
remote_record.get("repo", ""),
|
||||||
|
remote_record.get("org_perm"),
|
||||||
|
remote_record.get("repo_perm"),
|
||||||
|
)
|
||||||
|
if token_record is not None:
|
||||||
|
normalized_records.append(token_record)
|
||||||
|
|
||||||
return token_record
|
if not normalized_records and isinstance(legacy_token_id, str) and legacy_token_id:
|
||||||
|
token_record = normalize_flat_token(
|
||||||
|
config,
|
||||||
|
raw_token,
|
||||||
|
legacy_token_id,
|
||||||
|
token_value,
|
||||||
|
user_name,
|
||||||
|
str(raw_token.get("org", "")),
|
||||||
|
str(raw_token.get("repo", "")),
|
||||||
|
)
|
||||||
|
if token_record is not None:
|
||||||
|
normalized_records.append(token_record)
|
||||||
|
return normalized_records
|
||||||
|
|
||||||
|
|
||||||
def append_token_record(token_data: dict, token_record: dict) -> None:
|
def append_token_record(token_data: dict, token_record: dict) -> None:
|
||||||
tokens = token_data.setdefault("tokens", [])
|
tokens = token_data.setdefault("tokens", [])
|
||||||
token_key = (
|
token_key = (
|
||||||
token_record.get("server", {}).get("endpoint", ""),
|
token_record.get("server", {}).get("endpoint", ""),
|
||||||
token_record.get("token_id", ""),
|
token_record.get("id", ""),
|
||||||
token_record.get("value", ""),
|
|
||||||
)
|
)
|
||||||
for existing_record in tokens:
|
for existing_record in tokens:
|
||||||
existing_key = (
|
existing_key = (
|
||||||
existing_record.get("server", {}).get("endpoint", ""),
|
existing_record.get("server", {}).get("endpoint", ""),
|
||||||
existing_record.get("token_id", ""),
|
existing_record.get("id", ""),
|
||||||
existing_record.get("value", ""),
|
|
||||||
)
|
)
|
||||||
if existing_key == token_key:
|
if existing_key == token_key:
|
||||||
existing_remotes = existing_record.setdefault("remotes", [])
|
existing_record.update(token_record)
|
||||||
existing_remote_names = {remote.get("name", "") for remote in existing_remotes}
|
|
||||||
for remote_record in token_record.get("remotes", []):
|
|
||||||
if remote_record.get("name", "") not in existing_remote_names:
|
|
||||||
existing_remotes.append(remote_record)
|
|
||||||
return
|
return
|
||||||
tokens.append(token_record)
|
tokens.append(token_record)
|
||||||
|
|
||||||
@@ -504,27 +657,20 @@ def copy_legacy_users(token_data: dict, server_info: dict, raw_users: dict) -> d
|
|||||||
if token_entry is None:
|
if token_entry is None:
|
||||||
continue
|
continue
|
||||||
token_value = token_entry_value(token_entry)
|
token_value = token_entry_value(token_entry)
|
||||||
|
remote_name = token_entry_field(token_entry, "remote") or str(token_name)
|
||||||
|
|
||||||
token_record = {
|
token_record = {
|
||||||
"token_id": str(token_name),
|
"id": remote_name,
|
||||||
"value": token_value,
|
"value": token_value,
|
||||||
"server": server_record_from_info(server_info),
|
"server": server_record_from_info(server_info),
|
||||||
"user": str(user_name),
|
"user": str(user_name),
|
||||||
"remotes": [],
|
"org": token_entry_field(token_entry, "org"),
|
||||||
|
"repo": token_entry_field(token_entry, "repo"),
|
||||||
}
|
}
|
||||||
expires_at = token_entry_expires_at(token_entry)
|
expires_at = token_entry_expires_at(token_entry)
|
||||||
if expires_at:
|
if expires_at:
|
||||||
token_record["expires_at"] = expires_at
|
token_record["expires_at"] = expires_at
|
||||||
|
|
||||||
remote_name = token_entry_field(token_entry, "remote")
|
|
||||||
if remote_name:
|
|
||||||
remote_record = {"name": remote_name, "org": "", "repo": ""}
|
|
||||||
for source_name, target_name in [("org", "org"), ("repo", "repo")]:
|
|
||||||
source_value = token_entry_field(token_entry, source_name)
|
|
||||||
if source_value:
|
|
||||||
remote_record[target_name] = source_value
|
|
||||||
token_record["remotes"].append(remote_record)
|
|
||||||
|
|
||||||
append_token_record(token_data, token_record)
|
append_token_record(token_data, token_record)
|
||||||
|
|
||||||
return token_data
|
return token_data
|
||||||
@@ -534,8 +680,7 @@ def normalize_token_store(config: WorkspaceConfig, raw_data: dict) -> dict:
|
|||||||
if raw_data.get("version") == 3 and isinstance(raw_data.get("tokens"), list):
|
if raw_data.get("version") == 3 and isinstance(raw_data.get("tokens"), list):
|
||||||
token_data = empty_token_store()
|
token_data = empty_token_store()
|
||||||
for raw_token in raw_data["tokens"]:
|
for raw_token in raw_data["tokens"]:
|
||||||
token_record = normalize_v3_token(config, raw_token)
|
for token_record in normalize_v3_tokens(config, raw_token):
|
||||||
if token_record is not None:
|
|
||||||
append_token_record(token_data, token_record)
|
append_token_record(token_data, token_record)
|
||||||
return token_data
|
return token_data
|
||||||
|
|
||||||
@@ -582,7 +727,7 @@ def write_token_store(config: WorkspaceConfig, token_data: dict) -> None:
|
|||||||
os.chmod(config.token_path.parent, 0o700)
|
os.chmod(config.token_path.parent, 0o700)
|
||||||
|
|
||||||
temp_path = config.token_path.with_suffix(config.token_path.suffix + ".tmp")
|
temp_path = config.token_path.with_suffix(config.token_path.suffix + ".tmp")
|
||||||
temp_path.write_text(json.dumps(token_data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
temp_path.write_text(json.dumps(token_data, indent=2) + "\n", encoding="utf-8")
|
||||||
os.chmod(temp_path, 0o600)
|
os.chmod(temp_path, 0o600)
|
||||||
temp_path.replace(config.token_path)
|
temp_path.replace(config.token_path)
|
||||||
os.chmod(config.token_path, 0o600)
|
os.chmod(config.token_path, 0o600)
|
||||||
@@ -595,110 +740,98 @@ def token_server_endpoint(token_record: dict) -> str:
|
|||||||
return str(server_record.get("endpoint", ""))
|
return str(server_record.get("endpoint", ""))
|
||||||
|
|
||||||
|
|
||||||
def find_token_record(token_data: dict, endpoint: str, token_id: str) -> dict | None:
|
def token_record_id(token_record: dict) -> str:
|
||||||
|
return str(token_record.get("id", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def find_token_record(token_data: dict, endpoint: str, remote_id: str) -> dict | None:
|
||||||
for token_record in token_data.get("tokens", []):
|
for token_record in token_data.get("tokens", []):
|
||||||
if token_server_endpoint(token_record) == endpoint and token_record.get("token_id") == token_id:
|
if token_server_endpoint(token_record) == endpoint and token_record_id(token_record) == remote_id:
|
||||||
return token_record
|
return token_record
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def find_orphan_token_by_value(token_data: dict, endpoint: str, token_value: str) -> dict | None:
|
def apply_authz_to_token(token_record: dict, authz: dict) -> bool:
|
||||||
for token_record in token_data.get("tokens", []):
|
|
||||||
if token_server_endpoint(token_record) != endpoint:
|
|
||||||
continue
|
|
||||||
if token_record.get("value") != token_value:
|
|
||||||
continue
|
|
||||||
if token_record.get("remotes"):
|
|
||||||
continue
|
|
||||||
return token_record
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_remote_record(token_record: dict, remote_name: str) -> dict | None:
|
|
||||||
for remote_record in token_record.setdefault("remotes", []):
|
|
||||||
if remote_record.get("name") == remote_name:
|
|
||||||
return remote_record
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def upsert_remote_record(token_record: dict, remote_name: str, server_info: dict) -> tuple[dict, bool]:
|
|
||||||
remote_record = find_remote_record(token_record, remote_name)
|
|
||||||
changed = False
|
|
||||||
if remote_record is None:
|
|
||||||
remote_record = {"name": remote_name, "org": "", "repo": ""}
|
|
||||||
token_record.setdefault("remotes", []).append(remote_record)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
for source_name, target_name in [("org", "org"), ("repo", "repo")]:
|
|
||||||
source_value = server_info.get(source_name)
|
|
||||||
if isinstance(source_value, str) and source_value and remote_record.get(target_name) != source_value:
|
|
||||||
remote_record[target_name] = source_value
|
|
||||||
changed = True
|
|
||||||
return remote_record, changed
|
|
||||||
|
|
||||||
|
|
||||||
def apply_authz_to_token(token_record: dict, remote_record: dict, authz: dict) -> bool:
|
|
||||||
changed = False
|
changed = False
|
||||||
for source_name, target_name in [
|
for source_name, target_name in [
|
||||||
("user", "user"),
|
("user", "user"),
|
||||||
("valid", "valid"),
|
("valid", "valid"),
|
||||||
("scope_mask", "scope"),
|
|
||||||
]:
|
]:
|
||||||
source_value = authz.get(source_name)
|
source_value = authz.get(source_name)
|
||||||
if isinstance(source_value, str) and source_value and token_record.get(target_name) != source_value:
|
if isinstance(source_value, str) and source_value and token_record.get(target_name) != source_value:
|
||||||
token_record[target_name] = source_value
|
token_record[target_name] = source_value
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
|
scope_value = authz.get("scope_mask")
|
||||||
|
if isinstance(scope_value, str):
|
||||||
|
scope_record = permission_map_from_mask(scope_value, SCOPE_FIELDS, "rw?!-")
|
||||||
|
if scope_record is not None and token_record.get("scope") != scope_record:
|
||||||
|
token_record["scope"] = scope_record
|
||||||
|
changed = True
|
||||||
|
|
||||||
for source_name, target_name in [
|
for source_name, target_name in [
|
||||||
("org_mask", "org_perm"),
|
("org_mask", "org_perm"),
|
||||||
("repo_mask", "repo_perm"),
|
("repo_mask", "repo_perm"),
|
||||||
]:
|
]:
|
||||||
source_value = authz.get(source_name)
|
source_value = authz.get(source_name)
|
||||||
if isinstance(source_value, str) and source_value and remote_record.get(target_name) != source_value:
|
if not isinstance(source_value, str):
|
||||||
remote_record[target_name] = source_value
|
continue
|
||||||
|
fields = ORG_PERMISSION_FIELDS if target_name == "org_perm" else REPO_PERMISSION_FIELDS
|
||||||
|
permission_record = permission_map_from_mask(source_value, fields, "+?!-")
|
||||||
|
if permission_record is not None and token_record.get(target_name) != permission_record:
|
||||||
|
token_record[target_name] = permission_record
|
||||||
changed = True
|
changed = True
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def register_token(token_data: dict, server_info: dict, remote_name: str, token_id: str, token_value: str) -> str:
|
def register_token(
|
||||||
|
token_data: dict,
|
||||||
|
server_info: dict,
|
||||||
|
remote_name: str,
|
||||||
|
user_name: str,
|
||||||
|
token_value: str,
|
||||||
|
hydrate: bool = True,
|
||||||
|
) -> str:
|
||||||
endpoint = server_info["endpoint"]
|
endpoint = server_info["endpoint"]
|
||||||
token_record = find_token_record(token_data, endpoint, token_id)
|
token_record = find_token_record(token_data, endpoint, remote_name)
|
||||||
if token_record is None:
|
|
||||||
token_record = find_orphan_token_by_value(token_data, endpoint, token_value)
|
|
||||||
if token_record is None:
|
if token_record is None:
|
||||||
token_record = {
|
token_record = {
|
||||||
"token_id": token_id,
|
"id": remote_name,
|
||||||
"value": token_value,
|
"value": token_value,
|
||||||
"server": server_record_from_info(server_info),
|
"server": server_record_from_info(server_info),
|
||||||
"remotes": [],
|
"user": user_name,
|
||||||
|
"org": str(server_info.get("org", "")),
|
||||||
|
"repo": str(server_info.get("repo", "")),
|
||||||
}
|
}
|
||||||
token_data.setdefault("tokens", []).append(token_record)
|
token_data.setdefault("tokens", []).append(token_record)
|
||||||
result = "added"
|
result = "added"
|
||||||
else:
|
|
||||||
token_record["token_id"] = token_id
|
|
||||||
result = "updated"
|
|
||||||
else:
|
else:
|
||||||
result = "existing"
|
result = "existing"
|
||||||
if token_record.get("value") != token_value:
|
if token_record.get("value") != token_value:
|
||||||
token_record["value"] = token_value
|
token_record["value"] = token_value
|
||||||
result = "updated"
|
result = "updated"
|
||||||
|
if token_record.get("user") != user_name:
|
||||||
|
token_record["user"] = user_name
|
||||||
|
result = "updated"
|
||||||
if token_record.get("server") != server_record_from_info(server_info):
|
if token_record.get("server") != server_record_from_info(server_info):
|
||||||
token_record["server"] = server_record_from_info(server_info)
|
token_record["server"] = server_record_from_info(server_info)
|
||||||
result = "updated"
|
result = "updated"
|
||||||
|
for source_name, target_name in [("org", "org"), ("repo", "repo")]:
|
||||||
remote_record, remote_changed = upsert_remote_record(token_record, remote_name, server_info)
|
source_value = server_info.get(source_name)
|
||||||
if remote_changed and result == "existing":
|
if isinstance(source_value, str) and token_record.get(target_name) != source_value:
|
||||||
|
token_record[target_name] = source_value
|
||||||
result = "updated"
|
result = "updated"
|
||||||
|
|
||||||
|
if hydrate:
|
||||||
authz = load_gitea_authz(
|
authz = load_gitea_authz(
|
||||||
endpoint,
|
endpoint,
|
||||||
token_id,
|
remote_name,
|
||||||
token_value,
|
token_value,
|
||||||
token_record.get("expires_at", ""),
|
token_record.get("expires_at", ""),
|
||||||
remote_record.get("org", ""),
|
token_record.get("org", ""),
|
||||||
remote_record.get("repo", ""),
|
token_record.get("repo", ""),
|
||||||
)
|
)
|
||||||
if apply_authz_to_token(token_record, remote_record, authz) and result == "existing":
|
if apply_authz_to_token(token_record, authz) and result == "existing":
|
||||||
result = "updated"
|
result = "updated"
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -804,7 +937,7 @@ def scan_repo_remotes(config: WorkspaceConfig, repo_path: Path) -> dict:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
auth_urls += 1
|
auth_urls += 1
|
||||||
token_id, token_value = credentials
|
user_name, token_value = credentials
|
||||||
row_index = len(remote_rows)
|
row_index = len(remote_rows)
|
||||||
remote_rows.append(
|
remote_rows.append(
|
||||||
{
|
{
|
||||||
@@ -814,12 +947,13 @@ def scan_repo_remotes(config: WorkspaceConfig, repo_path: Path) -> dict:
|
|||||||
"org": server_info["org"],
|
"org": server_info["org"],
|
||||||
"repo": server_info["repo"],
|
"repo": server_info["repo"],
|
||||||
"url_kind": "auth",
|
"url_kind": "auth",
|
||||||
"token_id": token_id,
|
"token_id": remote_name,
|
||||||
|
"user": user_name,
|
||||||
"token_value": token_value,
|
"token_value": token_value,
|
||||||
"result": "found",
|
"result": "found",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
found_credentials.append((row_index, server_info, remote_name, token_id, token_value))
|
found_credentials.append((row_index, server_info, remote_name, user_name, token_value))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"repo_root": repo_root,
|
"repo_root": repo_root,
|
||||||
@@ -848,8 +982,8 @@ def sync_tokens_from_repo(config: WorkspaceConfig, repo_path: Path) -> dict:
|
|||||||
token_data = load_token_store(config)
|
token_data = load_token_store(config)
|
||||||
added = 0
|
added = 0
|
||||||
updated = 0
|
updated = 0
|
||||||
for row_index, server_info, remote_name, token_id, token_value in found_credentials:
|
for row_index, server_info, remote_name, user_name, token_value in found_credentials:
|
||||||
result = register_token(token_data, server_info, remote_name, token_id, token_value)
|
result = register_token(token_data, server_info, remote_name, user_name, token_value)
|
||||||
if result == "added":
|
if result == "added":
|
||||||
added += 1
|
added += 1
|
||||||
elif result == "updated":
|
elif result == "updated":
|
||||||
@@ -866,6 +1000,67 @@ def sync_tokens_from_repo(config: WorkspaceConfig, repo_path: Path) -> dict:
|
|||||||
return report
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def sync_token_from_remote(config: WorkspaceConfig, repo_path: Path, remote_name: str, dry_run: bool = False) -> dict:
|
||||||
|
repo_root = git_repo_root(repo_path)
|
||||||
|
if repo_root is None:
|
||||||
|
raise SystemExit(f"Missing git repo at path: {repo_path}")
|
||||||
|
|
||||||
|
remote_url = git_capture(repo_root, ["remote", "get-url", remote_name])
|
||||||
|
if not remote_url:
|
||||||
|
raise SystemExit(f"Missing git remote: {remote_name}")
|
||||||
|
server_info = server_info_from_url(config, remote_url)
|
||||||
|
if server_info is None:
|
||||||
|
raise SystemExit(f"Only http/https remote URLs are supported: {remote_url}")
|
||||||
|
credentials = remote_credentials(remote_url)
|
||||||
|
if credentials is None:
|
||||||
|
raise SystemExit(f"Remote '{remote_name}' has no credentials.")
|
||||||
|
|
||||||
|
user_name, token_value = credentials
|
||||||
|
result = "dry-run"
|
||||||
|
if not dry_run:
|
||||||
|
token_data = load_token_store(config)
|
||||||
|
result = register_token(token_data, server_info, remote_name, user_name, token_value, hydrate=False)
|
||||||
|
write_token_store(config, token_data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"repo_root": str(repo_root),
|
||||||
|
"remote": remote_name,
|
||||||
|
"server": server_info["endpoint"],
|
||||||
|
"user": user_name,
|
||||||
|
"org": server_info.get("org", ""),
|
||||||
|
"repo": server_info.get("repo", ""),
|
||||||
|
"status": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def hydrate_token_from_api(config: WorkspaceConfig, remote_name: str, endpoint: str | None, dry_run: bool = False) -> dict:
|
||||||
|
token_data = load_token_store(config, write_normalized=not dry_run)
|
||||||
|
token_record = resolve_token_from_store(token_data, endpoint, remote_name, None)
|
||||||
|
token_value = str(token_record.get("value", ""))
|
||||||
|
if not token_value:
|
||||||
|
raise SystemExit(f"Token value is empty in tokens.json: {token_server_endpoint(token_record)}:{remote_name}")
|
||||||
|
|
||||||
|
authz = load_gitea_authz(
|
||||||
|
token_server_endpoint(token_record),
|
||||||
|
remote_name,
|
||||||
|
token_value,
|
||||||
|
token_record.get("expires_at", ""),
|
||||||
|
token_record.get("org", ""),
|
||||||
|
token_record.get("repo", ""),
|
||||||
|
)
|
||||||
|
changed = apply_authz_to_token(token_record, authz)
|
||||||
|
if changed and not dry_run:
|
||||||
|
write_token_store(config, token_data)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"remote": remote_name,
|
||||||
|
"server": token_server_endpoint(token_record),
|
||||||
|
"user": str(token_record.get("user", "")),
|
||||||
|
"valid": str(token_record.get("valid", "")),
|
||||||
|
"status": "dry-run" if dry_run else ("updated" if changed else "unchanged"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def count_server_tokens(server_entry: dict) -> int:
|
def count_server_tokens(server_entry: dict) -> int:
|
||||||
return len(server_entry.get("tokens", []))
|
return len(server_entry.get("tokens", []))
|
||||||
|
|
||||||
@@ -900,26 +1095,23 @@ def store_servers_from_tokens(token_data: dict) -> dict[str, dict]:
|
|||||||
if server_entry.get(field_name) in {"", None, "unknown"} and server_record.get(field_name) not in {"", None}:
|
if server_entry.get(field_name) in {"", None, "unknown"} and server_record.get(field_name) not in {"", None}:
|
||||||
server_entry[field_name] = server_record.get(field_name)
|
server_entry[field_name] = server_record.get(field_name)
|
||||||
|
|
||||||
remotes = token_record.get("remotes", [])
|
token_id = token_record_id(token_record)
|
||||||
if not isinstance(remotes, list) or not remotes:
|
if not token_id:
|
||||||
remotes = [{}]
|
|
||||||
for remote_record in remotes:
|
|
||||||
if not isinstance(remote_record, dict):
|
|
||||||
continue
|
continue
|
||||||
server_entry["tokens"].append(
|
server_entry["tokens"].append(
|
||||||
{
|
{
|
||||||
"endpoint": endpoint,
|
"endpoint": endpoint,
|
||||||
"token_id": str(token_record.get("token_id", "")),
|
"token_id": token_id,
|
||||||
"token_value": str(token_record.get("value", "")),
|
"token_value": str(token_record.get("value", "")),
|
||||||
"user": str(token_record.get("user", "")),
|
"user": str(token_record.get("user", "")),
|
||||||
"valid": str(token_record.get("valid", "")),
|
"valid": str(token_record.get("valid", "")),
|
||||||
"scope_mask": str(token_record.get("scope", "")),
|
"scope_mask": permission_map_to_mask(token_record.get("scope"), SCOPE_FIELDS, "rw?!-"),
|
||||||
"expires_at": str(token_record.get("expires_at", "")),
|
"expires_at": str(token_record.get("expires_at", "")),
|
||||||
"remote": str(remote_record.get("name", "")),
|
"remote": token_id,
|
||||||
"owner": str(remote_record.get("org", "")),
|
"owner": str(token_record.get("org", "")),
|
||||||
"repo_name": str(remote_record.get("repo", "")),
|
"repo_name": str(token_record.get("repo", "")),
|
||||||
"org_mask": str(remote_record.get("org_perm", "")),
|
"org_mask": permission_map_to_mask(token_record.get("org_perm"), ORG_PERMISSION_FIELDS, "+?!-"),
|
||||||
"repo_mask": str(remote_record.get("repo_perm", "")),
|
"repo_mask": permission_map_to_mask(token_record.get("repo_perm"), REPO_PERMISSION_FIELDS, "+?!-"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return servers
|
return servers
|
||||||
@@ -938,7 +1130,7 @@ def token_pairs_from_entry(server_entry: dict) -> set[tuple[str, str, str, str,
|
|||||||
token_pairs.add(
|
token_pairs.add(
|
||||||
(
|
(
|
||||||
str(row.get("remote", "")),
|
str(row.get("remote", "")),
|
||||||
str(token_id),
|
str(row.get("user", "")),
|
||||||
str(token_value),
|
str(token_value),
|
||||||
str(row.get("org", row.get("owner", ""))),
|
str(row.get("org", row.get("owner", ""))),
|
||||||
str(row.get("repo", row.get("repo_name", ""))),
|
str(row.get("repo", row.get("repo_name", ""))),
|
||||||
@@ -997,13 +1189,14 @@ def collect_repo_server_entries(config: WorkspaceConfig, repo_path: Path) -> tup
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
entry["auth_urls"] += 1
|
entry["auth_urls"] += 1
|
||||||
token_id, token_value = credentials
|
user_name, token_value = credentials
|
||||||
entry["tokens"].append(
|
entry["tokens"].append(
|
||||||
{
|
{
|
||||||
"remote": remote_name,
|
"remote": remote_name,
|
||||||
"org": server_info["org"],
|
"org": server_info["org"],
|
||||||
"repo": server_info["repo"],
|
"repo": server_info["repo"],
|
||||||
"token_id": token_id,
|
"token_id": remote_name,
|
||||||
|
"user": user_name,
|
||||||
"token_value": token_value,
|
"token_value": token_value,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1189,6 +1382,7 @@ def repo_token_rows(
|
|||||||
"owner": row.get("org", ""),
|
"owner": row.get("org", ""),
|
||||||
"repo_name": row.get("repo", ""),
|
"repo_name": row.get("repo", ""),
|
||||||
"token_id": row.get("token_id", ""),
|
"token_id": row.get("token_id", ""),
|
||||||
|
"user": row.get("user", ""),
|
||||||
"token_value": row.get("token_value", ""),
|
"token_value": row.get("token_value", ""),
|
||||||
}
|
}
|
||||||
for row in scan_rows
|
for row in scan_rows
|
||||||
@@ -1208,6 +1402,7 @@ def repo_token_rows(
|
|||||||
"owner": row.get("org", ""),
|
"owner": row.get("org", ""),
|
||||||
"repo_name": row.get("repo", ""),
|
"repo_name": row.get("repo", ""),
|
||||||
"token_id": row.get("token_id", ""),
|
"token_id": row.get("token_id", ""),
|
||||||
|
"user": row.get("user", ""),
|
||||||
"token_value": row.get("token_value", ""),
|
"token_value": row.get("token_value", ""),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1259,20 +1454,9 @@ def api_get_json(api_url: str, token_value: str, user_name: str = "", basic: boo
|
|||||||
|
|
||||||
def scope_mask(scopes: list[str]) -> str:
|
def scope_mask(scopes: list[str]) -> str:
|
||||||
scope_set = {scope.lower() for scope in scopes}
|
scope_set = {scope.lower() for scope in scopes}
|
||||||
categories = [
|
|
||||||
"activitypub",
|
|
||||||
"admin",
|
|
||||||
"issue",
|
|
||||||
"misc",
|
|
||||||
"notification",
|
|
||||||
"organization",
|
|
||||||
"package",
|
|
||||||
"repository",
|
|
||||||
"user",
|
|
||||||
]
|
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
for category in categories:
|
for _, category in SCOPE_FIELDS:
|
||||||
if f"write:{category}" in scope_set:
|
if f"write:{category}" in scope_set:
|
||||||
values.append("w")
|
values.append("w")
|
||||||
elif f"read:{category}" in scope_set:
|
elif f"read:{category}" in scope_set:
|
||||||
@@ -1448,6 +1632,7 @@ def print_token_item_rows(
|
|||||||
if store_row:
|
if store_row:
|
||||||
repo_fields_match = (
|
repo_fields_match = (
|
||||||
store_row["token_id"] == row["token_id"]
|
store_row["token_id"] == row["token_id"]
|
||||||
|
and store_row["user"] == row.get("user", "")
|
||||||
and store_row["token_value"] == token_value
|
and store_row["token_value"] == token_value
|
||||||
and store_row["owner"] == row["owner"]
|
and store_row["owner"] == row["owner"]
|
||||||
and store_row["repo_name"] == row["repo_name"]
|
and store_row["repo_name"] == row["repo_name"]
|
||||||
@@ -1458,7 +1643,7 @@ def print_token_item_rows(
|
|||||||
marker = "!" if store_row.get("valid") in {"!", "invalid"} else "*"
|
marker = "!" if store_row.get("valid") in {"!", "invalid"} else "*"
|
||||||
append_token_row(
|
append_token_row(
|
||||||
endpoint,
|
endpoint,
|
||||||
synced_store_row.get("user", "") if synced_store_row else "",
|
synced_store_row.get("user", "") if synced_store_row else row.get("user", ""),
|
||||||
row["remote"],
|
row["remote"],
|
||||||
token_id,
|
token_id,
|
||||||
token_value,
|
token_value,
|
||||||
@@ -1491,7 +1676,7 @@ def print_token_item_rows(
|
|||||||
def resolve_token_from_store(
|
def resolve_token_from_store(
|
||||||
token_data: dict,
|
token_data: dict,
|
||||||
endpoint: str | None,
|
endpoint: str | None,
|
||||||
token_id: str | None,
|
remote_id: str | None,
|
||||||
user_name: str | None,
|
user_name: str | None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
candidates = []
|
candidates = []
|
||||||
@@ -1499,7 +1684,7 @@ def resolve_token_from_store(
|
|||||||
token_endpoint = token_server_endpoint(token_record)
|
token_endpoint = token_server_endpoint(token_record)
|
||||||
if endpoint and token_endpoint != endpoint:
|
if endpoint and token_endpoint != endpoint:
|
||||||
continue
|
continue
|
||||||
if token_id and token_record.get("token_id") != token_id:
|
if remote_id and token_record_id(token_record) != remote_id:
|
||||||
continue
|
continue
|
||||||
if user_name and token_record.get("user") != user_name:
|
if user_name and token_record.get("user") != user_name:
|
||||||
continue
|
continue
|
||||||
@@ -1510,11 +1695,11 @@ def resolve_token_from_store(
|
|||||||
if len(candidates) > 1:
|
if len(candidates) > 1:
|
||||||
available = ", ".join(
|
available = ", ".join(
|
||||||
sorted(
|
sorted(
|
||||||
f"{token_server_endpoint(token_record)}:{token_record.get('token_id', '')}"
|
f"{token_server_endpoint(token_record)}:{token_record_id(token_record)}"
|
||||||
for token_record in candidates
|
for token_record in candidates
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
raise SystemExit(f"Multiple matching tokens. Pass --server/--token-name. Available: {available}")
|
raise SystemExit(f"Multiple matching tokens. Pass --server/--token-name or --remote. Available: {available}")
|
||||||
return candidates[0]
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
@@ -1585,10 +1770,10 @@ def run_tokens_read(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
print(f"host\t{server_entry.get('host', '')}")
|
print(f"host\t{server_entry.get('host', '')}")
|
||||||
print(f"port\t{server_entry.get('port', '')}")
|
print(f"port\t{server_entry.get('port', '')}")
|
||||||
print(f"tokens\t{count_server_tokens(server_entry)}")
|
print(f"tokens\t{count_server_tokens(server_entry)}")
|
||||||
for row in sorted(server_entry.get("tokens", []), key=lambda item: (item.get("token_id", ""), item.get("remote", ""))):
|
for row in sorted(server_entry.get("tokens", []), key=lambda item: item.get("token_id", "")):
|
||||||
token_value = row.get("token_value", "")
|
token_value = row.get("token_value", "")
|
||||||
secret_value = token_value if args.show_secrets else mask_secret(token_value)
|
secret_value = token_value if args.show_secrets else mask_secret(token_value)
|
||||||
print(f"token\t{row.get('token_id', '')}\t{secret_value}")
|
print(f"id\t{row.get('token_id', '')}\t{secret_value}")
|
||||||
for field_name in ["user", "valid", "scope_mask", "expires_at"]:
|
for field_name in ["user", "valid", "scope_mask", "expires_at"]:
|
||||||
field_value = row.get(field_name, "")
|
field_value = row.get(field_name, "")
|
||||||
if field_value:
|
if field_value:
|
||||||
@@ -1622,6 +1807,26 @@ def run_tokens_stats(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
print_status_counts(status_counts)
|
print_status_counts(status_counts)
|
||||||
|
|
||||||
|
|
||||||
|
def print_token_action_result(result: dict[str, str]) -> None:
|
||||||
|
for field_name in ["repo_root", "remote", "server", "user", "org", "repo", "valid", "status"]:
|
||||||
|
field_value = result.get(field_name)
|
||||||
|
if field_value is not None:
|
||||||
|
print(f"{field_name}\t{field_value}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_tokens_sync(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
if args.direction == "remote":
|
||||||
|
result = sync_token_from_remote(config, resolve_repo_argument(args.repo), args.remote_id, args.dry_run)
|
||||||
|
print_token_action_result(result)
|
||||||
|
return
|
||||||
|
if args.direction == "store":
|
||||||
|
args.remote = args.remote_id
|
||||||
|
args.token_name = args.remote_id
|
||||||
|
run_tokens_write(config, args)
|
||||||
|
return
|
||||||
|
raise SystemExit(f"Unsupported tokens sync direction: {args.direction}")
|
||||||
|
|
||||||
|
|
||||||
def run_tokens_add(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
def run_tokens_add(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
server_url = (args.server or config.git.base_url).rstrip("/")
|
server_url = (args.server or config.git.base_url).rstrip("/")
|
||||||
server_info = server_info_from_url(config, server_url)
|
server_info = server_info_from_url(config, server_url)
|
||||||
@@ -1629,32 +1834,29 @@ def run_tokens_add(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
raise SystemExit(f"Only http/https server URLs are supported: {server_url}")
|
raise SystemExit(f"Only http/https server URLs are supported: {server_url}")
|
||||||
|
|
||||||
token_data = load_token_store(config)
|
token_data = load_token_store(config)
|
||||||
existing_token = find_token_record(token_data, server_info["endpoint"], args.token_id)
|
remote_id = args.token_id
|
||||||
|
if args.remote and args.remote != remote_id:
|
||||||
|
raise SystemExit("Token id must match --remote because tokens.json is keyed 1:1 by git remote name.")
|
||||||
|
|
||||||
|
existing_token = find_token_record(token_data, server_info["endpoint"], remote_id)
|
||||||
if existing_token is not None:
|
if existing_token is not None:
|
||||||
raise SystemExit(f"Token already exists in tokens.json: {server_info['endpoint']}:{args.token_id}")
|
raise SystemExit(f"Token already exists in tokens.json: {server_info['endpoint']}:{remote_id}")
|
||||||
|
|
||||||
token_record = {
|
token_record = {
|
||||||
"token_id": args.token_id,
|
"id": remote_id,
|
||||||
"value": args.value or "",
|
"value": args.value or "",
|
||||||
"server": server_record_from_info(server_info),
|
"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 "",
|
"org": args.org or "",
|
||||||
"repo": args.repo_name or "",
|
"repo": args.repo_name or "",
|
||||||
}
|
}
|
||||||
)
|
if args.user:
|
||||||
|
token_record["user"] = args.user
|
||||||
|
|
||||||
print(f"token_path\t{config.token_path}")
|
print(f"token_path\t{config.token_path}")
|
||||||
print(f"server\t{server_info['endpoint']}")
|
print(f"server\t{server_info['endpoint']}")
|
||||||
print(f"token_id\t{args.token_id}")
|
print(f"id\t{remote_id}")
|
||||||
print(f"value\t{'set' if token_record['value'] else 'empty'}")
|
print(f"value\t{'set' if token_record['value'] else 'empty'}")
|
||||||
print(f"remote\t{args.remote or ''}")
|
print(f"remote\t{remote_id}")
|
||||||
print(f"status\t{'dry-run' if args.dry_run else 'added'}")
|
print(f"status\t{'dry-run' if args.dry_run else 'added'}")
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
@@ -1686,19 +1888,22 @@ def run_tokens_write(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
token_record = resolve_token_from_store(
|
token_record = resolve_token_from_store(
|
||||||
token_data,
|
token_data,
|
||||||
args.server or target_server_info["endpoint"],
|
args.server or target_server_info["endpoint"],
|
||||||
args.token_name,
|
args.token_name or args.remote,
|
||||||
args.user,
|
args.user,
|
||||||
)
|
)
|
||||||
server_endpoint = token_server_endpoint(token_record)
|
server_endpoint = token_server_endpoint(token_record)
|
||||||
token_id = str(token_record.get("token_id", ""))
|
remote_id = token_record_id(token_record)
|
||||||
|
user_name = str(token_record.get("user", ""))
|
||||||
token_value = str(token_record.get("value", ""))
|
token_value = str(token_record.get("value", ""))
|
||||||
if not token_value:
|
if not token_value:
|
||||||
raise SystemExit(f"Token value is empty in tokens.json: {server_endpoint}:{token_id}")
|
raise SystemExit(f"Token value is empty in tokens.json: {server_endpoint}:{remote_id}")
|
||||||
|
if not user_name:
|
||||||
|
raise SystemExit(f"Token user is empty in tokens.json: {server_endpoint}:{remote_id}")
|
||||||
|
|
||||||
final_url = credentialed_remote_url(target_url, token_id, token_value)
|
final_url = credentialed_remote_url(target_url, user_name, token_value)
|
||||||
status = "added" if existing_remote_url is None else "updated"
|
status = "added" if existing_remote_url is None else "updated"
|
||||||
existing_credentials = remote_credentials(existing_remote_url) if existing_remote_url else None
|
existing_credentials = remote_credentials(existing_remote_url) if existing_remote_url else None
|
||||||
if existing_credentials == (token_id, token_value):
|
if existing_credentials == (user_name, token_value):
|
||||||
status = "unchanged"
|
status = "unchanged"
|
||||||
elif existing_credentials is not None and not args.replace:
|
elif existing_credentials is not None and not args.replace:
|
||||||
raise SystemExit(
|
raise SystemExit(
|
||||||
@@ -1709,8 +1914,8 @@ def run_tokens_write(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
print(f"repo_root\t{repo_root}")
|
print(f"repo_root\t{repo_root}")
|
||||||
print(f"remote\t{args.remote}")
|
print(f"remote\t{args.remote}")
|
||||||
print(f"server\t{server_endpoint}")
|
print(f"server\t{server_endpoint}")
|
||||||
print(f"user\t{token_record.get('user', '')}")
|
print(f"user\t{user_name}")
|
||||||
print(f"token_name\t{token_id}")
|
print(f"id\t{remote_id}")
|
||||||
print(f"status\t{status}")
|
print(f"status\t{status}")
|
||||||
print(f"url\t{sanitize_remote_url(final_url)[0]}")
|
print(f"url\t{sanitize_remote_url(final_url)[0]}")
|
||||||
|
|
||||||
@@ -1721,6 +1926,11 @@ def run_tokens_write(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def run_tokens_update(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
def run_tokens_update(config: WorkspaceConfig, args: argparse.Namespace) -> None:
|
||||||
|
if args.remote_id:
|
||||||
|
result = hydrate_token_from_api(config, args.remote_id, args.server, args.dry_run)
|
||||||
|
print_token_action_result(result)
|
||||||
|
return
|
||||||
|
|
||||||
if args.from_source == "remotes":
|
if args.from_source == "remotes":
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
run_tokens_scan(config, args)
|
run_tokens_scan(config, args)
|
||||||
@@ -1736,7 +1946,7 @@ def run_tokens_update(config: WorkspaceConfig, args: argparse.Namespace) -> None
|
|||||||
if args.from_source == "store":
|
if args.from_source == "store":
|
||||||
run_tokens_write(config, args)
|
run_tokens_write(config, args)
|
||||||
return
|
return
|
||||||
raise SystemExit(f"Unsupported tokens update source: {args.from_source}")
|
raise SystemExit("Pass REMOTE_ID for API metadata refresh or use legacy --from remotes|store.")
|
||||||
|
|
||||||
|
|
||||||
def git_current_branch(config: WorkspaceConfig, repo_path: Path) -> str:
|
def git_current_branch(config: WorkspaceConfig, repo_path: Path) -> str:
|
||||||
@@ -1959,7 +2169,7 @@ def add_repo_option(parser: argparse.ArgumentParser) -> None:
|
|||||||
def add_store_selection_options(parser: argparse.ArgumentParser) -> None:
|
def add_store_selection_options(parser: argparse.ArgumentParser) -> None:
|
||||||
parser.add_argument("--server", help="Server endpoint from tokens.json.")
|
parser.add_argument("--server", help="Server endpoint from tokens.json.")
|
||||||
parser.add_argument("--user", help="API user recorded on the token.")
|
parser.add_argument("--user", help="API user recorded on the token.")
|
||||||
parser.add_argument("--token-name", help="Token id inside tokens.json, for example 't1'.")
|
parser.add_argument("--token-name", help="Remote id inside tokens.json, for example 'r1'. Default: --remote.")
|
||||||
|
|
||||||
|
|
||||||
def print_table(headers: list[str], rows: list[list[str]]) -> None:
|
def print_table(headers: list[str], rows: list[list[str]]) -> None:
|
||||||
@@ -2017,7 +2227,7 @@ def print_main_overview(config_path: Path) -> None:
|
|||||||
["list-cards", "[series]", "list cards in a selected series"],
|
["list-cards", "[series]", "list cards in a selected series"],
|
||||||
["tmux-container", "[series] [card]", "start tmux with container in pane 0"],
|
["tmux-container", "[series] [card]", "start tmux with container in pane 0"],
|
||||||
["submission", "[series] [card] --class K --nick N", "prepare answer repo and student branch"],
|
["submission", "[series] [card] --class K --nick N", "prepare answer repo and student branch"],
|
||||||
["tokens", "scan|add|read|stats|write|update", "manage tokens.json and Git remote credentials"],
|
["tokens", "scan|sync|update|add|read|stats|write", "manage tokens.json and Git remote credentials"],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
@@ -2054,11 +2264,13 @@ def print_tokens_overview() -> None:
|
|||||||
["command", "common options", "direction", "purpose"],
|
["command", "common options", "direction", "purpose"],
|
||||||
[
|
[
|
||||||
["scan", "[--repo PATH]", "read-only", "print token table with remote/store marker and auth masks"],
|
["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"],
|
["add", "REMOTE_ID", "tokens.json", "add an empty token skeleton for manual editing"],
|
||||||
["read", "[--server ENDPOINT]", "tokens.json", "show servers, token ids and remotes"],
|
["read", "[--server ENDPOINT]", "tokens.json", "show servers, remote ids and tokens"],
|
||||||
["stats", "[--repo PATH]", "repo + tokens.json", "compare remote URLs with local token store"],
|
["stats", "[--repo PATH]", "repo + tokens.json", "compare remote URLs with local token store"],
|
||||||
|
["sync remote", "REMOTE_ID [--repo PATH]", "repo -> tokens.json", "copy one remote URL into the store"],
|
||||||
|
["sync store", "REMOTE_ID [--repo PATH]", "tokens.json -> repo", "write one store record into a remote URL"],
|
||||||
["write", "--remote R [--replace]", "tokens.json -> repo", "write selected token into a remote URL"],
|
["write", "--remote R [--replace]", "tokens.json -> repo", "write selected token into a remote URL"],
|
||||||
["update", "--from remotes|store", "selected", "copy token data in an explicit direction"],
|
["update", "REMOTE_ID", "API -> tokens.json", "refresh valid/scope/org/repo metadata"],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
@@ -2067,9 +2279,12 @@ def print_tokens_overview() -> None:
|
|||||||
["case", "command"],
|
["case", "command"],
|
||||||
[
|
[
|
||||||
["scan current repo", "./rvctl tokens scan"],
|
["scan current repo", "./rvctl tokens scan"],
|
||||||
|
["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"],
|
||||||
["scan selected card", "./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03"],
|
["scan selected card", "./rvctl tokens scan --repo ~/dev/workspace/rv/series/inf/03"],
|
||||||
["compare state", "./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03"],
|
["compare state", "./rvctl tokens stats --repo ~/dev/workspace/rv/series/inf/03"],
|
||||||
["write auth to r1", "./rvctl tokens write --repo PATH --remote r1 --server URL --token-name t1"],
|
["write auth to r1", "./rvctl tokens write --repo PATH --remote r1 --server URL"],
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2177,11 +2392,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
"add",
|
"add",
|
||||||
help="Add an empty token skeleton to tokens.json.",
|
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("token_id", metavar="REMOTE_ID", help="Remote id, for example 'r1' or 'r1a'.")
|
||||||
tokens_add_parser.add_argument("--server", help="Server endpoint. Default: git.base_url from workspace.json.")
|
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("--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("--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("--remote", help="Compatibility alias. If used, it must match REMOTE_ID.")
|
||||||
tokens_add_parser.add_argument("--org", help="Optional remote organization.")
|
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("--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_add_parser.add_argument("--dry-run", action="store_true", help="Print the planned token without writing it.")
|
||||||
@@ -2193,6 +2408,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
add_repo_option(tokens_stats_parser)
|
add_repo_option(tokens_stats_parser)
|
||||||
tokens_stats_parser.add_argument("--server", help="Filter output to one server endpoint.")
|
tokens_stats_parser.add_argument("--server", help="Filter output to one server endpoint.")
|
||||||
|
|
||||||
|
tokens_sync_parser = tokens_subparsers.add_parser(
|
||||||
|
"sync",
|
||||||
|
help="Synchronize one remote id between git remote URL and tokens.json.",
|
||||||
|
)
|
||||||
|
tokens_sync_parser.add_argument("direction", choices=["remote", "store"], help="'remote' reads git remote into store; 'store' writes store into git remote.")
|
||||||
|
tokens_sync_parser.add_argument("remote_id", help="Remote id, for example 'r1' or 'r1a'.")
|
||||||
|
add_repo_option(tokens_sync_parser)
|
||||||
|
tokens_sync_parser.add_argument("--url", help="Target remote URL used when direction is 'store' and the remote does not exist yet.")
|
||||||
|
add_store_selection_options(tokens_sync_parser)
|
||||||
|
tokens_sync_parser.add_argument("--replace", action="store_true", help="Replace different credentials when writing store -> remote.")
|
||||||
|
tokens_sync_parser.add_argument("--dry-run", action="store_true", help="Print the planned sync without writing it.")
|
||||||
|
|
||||||
tokens_write_parser = tokens_subparsers.add_parser(
|
tokens_write_parser = tokens_subparsers.add_parser(
|
||||||
"write",
|
"write",
|
||||||
help="Write credentials from tokens.json into a repo remote URL.",
|
help="Write credentials from tokens.json into a repo remote URL.",
|
||||||
@@ -2206,14 +2433,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
tokens_update_parser = tokens_subparsers.add_parser(
|
tokens_update_parser = tokens_subparsers.add_parser(
|
||||||
"update",
|
"update",
|
||||||
help="Synchronize token data in a selected direction.",
|
help="Refresh API metadata for one remote id, or use a legacy --from direction.",
|
||||||
)
|
)
|
||||||
|
tokens_update_parser.add_argument("remote_id", nargs="?", help="Remote id to refresh from API, for example 'r1'.")
|
||||||
tokens_update_parser.add_argument(
|
tokens_update_parser.add_argument(
|
||||||
"--from",
|
"--from",
|
||||||
dest="from_source",
|
dest="from_source",
|
||||||
required=True,
|
|
||||||
choices=["remotes", "store"],
|
choices=["remotes", "store"],
|
||||||
help="Synchronization direction source: 'remotes' or 'store'.",
|
help="Legacy synchronization direction source: 'remotes' or 'store'.",
|
||||||
)
|
)
|
||||||
add_repo_option(tokens_update_parser)
|
add_repo_option(tokens_update_parser)
|
||||||
tokens_update_parser.add_argument("--remote", help="Remote name used when --from store.")
|
tokens_update_parser.add_argument("--remote", help="Remote name used when --from store.")
|
||||||
@@ -2266,6 +2493,9 @@ def main() -> int:
|
|||||||
if args.tokens_command == "stats":
|
if args.tokens_command == "stats":
|
||||||
run_tokens_stats(config, args)
|
run_tokens_stats(config, args)
|
||||||
return 0
|
return 0
|
||||||
|
if args.tokens_command == "sync":
|
||||||
|
run_tokens_sync(config, args)
|
||||||
|
return 0
|
||||||
if args.tokens_command == "write":
|
if args.tokens_command == "write":
|
||||||
run_tokens_write(config, args)
|
run_tokens_write(config, args)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user