backend redesign
This commit is contained in:
374
backend.old/src/secrets_manager/cli.py
Normal file
374
backend.old/src/secrets_manager/cli.py
Normal file
@@ -0,0 +1,374 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Command-line interface for managing the encrypted secrets store.
|
||||
|
||||
Usage:
|
||||
python -m secrets.cli init # Initialize new secrets store
|
||||
python -m secrets.cli set KEY VALUE # Set a secret
|
||||
python -m secrets.cli get KEY # Get a secret
|
||||
python -m secrets.cli delete KEY # Delete a secret
|
||||
python -m secrets.cli list # List all secret keys
|
||||
python -m secrets.cli change-password # Change master password
|
||||
python -m secrets.cli export FILE # Export encrypted backup
|
||||
python -m secrets.cli import FILE # Import encrypted backup
|
||||
python -m secrets.cli migrate-from-env # Migrate secrets from .env file
|
||||
"""
|
||||
import sys
|
||||
import argparse
|
||||
import getpass
|
||||
from pathlib import Path
|
||||
|
||||
from .store import SecretsStore, SecretsStoreError, InvalidMasterPassword
|
||||
|
||||
|
||||
def get_password(prompt: str = "Master password: ", confirm: bool = False) -> str:
|
||||
"""
|
||||
Securely get password from user.
|
||||
|
||||
Args:
|
||||
prompt: Password prompt
|
||||
confirm: If True, ask for confirmation
|
||||
|
||||
Returns:
|
||||
Password string
|
||||
"""
|
||||
password = getpass.getpass(prompt)
|
||||
|
||||
if confirm:
|
||||
confirm_password = getpass.getpass("Confirm password: ")
|
||||
if password != confirm_password:
|
||||
print("Error: Passwords do not match", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return password
|
||||
|
||||
|
||||
def cmd_init(args):
|
||||
"""Initialize a new secrets store."""
|
||||
store = SecretsStore()
|
||||
|
||||
if store.is_initialized:
|
||||
print("Error: Secrets store is already initialized", file=sys.stderr)
|
||||
print(f"Location: {store.secrets_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password = get_password("Create master password: ", confirm=True)
|
||||
|
||||
if len(password) < 8:
|
||||
print("Error: Password must be at least 8 characters", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
store.initialize(password)
|
||||
print(f"Secrets store initialized at {store.secrets_file}")
|
||||
|
||||
|
||||
def cmd_set(args):
|
||||
"""Set a secret value."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password = get_password()
|
||||
|
||||
try:
|
||||
store.unlock(password)
|
||||
except InvalidMasterPassword:
|
||||
print("Error: Invalid master password", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
store.set(args.key, args.value)
|
||||
print(f"✓ Secret '{args.key}' saved")
|
||||
|
||||
|
||||
def cmd_get(args):
|
||||
"""Get a secret value."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password = get_password()
|
||||
|
||||
try:
|
||||
store.unlock(password)
|
||||
except InvalidMasterPassword:
|
||||
print("Error: Invalid master password", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
value = store.get(args.key)
|
||||
if value is None:
|
||||
print(f"Error: Secret '{args.key}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Print to stdout (can be captured)
|
||||
print(value)
|
||||
|
||||
|
||||
def cmd_delete(args):
|
||||
"""Delete a secret."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password = get_password()
|
||||
|
||||
try:
|
||||
store.unlock(password)
|
||||
except InvalidMasterPassword:
|
||||
print("Error: Invalid master password", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if store.delete(args.key):
|
||||
print(f"✓ Secret '{args.key}' deleted")
|
||||
else:
|
||||
print(f"Error: Secret '{args.key}' not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
"""List all secret keys."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password = get_password()
|
||||
|
||||
try:
|
||||
store.unlock(password)
|
||||
except InvalidMasterPassword:
|
||||
print("Error: Invalid master password", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
keys = store.list_keys()
|
||||
|
||||
if not keys:
|
||||
print("No secrets stored")
|
||||
else:
|
||||
print(f"Stored secrets ({len(keys)}):")
|
||||
for key in sorted(keys):
|
||||
# Show key and value length for verification
|
||||
value = store.get(key)
|
||||
value_str = str(value)
|
||||
value_preview = value_str[:50] + "..." if len(value_str) > 50 else value_str
|
||||
print(f" {key}: {value_preview}")
|
||||
|
||||
|
||||
def cmd_change_password(args):
|
||||
"""Change the master password."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
current_password = get_password("Current master password: ")
|
||||
new_password = get_password("New master password: ", confirm=True)
|
||||
|
||||
if len(new_password) < 8:
|
||||
print("Error: Password must be at least 8 characters", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
store.change_master_password(current_password, new_password)
|
||||
except InvalidMasterPassword:
|
||||
print("Error: Invalid current password", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_export(args):
|
||||
"""Export encrypted secrets to a backup file."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
output_path = Path(args.file)
|
||||
|
||||
if output_path.exists() and not args.force:
|
||||
print(f"Error: File {output_path} already exists. Use --force to overwrite.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
store.export_encrypted(output_path)
|
||||
except SecretsStoreError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_import(args):
|
||||
"""Import encrypted secrets from a backup file."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
input_path = Path(args.file)
|
||||
|
||||
if not input_path.exists():
|
||||
print(f"Error: File {input_path} does not exist", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password = get_password()
|
||||
|
||||
try:
|
||||
store.import_encrypted(input_path, password)
|
||||
except InvalidMasterPassword:
|
||||
print("Error: Invalid master password or incompatible backup", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except SecretsStoreError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_migrate_from_env(args):
|
||||
"""Migrate secrets from .env file to encrypted store."""
|
||||
store = SecretsStore()
|
||||
|
||||
if not store.is_initialized:
|
||||
print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Look for .env file
|
||||
backend_root = Path(__file__).parent.parent.parent
|
||||
env_file = backend_root / ".env"
|
||||
|
||||
if not env_file.exists():
|
||||
print(f"Error: .env file not found at {env_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
password = get_password()
|
||||
|
||||
try:
|
||||
store.unlock(password)
|
||||
except InvalidMasterPassword:
|
||||
print("Error: Invalid master password", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse .env file (simple parser - doesn't handle all edge cases)
|
||||
migrated = 0
|
||||
skipped = 0
|
||||
|
||||
with open(env_file) as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
# Parse KEY=VALUE format
|
||||
if '=' not in line:
|
||||
print(f"Warning: Skipping invalid line {line_num}: {line}", file=sys.stderr)
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Remove quotes if present
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
elif value.startswith("'") and value.endswith("'"):
|
||||
value = value[1:-1]
|
||||
|
||||
# Check if key already exists
|
||||
existing = store.get(key)
|
||||
if existing is not None:
|
||||
print(f"Warning: Secret '{key}' already exists, skipping", file=sys.stderr)
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
store.set(key, value)
|
||||
print(f"✓ Migrated: {key}")
|
||||
migrated += 1
|
||||
|
||||
print(f"\nMigration complete: {migrated} secrets migrated, {skipped} skipped")
|
||||
|
||||
if not args.keep_env:
|
||||
# Ask for confirmation before deleting .env
|
||||
confirm = input(f"\nDelete {env_file}? [y/N]: ").strip().lower()
|
||||
if confirm == 'y':
|
||||
env_file.unlink()
|
||||
print(f"✓ Deleted {env_file}")
|
||||
else:
|
||||
print(f"Kept {env_file} (consider deleting it manually)")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Manage encrypted secrets store",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Command to run')
|
||||
subparsers.required = True
|
||||
|
||||
# init
|
||||
parser_init = subparsers.add_parser('init', help='Initialize new secrets store')
|
||||
parser_init.set_defaults(func=cmd_init)
|
||||
|
||||
# set
|
||||
parser_set = subparsers.add_parser('set', help='Set a secret value')
|
||||
parser_set.add_argument('key', help='Secret key name')
|
||||
parser_set.add_argument('value', help='Secret value')
|
||||
parser_set.set_defaults(func=cmd_set)
|
||||
|
||||
# get
|
||||
parser_get = subparsers.add_parser('get', help='Get a secret value')
|
||||
parser_get.add_argument('key', help='Secret key name')
|
||||
parser_get.set_defaults(func=cmd_get)
|
||||
|
||||
# delete
|
||||
parser_delete = subparsers.add_parser('delete', help='Delete a secret')
|
||||
parser_delete.add_argument('key', help='Secret key name')
|
||||
parser_delete.set_defaults(func=cmd_delete)
|
||||
|
||||
# list
|
||||
parser_list = subparsers.add_parser('list', help='List all secret keys')
|
||||
parser_list.set_defaults(func=cmd_list)
|
||||
|
||||
# change-password
|
||||
parser_change = subparsers.add_parser('change-password', help='Change master password')
|
||||
parser_change.set_defaults(func=cmd_change_password)
|
||||
|
||||
# export
|
||||
parser_export = subparsers.add_parser('export', help='Export encrypted backup')
|
||||
parser_export.add_argument('file', help='Output file path')
|
||||
parser_export.add_argument('--force', action='store_true', help='Overwrite existing file')
|
||||
parser_export.set_defaults(func=cmd_export)
|
||||
|
||||
# import
|
||||
parser_import = subparsers.add_parser('import', help='Import encrypted backup')
|
||||
parser_import.add_argument('file', help='Input file path')
|
||||
parser_import.set_defaults(func=cmd_import)
|
||||
|
||||
# migrate-from-env
|
||||
parser_migrate = subparsers.add_parser('migrate-from-env', help='Migrate from .env file')
|
||||
parser_migrate.add_argument('--keep-env', action='store_true', help='Keep .env file after migration')
|
||||
parser_migrate.set_defaults(func=cmd_migrate_from_env)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
args.func(args)
|
||||
except KeyboardInterrupt:
|
||||
print("\nAborted", file=sys.stderr)
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user