-
Notifications
You must be signed in to change notification settings - Fork 1
fix: OAuth2 token encoding and /auth/info route #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
4a882b5
2185149
f1547b1
526553d
343b88f
0831ab7
9e3d19c
3306569
5fd3be4
0d667c8
1c741c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,9 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import contextlib | ||
| import os | ||
| import re | ||
| import secrets | ||
| import string | ||
| from pathlib import Path | ||
|
|
@@ -176,3 +178,83 @@ def rotate_env_var( | |
| audit.log("rotate", key, env_file=str(env_file)) | ||
|
|
||
| return True, new_value | ||
|
|
||
|
|
||
| def _atomic_write(path: Path, content: str) -> None: | ||
| """Write *content* to *path* atomically (temp file + os.replace). | ||
|
|
||
| A crash mid-write must never leave a truncated or half-rotated .env file. | ||
| """ | ||
| tmp = path.with_name(f".{path.name}.rotate-tmp-{os.getpid()}") | ||
| try: | ||
| with open(tmp, "w") as f: | ||
| f.write(content) | ||
| f.flush() | ||
| os.fsync(f.fileno()) | ||
| os.replace(tmp, path) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the configured environment file is a symlink, the preceding read follows the link but Useful? React with 👍 / 👎. |
||
| except BaseException: | ||
| with contextlib.suppress(OSError): | ||
| os.unlink(tmp) | ||
| raise | ||
|
|
||
|
|
||
| def rotate_env_file( | ||
| env_file: str | Path, | ||
| *, | ||
| length: int = 32, | ||
| exclude: set[str] | None = None, | ||
| dry_run: bool = False, | ||
| audit: AuditLogger | None = None, | ||
| ) -> dict[str, str]: | ||
| """Rotate every variable in a .env file with ONE atomic rewrite. | ||
|
|
||
| Args: | ||
| env_file: Path to the .env file. | ||
| length: Length of generated secrets. | ||
| exclude: Keys to leave untouched. | ||
| dry_run: If True, don't modify the file. | ||
| audit: Optional audit logger (one entry per rotated key). | ||
|
|
||
| Returns: | ||
| Mapping of key -> new value for every rotated key. | ||
| """ | ||
| from dotenv import dotenv_values | ||
|
|
||
| env_file = Path(env_file) | ||
| exclude = exclude or set() | ||
| env_vars = dotenv_values(env_file) | ||
|
|
||
| plan: dict[str, str] = {} | ||
| for key, value in env_vars.items(): | ||
| if key in exclude or value is None: | ||
| continue | ||
| plan[key] = rotate_value(key, value, length=length) | ||
|
|
||
| if dry_run or not plan: | ||
| return plan | ||
|
|
||
| lines = env_file.read_text().split("\n") | ||
| seen: set[str] = set() | ||
| out_lines: list[str] = [] | ||
| key_line = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=") | ||
| for line in lines: | ||
| m = key_line.match(line) | ||
| if m and m.group(1) in plan and m.group(1) not in seen: | ||
| key = m.group(1) | ||
| seen.add(key) | ||
|
Comment on lines
+242
to
+244
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a file containing the same key more than once, such as Useful? React with 👍 / 👎. |
||
| new_value = plan[key] | ||
| if any(c in new_value for c in " #'\"\n\t"): | ||
| safe = new_value.replace("\\", "\\\\").replace('"', '\\"') | ||
| out_lines.append(f'{key}="{safe}"') | ||
| else: | ||
| out_lines.append(f"{key}={new_value}") | ||
| else: | ||
| out_lines.append(line) | ||
|
|
||
| _atomic_write(env_file, "\n".join(out_lines)) | ||
|
|
||
| if audit: | ||
| for key in plan: | ||
| audit.log("rotate", key, env_file=str(env_file)) | ||
|
|
||
| return plan | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the source
.envis secured with mode 0600 and the process has a typical 0022 umask, opening this new temporary file creates it as 0644;os.replace()then installs those permissions on the rotated.env. A successfulrotate-alltherefore makes every newly generated secret readable by other local users, so copy the original file mode to the temporary file before replacing it.Useful? React with 👍 / 👎.