[SECURITY] Credential rotation scheduler + API permission validation - closes #58 #59#80
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds credential rotation scheduling, metadata tracking, and API permission validation to the credential management subsystem, plus unit tests.
Changes:
- Introduces
CredentialMetadata,PermissionAuditResult,PermissionValidator, andCredentialRotationScheduleralong with rotation/backup logic inSecureCredentialManager. - Adds
validate_credentials_on_startupconvenience entry point and structured logging throughout. - Adds unit tests covering metadata, encryption roundtrip, rotation, permission validation, and scheduler lifecycle.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| app/pt_credentials.py | New rotation/permission classes + refactored encrypt/decrypt/migrate flow. |
| app/test_credentials_rotation.py | New tests for metadata, manager, validator, and scheduler. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hello @Ibrahim-3d, thank you for your great efforts here. I can see what you are striving to achieve, I would like to pull this branch and test it, just no time lately. Can I ask you to review the copilot comments here #80 (review) and respond to each with either justification to resolve the dispute, or counter/dismiss it with a valid reason. I’ll try and get some time towards the end of the week to look at your copy of this code. Looks comprehensive from my initial view. |
|
All 14 Copilot review comments addressed in commit
|
Local CI Simulation ResultsRan all workflow steps locally against this branch prior to owner approval. Results: Code Quality & Testing
Unit Test Results
CI/CD Pipeline
|
Extends SecureCredentialManager with rotation status tracking and graceful rotation with backup/restore on failure. Adds CredentialRotationScheduler daemon thread for automated rotation warnings. Adds PermissionValidator with JSONL audit trail and validate_credentials_on_startup() helper. - 22 unit tests, all passing
…-platform fixes - Atomic writes via temp->rename prevent mismatched key/secret on partial failure - rotate_credentials now backs up and restores metadata alongside ciphertext - rotation_interval_days updated in existing metadata on re-encrypt - _get_machine_password uses socket.gethostname()+getpass.getuser() (cross-platform) - CredentialRotationScheduler deduplicates warnings (only fires on message change) - Minimum scheduler interval enforced (60s) to prevent tight-loop misconfiguration - PermissionValidator audit log has MAX_AUDIT_LINES cap + secure file permissions - get_credentials restores plaintext fallback to prevent user lockout on vault failure - _load_metadata catches TypeError for corrupt/partial JSON - shutil moved to module-level import - timedelta import removed (unused) - Tests: shutil at top, proper tearDown in all classes, dedup and overdue callback tests
296124a to
d999b4b
Compare
|
@Ibrahim-3d we have another large series of copilot reviewer comments. Any chance you can read and respond to them? #80 (review) |
- rotate_credentials rollback: replace shutil.copy2 with os.replace for atomic restore (POSIX rename, no partial-restore window on interruption) - rotate_credentials: snapshot created_at before calling encrypt_credentials; restore it if encrypt_credentials constructed fresh metadata (which resets created_at when metadata file is missing or corrupt at rotation time)
Manual Testing Guide — PR #80: Credential Rotation + Permission ValidationPrerequisitesgit fetch fork && git checkout feat/58-59-credential-rotation-permission-validation
cd app
pip install cryptography1. Run unit testspython -m pytest test_credentials_rotation.py -vExpected: All 30 tests pass. 2. Smoke test — full rotation with rollback protectionpython -c "
import tempfile
from pt_credentials import SecureCredentialManager
with tempfile.TemporaryDirectory() as d:
m = SecureCredentialManager(d)
# Initial encrypt
m.encrypt_credentials('key_v1', 'secret_v1')
print('v1 encrypted:', m.decrypt_credentials())
# Rotate to v2
ok = m.rotate_credentials('key_v2', 'secret_v2')
print('rotation ok:', ok)
print('v2 decrypts:', m.decrypt_credentials())
# Verify created_at preserved
meta1 = m._load_metadata()
m.rotate_credentials('key_v3', 'secret_v3')
meta2 = m._load_metadata()
assert meta1.created_at == meta2.created_at, 'created_at changed!'
print('PASS: created_at preserved across rotation')
"3. Verify atomic rollback (key change)python -c "
import tempfile, unittest.mock
from pt_credentials import SecureCredentialManager
with tempfile.TemporaryDirectory() as d:
m = SecureCredentialManager(d)
m.encrypt_credentials('orig_key', 'orig_secret')
# Force encrypt_credentials to fail mid-way
with unittest.mock.patch.object(m, 'encrypt_credentials', return_value=False):
result = m.rotate_credentials('new_key', 'new_secret')
print('rotation result (should be False):', result)
# Original creds must still be intact
creds = m.decrypt_credentials()
print('creds after failed rotation:', creds)
assert creds == ('orig_key', 'orig_secret'), f'FAIL: got {creds}'
print('PASS: rollback preserved original credentials')
"4. Verify rotation warning fires once (not every tick)python -c "
import tempfile, time
from pt_credentials import SecureCredentialManager, CredentialRotationScheduler
fires = []
with tempfile.TemporaryDirectory() as d:
m = SecureCredentialManager(d)
m.encrypt_credentials('k', 's', rotation_interval_days=0) # immediately overdue
sched = CredentialRotationScheduler(m, check_interval_hours=0.0001,
callback=lambda w: fires.append(w))
sched.start()
time.sleep(0.1)
sched.stop()
print('callback fired', len(fires), 'time(s)')
# Should fire once (warning text unchanged = no re-fire)
assert len(fires) >= 1, 'callback never fired'
print('PASS')
"Rollbackgit checkout main -- app/pt_credentials.py app/test_credentials_rotation.py |
- from_dict: raise ValueError on missing required fields (clearer than TypeError) - atomic writes use tempfile.mkstemp to avoid `.tmp` name collisions - decrypt: legacy COMPUTERNAME/USERNAME derivation fallback + auto re-encrypt - get_rotation_status: consistent dict shape (rotation_due_at always present) - audit log: O(1) size-based rotation to .1 instead of per-call O(n) rewrite - PermissionValidator: documented default base_dir caveat - scheduler: extracted _tick() so tests exercise real dedup; clamp warning; stop() reports if join times out - get_credentials: plaintext fallback logged at error level with explicit warning - validate_credentials_on_startup: accepts base_dir for testability - tests: addCleanup for tmpdirs, audit log permission assertion, metadata rollback assertions, real _tick() in dedup tests
|
All 19 round-2 Copilot threads addressed in commit Highlights:
Ready for re-review. |
|
@sjackson0109 all 19 round-2 Copilot threads addressed in cd45203, replied inline, resolved. 32 tests pass. Ready for re-review. |
|
@Ibrahim-3d This PR review #80 (review) has left us with 4x open comments/concerns of varying risk-levels. Can you review and repy to each (inline, or all at once). |
- encrypt_credentials: two-phase commit for key/secret ciphertexts. Both ciphertexts are now staged to temp files before either rename happens. If the second rename fails after the first commits, the previously-saved key ciphertext is restored from an in-memory snapshot, so the vault never ends up with a new key paired with the old secret. - _stage_temp_binary: new helper that writes a temp file and returns its path without renaming. _atomic_write_binary refactored to use it. - PermissionValidator.validate: missing_required / missing_trading lists now sorted() for stable, diff-friendly audit log entries. - _rotate_audit_if_needed: AUDIT_ROTATION_KEEP is now actually wired in. Older generations shift down (.N-1 -> .N) and anything past the keep window is dropped, instead of the previous single-.1 rotation. - CredentialRotationScheduler._tick: update _last_warning BEFORE calling the user callback so a broken callback cannot re-fire the same warning on every subsequent tick. Callback exceptions are caught and logged with traceback, isolated from the scheduler's dedup state. - test_rotate_restores_metadata_on_failure: patch _stage_temp_binary (new internal seam) instead of _atomic_write_binary.
|
@sjackson0109 round-3 done — addressed in 4c5c927:
32 unit tests pass locally. Threads resolved. |
|
Hey - please hold off on merging this one for a moment. I want to add a round-trip migration test for the new vault derivation before this lands, since changing how credentials are scrambled could lock users out if we get it wrong. Will follow up here as soon as the test is in. |
Adds regression test for the legacy-derivation fallback added in this PR. Encrypts a vault under the legacy COMPUTERNAME/USERNAME password, upgrades to the new gethostname()/getuser() derivation, and asserts that decrypt_credentials() transparently: 1. Falls back to the legacy derivation and returns the original creds. 2. Auto-rewrites the vault under the new derivation in the same call. 3. Subsequent decrypt with only the new derivation available (legacy fallback returning None) still succeeds — proving the rewrite happened on disk, not just in memory. Closes the migration-risk review feedback on this PR.
|
Test is in. The new test (test_legacy_vault_auto_migrates_to_new_derivation) walks the whole upgrade path: encrypt a vault with the old Windows-only password, upgrade to the new cross-platform derivation, decrypt successfully via the legacy fallback, and confirm the vault gets quietly rewritten under the new key on disk so the next decrypt does not need the fallback. 33 tests passing. Good to review when you have a minute. |
|
@copilot review |
sjackson0109
left a comment
There was a problem hiding this comment.
Really happy to see this change go in...
…er tick callback)
- pt_credentials.py: legacy derivation migration in decrypt_credentials()
now snapshots the existing rotation metadata before calling
encrypt_credentials() and restores last_rotated_at / rotation_due_at /
created_at / rotation_interval_days afterwards. Without this, a derivation
upgrade silently looked like a real credential rotation and pushed the
next rotation warning out by a full interval, defeating the scheduler for
any vault that triggered the fallback path.
- test_credentials_rotation.py:
* test_callback_fires_when_overdue now also exercises the real _tick()
dispatch path and asserts the callback is invoked with the OVERDUE
warning, so a regression in tick→callback wiring would actually fail
the test (matching what the docstring already promised).
* New test_legacy_migration_preserves_rotation_metadata pins the
fix above: backdated metadata must survive the derivation upgrade.
Summary
CredentialRotationSchedulerdaemon thread that checks rotation status every N hours and fires a notification callback (GUI alert, log, etc.)PermissionValidatorthat validates exchange API permissions on startup against required/trading permission sets, with full JSONL audit trailvalidate_credentials_on_startup()convenience function handles both checks in one callChanges
app/pt_credentials.py—CredentialMetadata, rotation methods,PermissionValidator,CredentialRotationScheduler,validate_credentials_on_startup()app/test_credentials_rotation.py— 22 tests covering all scenariosUsage
Test plan
Closes #58
Closes #59