-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrotate_tokens.py
More file actions
138 lines (121 loc) · 3.92 KB
/
rotate_tokens.py
File metadata and controls
138 lines (121 loc) · 3.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
"""Interactive token rotation.
For each platform, walks a small wizard:
1. Open the token-management page in the user's browser.
2. Prompt for the new token (silent input).
3. Store via OS keyring.
4. Re-run a doctor probe to confirm the token works.
This module is the library entry point; the CLI provides the
prompt UI via Typer.
"""
from __future__ import annotations
from dataclasses import dataclass
from ..core.logging import get_logger
from ..core.secrets import set_keyring
_log = get_logger(__name__)
@dataclass(frozen=True)
class RotationStep:
"""Per-platform rotation guidance: URL + the keyring slot to set."""
platform: str
token_management_url: str
keyring_key: str
env_var: str
notes: str = ""
# Static table; could move to a JSON resource if the list grows.
ROTATION_TABLE: dict[str, RotationStep] = {
"pypi": RotationStep(
"PyPI",
"https://pypi.org/manage/account/token/",
"pypi",
"PYPI_TOKEN",
"Prefer per-project tokens. Rotate annually.",
),
"npm": RotationStep(
"npm",
"https://www.npmjs.com/settings/your-username/tokens",
"npm",
"NPM_TOKEN",
"Use Automation tokens for CI (bypasses 2FA OTP).",
),
"dockerhub": RotationStep(
"Docker Hub",
"https://hub.docker.com/settings/security",
"dockerhub",
"DOCKERHUB_TOKEN",
"Per-namespace access token, Read+Write+Delete.",
),
"ghcr": RotationStep(
"GHCR",
"https://github.com/settings/tokens?type=beta",
"ghcr",
"GHCR_TOKEN",
"Use fine-grained PAT with Packages: Read & write.",
),
"github": RotationStep(
"GitHub.com",
"https://github.com/settings/tokens?type=beta",
"github",
"GITHUB_TOKEN",
"Use fine-grained PAT, scoped per-repo.",
),
"gitlab": RotationStep(
"GitLab.com",
"https://gitlab.com/-/user_settings/personal_access_tokens",
"gitlab",
"GITLAB_TOKEN",
"Project / Group Access Tokens preferred over PAT.",
),
"rubygems": RotationStep(
"RubyGems",
"https://rubygems.org/profile/api_keys",
"rubygems",
"RUBYGEMS_API_KEY",
"Per-gem scope landed in 2023; use it.",
),
"cargo": RotationStep(
"crates.io",
"https://crates.io/settings/tokens",
"cargo",
"CARGO_REGISTRY_TOKEN",
"Per-crate scopes landed in 2023.",
),
"nuget": RotationStep(
"NuGet.org",
"https://www.nuget.org/account/apikeys",
"nuget",
"NUGET_API_KEY",
"Use glob-pattern scope to limit blast radius.",
),
"packagist": RotationStep(
"Packagist",
"https://packagist.org/profile/",
"packagist",
"PACKAGIST_TOKEN",
"Account-level; rotate annually.",
),
"homebrew": RotationStep(
"Homebrew tap",
"https://github.com/settings/tokens?type=beta",
"homebrew",
"TAP_GITHUB_TOKEN",
"Fine-grained PAT scoped to the tap repo.",
),
}
def get_rotation_step(platform: str) -> RotationStep:
"""Look up the rotation guidance for a platform slug."""
if platform not in ROTATION_TABLE:
raise KeyError(
f"no rotation guidance for {platform!r}; " f"known: {sorted(ROTATION_TABLE)}"
)
return ROTATION_TABLE[platform]
def apply_rotation(platform: str, new_token: str) -> None:
"""
Persist the new token to the OS keyring.
Called by the CLI after the user has pasted the token. Doesn't
touch the env var or `.env`; the keyring is the canonical place
so the CLI's resolution chain finds it on the next run.
@param platform slug.
@param new_token the token value (already validated by the caller).
"""
step = get_rotation_step(platform)
set_keyring(step.keyring_key, new_token)
_log.info("rotated", platform=platform, keyring_key=step.keyring_key)