diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-16 00:00:45 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-16 00:00:45 -0500 |
| commit | 41dd030cd6414f0acf60a32dcc4de415966601d0 (patch) | |
| tree | e2756726bb8cc59124a2051486b017b730f734ba | |
| parent | 546bcf9bbe651eaaa0129346536c9e7e98748555 (diff) | |
| download | pearl-41dd030cd6414f0acf60a32dcc4de415966601d0.tar.gz pearl-41dd030cd6414f0acf60a32dcc4de415966601d0.zip | |
chore(scripts): warn when apikey.txt is group/other-readable
The seed script reads the Linear key from the environment, but the README sources it from a gitignored `apikey.txt` that was landing at mode 644. `main` now warns on startup when that file is group- or other-accessible, so a loose key file gets flagged instead of sitting silently exposed. The warning points at `chmod 600`.
| -rw-r--r-- | scripts/seed_pearl_workspace.py | 20 | ||||
| -rw-r--r-- | scripts/tests/test_seed_pearl_workspace.py | 22 |
2 files changed, 42 insertions, 0 deletions
diff --git a/scripts/seed_pearl_workspace.py b/scripts/seed_pearl_workspace.py index bc5300a..97079b6 100644 --- a/scripts/seed_pearl_workspace.py +++ b/scripts/seed_pearl_workspace.py @@ -41,6 +41,7 @@ Usage: import argparse import json import os +import stat import sys import urllib.request @@ -363,12 +364,31 @@ def seed(client): "default_issue_state": INTAKE_STATE if default_set else "unchanged"} +def warn_if_key_file_world_readable(path, stream=None): + """Warn when PATH exists and is readable by group or others. + + The key file holds a Linear API key, so group/other access is a leak risk. + Returns True when a warning was emitted, False otherwise (absent or 600).""" + if stream is None: + stream = sys.stderr + try: + mode = os.stat(path).st_mode + except OSError: + return False + if mode & (stat.S_IRWXG | stat.S_IRWXO): + print(f"warning: {path} is group/other-accessible; chmod 600 it " + "(it holds your Linear API key)", file=stream) + return True + return False + + def main(argv=None): parser = argparse.ArgumentParser(description="Seed a Pearl Linear workspace.") parser.add_argument("--dry-run", action="store_true", help="print the reconcile plan without changing anything") args = parser.parse_args(argv) + warn_if_key_file_world_readable("apikey.txt") api_key = os.environ.get("LINEAR_API_KEY") if not api_key: print("LINEAR_API_KEY is not set", file=sys.stderr) diff --git a/scripts/tests/test_seed_pearl_workspace.py b/scripts/tests/test_seed_pearl_workspace.py index 5f4147e..e52d78a 100644 --- a/scripts/tests/test_seed_pearl_workspace.py +++ b/scripts/tests/test_seed_pearl_workspace.py @@ -296,3 +296,25 @@ def test_client_raises_on_graphql_errors(): client = seed.LinearClient("key", transport=fake) with pytest.raises(seed.LinearError, match="bad key"): client.execute("query Teams { teams { nodes { id } } }", {}) + + +# --- key-file permission warning ---------------------------------------------- + +def test_warn_key_file_world_readable_warns(tmp_path, capsys): + p = tmp_path / "apikey.txt" + p.write_text("lin_api_secret") + os.chmod(p, 0o644) + assert seed.warn_if_key_file_world_readable(str(p)) is True + assert "600" in capsys.readouterr().err + + +def test_warn_key_file_mode_600_is_silent(tmp_path, capsys): + p = tmp_path / "apikey.txt" + p.write_text("lin_api_secret") + os.chmod(p, 0o600) + assert seed.warn_if_key_file_world_readable(str(p)) is False + assert capsys.readouterr().err == "" + + +def test_warn_key_file_absent_is_silent(tmp_path): + assert seed.warn_if_key_file_world_readable(str(tmp_path / "nope.txt")) is False |
