From 074dbeab8df120f53afe32a888c8fab2f8353b35 Mon Sep 17 00:00:00 2001 From: John Ajera <37360952+jajera@users.noreply.github.com> Date: Fri, 9 Jan 2026 01:52:43 +0000 Subject: [PATCH] feat: add bitwarden get secret adds support to get bitwarden secret --- .gitignore | 45 +++++ README.md | 2 + docs/bwsm_secret.md | 275 +++++++++++++++++++++++++++ scripts/bwsm_secret.py | 418 +++++++++++++++++++++++++++++++++++++++++ scripts/bwsm_secret.sh | 154 +++++++++++++++ 5 files changed, 894 insertions(+) create mode 100644 .gitignore create mode 100644 docs/bwsm_secret.md create mode 100755 scripts/bwsm_secret.py create mode 100755 scripts/bwsm_secret.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43667c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Temporary files +*.tmp +*.log +*.bak +*.cache + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index a490925..d1b4438 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Command-line interface tools/scripts - [install_aws_cli.sh](docs/install_aws_cli.md) - [install_aws_vault.sh](docs/install_aws_vault.md) +- [bwsm_secret.sh](docs/bwsm_secret.md) - [install_cloudwatch_agent.sh](docs/install_cloudwatch_agent.md) - [install_codedeploy_agent.sh](docs/install_codedeploy_agent.md) - [install_grafana.sh](docs/install_grafana.md) @@ -37,6 +38,7 @@ Refer to the respective files for detailed usage instructions: - **[install_aws_cli.sh](docs/install_aws_cli.md)**: Install or uninstall the AWS CLI on supported Linux distributions. - **[install_aws_vault.sh](docs/install_aws_vault.md)**: Manage AWS Vault installation and configuration. +- **[bwsm_secret.sh](docs/bwsm_secret.md)**: Manage secrets in Bitwarden Secrets Manager (get, create, update, delete, list) with automatic prerequisite handling. - **[install_cloudwatch_agent.sh](docs/install_cloudwatch_agent.md)**: Install or uninstall the AWS CloudWatch Unified Agent. - **[install_codedeploy_agent.sh](docs/install_codedeploy_agent.md)**: Install or uninstall the AWS CodeDeploy agent. - **[install_grafana.sh](docs/install_grafana.md)**: Install or uninstall Grafana web-based analytics and monitoring platform. diff --git a/docs/bwsm_secret.md b/docs/bwsm_secret.md new file mode 100644 index 0000000..8a1dba2 --- /dev/null +++ b/docs/bwsm_secret.md @@ -0,0 +1,275 @@ +# bwsm_secret.sh + +This script manages secrets in Bitwarden Secrets Manager using the Bitwarden SDK. It handles prerequisite installation (Python and bitwarden_sdk) and works both locally and when executed from a GitHub URL. + +## Usage + +```bash +./bwsm_secret.sh [subcommand] [options] +``` + +### Subcommands + +- `get` - Get a secret value (default if no subcommand provided) +- `create` - Create a new secret (coming soon) +- `update` - Update an existing secret (coming soon) +- `delete` - Delete secret(s) (coming soon) +- `list` - List all secrets (coming soon) + +**Note**: For backward compatibility, if no subcommand is provided, `get` is assumed. + +## Running Without Cloning + +```bash +bash <(curl -s https://raw.githubusercontent.com/jdevto/cli-tools/main/scripts/bwsm_secret.sh) get --secret-id --access-token "$BWS_ACCESS_TOKEN" +``` + +Or without explicit subcommand (defaults to `get`): + +```bash +bash <(curl -s https://raw.githubusercontent.com/jdevto/cli-tools/main/scripts/bwsm_secret.sh) --secret-id --access-token "$BWS_ACCESS_TOKEN" +``` + +## Get Subcommand + +The `get` subcommand retrieves a secret value from Bitwarden Secrets Manager. + +### Configuration Sources + +The script supports multiple configuration sources (checked in priority order): + +#### 1. Command-Line Arguments (Highest Priority) + +```bash +./bwsm_secret.sh get --secret-id --access-token "$BWS_ACCESS_TOKEN" +``` + +#### 2. Environment Variables + +```bash +export BWS_ACCESS_TOKEN="..." +export BWS_SECRET_ID="..." +export BWS_ORG_ID="..." # Optional, for organization-level secrets (or use BW_ORGANIZATION_ID) +./bwsm_secret.sh get +``` + +**Note**: The script supports both `BWS_ORG_ID` (standard) and `BW_ORGANIZATION_ID` (legacy) for organization ID. + +### Options (Get Subcommand) + +- `--secret-id `: Secret ID (UUID format required) to fetch +- `--secret-name `: Secret name/key to fetch (requires `--org-id`) +- `--access-token `: Bitwarden Secrets Manager access token (prefer env var `BWS_ACCESS_TOKEN`) +- `--org-id `: Organization ID (UUID) - required when using `--secret-name`, optional otherwise (prefer env var `BWS_ORG_ID` or `BW_ORGANIZATION_ID`) +- `--json`: Print JSON output (includes secret_id/secret_name, source, value) +- `--debug`: Print debug logs to stderr + +**Note**: Use `--secret-id` for UUIDs and `--secret-name` for names. `--secret-id` must be a valid UUID format. + +### Example Usage (Get) + +#### Using Secret ID (UUID) + +```bash +./bwsm_secret.sh get --secret-id "123e4567-e89b-12d3-a456-426614174000" --access-token "$BWS_ACCESS_TOKEN" +``` + +Or without explicit subcommand (backward compatible): + +```bash +./bwsm_secret.sh --secret-id "123e4567-e89b-12d3-a456-426614174000" --access-token "$BWS_ACCESS_TOKEN" +``` + +#### Using Secret Name + +```bash +./bwsm_secret.sh get --secret-name "bw-example-secret" --access-token "$BWS_ACCESS_TOKEN" --org-id "$BWS_ORG_ID" +``` + +#### Using Environment Variables + +```bash +export BWS_ACCESS_TOKEN="your-access-token" +export BWS_SECRET_ID="123e4567-e89b-12d3-a456-426614174000" +export BWS_ORG_ID="123e4567-e89b-12d3-a456-426614174000" # Optional +./bwsm_secret.sh get +``` + +**Note**: Organization ID is optional and is only used for reference/debugging. The organization is scoped via the access token. + +#### JSON Output + +```bash +./bwsm_secret.sh get --secret-id --access-token "$BWS_ACCESS_TOKEN" --json +``` + +Output: + +```json +{"secret_id":"123e4567-e89b-12d3-a456-426614174000","source":"cli","value":"secret-value"} +``` + +#### Debug Mode + +```bash +./bwsm_secret.sh get --secret-id --access-token "$BWS_ACCESS_TOKEN" --debug +``` + +## Direct Python Usage + +You can also run the Python script directly: + +```bash +python3 scripts/bwsm_secret.py get --secret-id --access-token "$BWS_ACCESS_TOKEN" +``` + +## Prerequisites + +The bash wrapper automatically handles prerequisites: + +- **Python 3**: Automatically installed via `install_python.sh` if not found +- **bitwarden_sdk**: Automatically installed via pip if not found + +### Manual Installation + +If automatic installation fails, install manually: + +```bash +# Install Python (if needed) +./scripts/install_python.sh install + +# Install bitwarden_sdk +python3 -m pip install bitwarden-sdk +``` + +## Output + +By default, the `get` subcommand prints **only the secret value** to stdout (suitable for piping): + +```bash +SECRET=$(./bwsm_secret.sh get --secret-id --access-token "$BWS_ACCESS_TOKEN") +echo "Secret: $SECRET" +``` + +Use `--json` for structured output with metadata. + +## Exit Codes + +- `0`: Success +- `2`: Configuration/usage error (missing credentials, invalid subcommand) +- `3`: Authentication error +- `4`: Secret not found +- `5`: SDK/runtime error + +## Error Handling + +### Missing Credentials + +```bash +$ ./bwsm_secret.sh get +Config error: missing Bitwarden credentials. +Provide both access token and secret id via one of: + - CLI: --access-token ... --secret-id ... + - Env: BWS_ACCESS_TOKEN and BWS_SECRET_ID +``` + +### Invalid Subcommand + +```bash +$ ./bwsm_secret.sh invalid +Error: Invalid subcommand 'invalid'. +Valid subcommands: get, create, update, delete, list +``` + +### Authentication Error + +```bash +$ ./bwsm_secret.sh get --secret-id --access-token "invalid" +Error: Authentication failed. Invalid access token +``` + +### Secret Not Found + +```bash +$ ./bwsm_secret.sh get --secret-id "invalid-uuid" --access-token "$BWS_ACCESS_TOKEN" +Error: Secret fetch failed. Secret not found +``` + +## Troubleshooting + +### Python Not Found + +If Python installation fails: + +```bash +# Check if Python is installed +python3 --version + +# Manually install Python +./scripts/install_python.sh install + +# Verify installation +which python3 +``` + +### bitwarden_sdk Installation Fails + +If bitwarden_sdk installation fails: + +```bash +# Try manual installation +python3 -m pip install bitwarden-sdk + +# Or with pip3 +pip3 install bitwarden-sdk + +# Verify installation +python3 -c "import bitwarden_sdk; print('OK')" +``` + +### Script Not Found When Running from URL + +If running from GitHub URL and Python script download fails: + +- Check internet connectivity +- Verify GitHub URL is accessible: `curl -I https://raw.githubusercontent.com/jdevto/cli-tools/main/scripts/bwsm_secret.py` +- Try running locally instead + +### Permission Denied + +Ensure scripts are executable: + +```bash +chmod +x scripts/bwsm_secret.sh +chmod +x scripts/bwsm_secret.py +``` + +## URL Execution + +The script detects when it's being run from a URL and automatically downloads the Python script: + +```bash +bash <(curl -s https://raw.githubusercontent.com/jdevto/cli-tools/main/scripts/bwsm_secret.sh) get --secret-id --access-token "$BWS_ACCESS_TOKEN" +``` + +When executed from URL: + +- Python script is downloaded to `/tmp/bwsm_secret.py` +- `install_python.sh` is downloaded if Python is missing +- Temporary files are cleaned up on exit + +## Additional Resources + +- **Bitwarden Secrets Manager**: +- **Bitwarden SDK**: +- **Bitwarden SDK Python**: + +## Notes + +- The script preserves all command-line arguments when passing to Python +- Environment variables are inherited by the Python process +- The script uses `exec` to replace the bash process with Python (preserves exit codes) +- Temporary files are automatically cleaned up on exit +- The script works in both local and URL execution contexts +- For backward compatibility, omitting the subcommand defaults to `get` +- Future subcommands (`create`, `update`, `delete`, `list`) will be added in upcoming releases diff --git a/scripts/bwsm_secret.py b/scripts/bwsm_secret.py new file mode 100755 index 0000000..e0948fa --- /dev/null +++ b/scripts/bwsm_secret.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +""" +bwsm_secret.py + +Manage secrets in Bitwarden Secrets Manager using the Bitwarden SDK. + +Subcommands: + get - Get a secret value + create - Create a new secret (coming soon) + update - Update an existing secret (coming soon) + delete - Delete secret(s) (coming soon) + list - List all secrets (coming soon) + +Usage: + bwsm_secret.py [options] + +Examples: + # Get secret by ID + ./bwsm_secret.py get --secret-id --access-token "$BWS_ACCESS_TOKEN" + + # Get secret by name (requires org-id) + ./bwsm_secret.py get --secret-name "my-secret" --access-token "$BWS_ACCESS_TOKEN" --org-id + + # Using environment variables + export BWS_ACCESS_TOKEN="..." + export BWS_SECRET_ID="..." # or BWS_SECRET_NAME="..." + export BWS_ORG_ID="..." # Required for name lookup + ./bwsm_secret.py get + +Config sources (highest priority first): +1) CLI args: --access-token, --secret-id or --secret-name, --org-id +2) Environment: BWS_ACCESS_TOKEN, BWS_SECRET_ID or BWS_SECRET_NAME, BWS_ORG_ID + +Output: + - By default prints ONLY the secret value to stdout. + - Use --json for structured output. + - Use --debug for extra stderr logs. + +Exit codes: + 0 success + 2 config/usage error + 3 auth error + 4 not found + 5 sdk/runtime error +""" + +import argparse +import os +import re +import sys +from typing import Optional, Tuple + +from bitwarden_sdk import BitwardenClient + + +def eprint(*args: object) -> None: + print(*args, file=sys.stderr) + + +def is_uuid(value: str) -> bool: + """Check if a string is a valid UUID format.""" + uuid_pattern = re.compile( + r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', + re.IGNORECASE + ) + return bool(uuid_pattern.match(value)) + + +def resolve_config(args: argparse.Namespace) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], str]: + """ + Resolve (access_token, secret_identifier, org_id, identifier_type, source_label). + identifier_type: 'id' or 'name' + """ + # Determine secret identifier (ID or name) + secret_identifier = None + identifier_type = None + + if args.secret_id: + secret_identifier = args.secret_id + identifier_type = "id" + elif args.secret_name: + secret_identifier = args.secret_name + identifier_type = "name" + + # 1) CLI args (check if both are provided and non-empty) + if args.access_token is not None and secret_identifier: + # If CLI args are provided but empty, fall through to env vars + if args.access_token and secret_identifier: + # Support both BWS_ORG_ID (standard) and BW_ORGANIZATION_ID (legacy) + org_id = args.org_id or os.getenv("BWS_ORG_ID") or os.getenv("BW_ORGANIZATION_ID") + return args.access_token, secret_identifier, org_id, identifier_type, "cli" + # If one is empty, try to fill from env + access_token = args.access_token if args.access_token else os.getenv("BWS_ACCESS_TOKEN") + if not secret_identifier: + # Try environment variables, but don't auto-detect type + if os.getenv("BWS_SECRET_ID"): + secret_identifier = os.getenv("BWS_SECRET_ID") + identifier_type = "id" + elif os.getenv("BWS_SECRET_NAME"): + secret_identifier = os.getenv("BWS_SECRET_NAME") + identifier_type = "name" + if access_token and secret_identifier: + # Support both BWS_ORG_ID (standard) and BW_ORGANIZATION_ID (legacy) + org_id = args.org_id or os.getenv("BWS_ORG_ID") or os.getenv("BW_ORGANIZATION_ID") + return access_token, secret_identifier, org_id, identifier_type, "cli" + + # 2) Environment + env_token = os.getenv("BWS_ACCESS_TOKEN") + env_secret_id = os.getenv("BWS_SECRET_ID") + env_secret_name = os.getenv("BWS_SECRET_NAME") + # Support both BWS_ORG_ID (standard) and BW_ORGANIZATION_ID (legacy) + env_org_id = os.getenv("BWS_ORG_ID") or os.getenv("BW_ORGANIZATION_ID") + + if env_token: + if env_secret_id: + return env_token, env_secret_id, env_org_id, "id", "env" + elif env_secret_name: + return env_token, env_secret_name, env_org_id, "name", "env" + + # Partial presence can be helpful to message + return None, None, None, None, "missing" + + +def find_secret_by_name(client, secret_name: str, org_id: Optional[str] = None, debug: bool = False) -> str: + """ + Find a secret by name by listing all secrets and matching the key/name. + Returns the secret ID if found. + """ + if debug: + eprint(f"Listing secrets to find secret with name/key='{secret_name}'...") + + # Get organization ID from access token response or use provided org_id + # We need org_id to list secrets + if not org_id: + # Try to get org_id from the client state if available + # For now, we'll need org_id to be provided + raise RuntimeError("ORG_ID_REQUIRED: Organization ID is required when searching by name") + + list_response = client.secrets().list(organization_id=org_id) + if not getattr(list_response, "success", False): + msg = getattr(list_response, "error_message", "Unknown error") + raise RuntimeError(f"LIST_ERROR: {msg}") + + data = getattr(list_response, "data", None) + if not data: + raise RuntimeError("LIST_ERROR: No data returned from secrets list") + + # Get the list of secrets + secrets = getattr(data, "data", []) or [] + + if debug: + eprint(f"Found {len(secrets)} secrets, searching for name/key='{secret_name}'...") + + # Search for secret by key (name) + for secret in secrets: + secret_key = getattr(secret, "key", None) + secret_id = getattr(secret, "id", None) + if secret_key == secret_name: + if debug: + eprint(f"Found secret: name='{secret_name}', id='{secret_id}'") + return secret_id + + raise RuntimeError(f"NOT_FOUND: Secret with name/key '{secret_name}' not found") + + +def get_secret_value(access_token: str, secret_identifier: str, identifier_type: Optional[str], org_id: Optional[str] = None, debug: bool = False) -> str: + """ + Authenticate with access token and retrieve a secret value by ID or name. + Raises RuntimeError with a categorized message on failures. + """ + client = BitwardenClient() + + if debug: + eprint("Authenticating with access token...") + + login_response = client.auth().login_access_token(access_token=access_token) + if not getattr(login_response, "success", False): + msg = getattr(login_response, "error_message", "Unknown authentication error") + raise RuntimeError(f"AUTH_ERROR: {msg}") + + # If searching by name, first find the secret ID + secret_id = secret_identifier + + if identifier_type == "name": + # Search by name + if debug: + if org_id: + eprint(f"Searching for secret name='{secret_identifier}' (organization id={org_id})...") + else: + eprint(f"Searching for secret name='{secret_identifier}'...") + if not org_id: + raise RuntimeError("ORG_ID_REQUIRED: Organization ID is required when using secret name (provide via --org-id or BWS_ORG_ID)") + secret_id = find_secret_by_name(client, secret_identifier, org_id=org_id, debug=debug) + elif identifier_type == "id": + # Validate that it's a UUID + if not is_uuid(secret_identifier): + raise RuntimeError(f"INVALID_ID: Secret ID must be a valid UUID format. Got: '{secret_identifier}'. Use --secret-name for name-based lookup.") + + if debug: + if org_id: + eprint(f"Fetching secret id={secret_id} (organization id={org_id})...") + else: + eprint(f"Fetching secret id={secret_id}...") + + # Note: organization_id is not a parameter for secrets().get() + # The organization is scoped via the access token + secret_response = client.secrets().get(id=secret_id) + if getattr(secret_response, "success", False) and getattr(secret_response, "data", None): + value = getattr(secret_response.data, "value", None) + if value is None: + raise RuntimeError("SDK_ERROR: Secret returned but value is empty/null") + return value + + msg = getattr(secret_response, "error_message", "Unknown error") + raise RuntimeError(f"NOT_FOUND_OR_ERROR: {msg}") + + +def build_parser(subcommand: str) -> argparse.ArgumentParser: + """Build argument parser for the given subcommand.""" + if subcommand == "get": + p = argparse.ArgumentParser( + description="Get a specific secret value from Bitwarden Secrets Manager (Bitwarden SDK)." + ) + p.add_argument("--secret-id", help="Secret ID (UUID format required) to fetch") + p.add_argument("--secret-name", help="Secret name/key to fetch (requires --org-id)") + p.add_argument( + "--access-token", + help="Bitwarden Secrets Manager access token (prefer env var BWS_ACCESS_TOKEN)", + ) + p.add_argument( + "--org-id", + help="Organization ID (UUID) - required when using --secret-name, optional otherwise (prefer env var BWS_ORG_ID or BW_ORGANIZATION_ID)", + ) + p.add_argument( + "--json", + action="store_true", + help="Print JSON output (includes secret_id, source). Value still included.", + ) + p.add_argument( + "--debug", + action="store_true", + help="Print debug logs to stderr.", + ) + elif subcommand in ("create", "update", "delete", "list"): + # Placeholder parsers for future subcommands + p = argparse.ArgumentParser( + description=f"{subcommand.capitalize()} secret(s) in Bitwarden Secrets Manager (coming soon)." + ) + p.add_argument( + "--access-token", + help="Bitwarden Secrets Manager access token (prefer env var BWS_ACCESS_TOKEN)", + ) + p.add_argument( + "--org-id", + help="Organization ID (UUID) (prefer env var BWS_ORG_ID or BW_ORGANIZATION_ID)", + ) + p.add_argument( + "--debug", + action="store_true", + help="Print debug logs to stderr.", + ) + else: + p = argparse.ArgumentParser( + description="Manage secrets in Bitwarden Secrets Manager (Bitwarden SDK)." + ) + return p + + +def handle_get(args: argparse.Namespace) -> int: + """Handle the 'get' subcommand.""" + # Validate that only one of --secret-id or --secret-name is provided + if args.secret_id and args.secret_name: + eprint("Error: Cannot specify both --secret-id and --secret-name. Use only one.") + return 2 + + access_token, secret_identifier, org_id, identifier_type, source = resolve_config(args) + + if not access_token or not secret_identifier: + eprint("Config error: missing Bitwarden credentials.") + eprint("Provide both access token and secret identifier via one of:") + eprint(" - CLI: --access-token ... --secret-id [--org-id ...]") + eprint(" - CLI: --access-token ... --secret-name --org-id ") + eprint(" - Env: BWS_ACCESS_TOKEN and BWS_SECRET_ID (or BWS_SECRET_NAME)") + eprint(" - Env: BWS_ORG_ID or BW_ORGANIZATION_ID (required for --secret-name)") + eprint("") + # Debug info + if args.debug: + eprint("Debug info:") + eprint(f" CLI --access-token: {'provided' if args.access_token is not None else 'not provided'} ({'empty' if args.access_token == '' else 'has value'})") + eprint(f" CLI --secret-id: {'provided' if args.secret_id is not None else 'not provided'} ({'empty' if args.secret_id == '' else 'has value'})") + eprint(f" CLI --secret-name: {'provided' if args.secret_name is not None else 'not provided'} ({'empty' if args.secret_name == '' else 'has value'})") + eprint(f" Env BWS_ACCESS_TOKEN: {'set' if os.getenv('BWS_ACCESS_TOKEN') else 'not set'}") + eprint(f" Env BWS_SECRET_ID: {'set' if os.getenv('BWS_SECRET_ID') else 'not set'}") + eprint(f" Env BWS_SECRET_NAME: {'set' if os.getenv('BWS_SECRET_NAME') else 'not set'}") + return 2 + + try: + value = get_secret_value(access_token, secret_identifier, identifier_type, org_id=org_id, debug=args.debug) + + if args.json: + # Minimal JSON, no extra deps + org_json = f",\"org_id\":\"{org_id}\"" if org_id else "" + identifier_json = f"\"secret_{identifier_type}\":\"{secret_identifier}\"," + out = ( + "{" + f"{identifier_json}" + f"\"source\":\"{source}\"{org_json}," + f"\"value\":\"{value.replace('\\\\', '\\\\\\\\').replace('\"', '\\\\\"')}\"" + "}" + ) + print(out) + else: + # value-only output for easy piping + print(value) + + return 0 + + except RuntimeError as exc: + msg = str(exc) + + if msg.startswith("AUTH_ERROR:"): + eprint(f"Error: Authentication failed. {msg[len('AUTH_ERROR: '):]}") + return 3 + + if msg.startswith("NOT_FOUND_OR_ERROR:") or msg.startswith("NOT_FOUND:"): + error_detail = msg[len('NOT_FOUND_OR_ERROR: '):] if msg.startswith('NOT_FOUND_OR_ERROR:') else msg[len('NOT_FOUND: '):] + eprint(f"Error: Secret not found. {error_detail}") + return 4 + + if msg.startswith("ORG_ID_REQUIRED:"): + eprint(f"Error: {msg[len('ORG_ID_REQUIRED: '):]}") + eprint("When using --secret-name, --org-id is required.") + return 2 + + if msg.startswith("INVALID_ID:"): + eprint(f"Error: {msg[len('INVALID_ID: '):]}") + return 2 + + if msg.startswith("LIST_ERROR:"): + eprint(f"Error: Failed to list secrets. {msg[len('LIST_ERROR: '):]}") + return 5 + + if msg.startswith("SDK_ERROR:"): + eprint(f"Error: SDK returned unexpected data. {msg[len('SDK_ERROR: '):]}") + return 5 + + eprint(f"Error: {msg}") + return 5 + + except Exception as exc: + eprint(f"Unexpected error: {exc}") + return 5 + + +def handle_create(args: argparse.Namespace) -> int: + """Handle the 'create' subcommand (coming soon).""" + eprint("Error: 'create' subcommand is not yet implemented.") + return 2 + + +def handle_update(args: argparse.Namespace) -> int: + """Handle the 'update' subcommand (coming soon).""" + eprint("Error: 'update' subcommand is not yet implemented.") + return 2 + + +def handle_delete(args: argparse.Namespace) -> int: + """Handle the 'delete' subcommand (coming soon).""" + eprint("Error: 'delete' subcommand is not yet implemented.") + return 2 + + +def handle_list(args: argparse.Namespace) -> int: + """Handle the 'list' subcommand (coming soon).""" + eprint("Error: 'list' subcommand is not yet implemented.") + return 2 + + +def main() -> int: + """Main entry point with subcommand routing.""" + # Parse subcommand from first argument + if len(sys.argv) < 2: + eprint("Error: Missing subcommand.") + eprint("Usage: bwsm_secret.py [options]") + eprint("Subcommands: get, create, update, delete, list") + return 2 + + subcommand = sys.argv[1] + valid_subcommands = ["get", "create", "update", "delete", "list"] + + if subcommand not in valid_subcommands: + eprint(f"Error: Invalid subcommand '{subcommand}'.") + eprint(f"Valid subcommands: {', '.join(valid_subcommands)}") + return 2 + + # Build parser for this subcommand and parse remaining arguments + parser = build_parser(subcommand) + # Skip the subcommand when parsing (sys.argv[0] is script name, sys.argv[1] is subcommand) + args = parser.parse_args(sys.argv[2:]) + + # Route to appropriate handler + if subcommand == "get": + return handle_get(args) + elif subcommand == "create": + return handle_create(args) + elif subcommand == "update": + return handle_update(args) + elif subcommand == "delete": + return handle_delete(args) + elif subcommand == "list": + return handle_list(args) + else: + eprint(f"Error: Unhandled subcommand '{subcommand}'.") + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/bwsm_secret.sh b/scripts/bwsm_secret.sh new file mode 100755 index 0000000..a79344f --- /dev/null +++ b/scripts/bwsm_secret.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +set -e + +GITHUB_BASE="https://raw.githubusercontent.com/jdevto/cli-tools/main/scripts" +TMP_PYTHON_SCRIPT="/tmp/bwsm_secret.py" +TMP_INSTALL_SCRIPT="/tmp/install_python.sh" + +# Valid subcommands +VALID_SUBCOMMANDS="get create update delete list" + +# Global variables +PYTHON_SCRIPT="" +IS_URL_EXECUTION=false +SCRIPT_DIR="" +SUBCOMMAND="" +REMAINING_ARGS=() + +usage() { + echo "Usage: $0 [subcommand] [options]" + echo "" + echo "Subcommands:" + echo " get - Get a secret value (default if no subcommand provided)" + echo " create - Create a new secret (coming soon)" + echo " update - Update an existing secret (coming soon)" + echo " delete - Delete secret(s) (coming soon)" + echo " list - List all secrets (coming soon)" + echo "" + echo "For backward compatibility, if no subcommand is provided, 'get' is assumed." + echo "Examples:" + echo " $0 --secret-id --access-token # Uses 'get' subcommand" + echo " $0 get --secret-id --access-token " + exit 1 +} + +# Parse subcommand from arguments +parse_subcommand() { + # Handle --help before parsing subcommand + if [[ $# -gt 0 ]] && [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then + usage + exit 0 + fi + + # Check if first argument is a valid subcommand + if [[ $# -gt 0 ]] && [[ " $VALID_SUBCOMMANDS " =~ " $1 " ]]; then + SUBCOMMAND="$1" + # Store remaining arguments (skip the first one which is the subcommand) + REMAINING_ARGS=("${@:2}") + else + # No subcommand provided, default to 'get' for backward compatibility + SUBCOMMAND="get" + # All arguments are remaining (no subcommand to skip) + REMAINING_ARGS=("$@") + fi +} + +cleanup() { + rm -f "$TMP_PYTHON_SCRIPT" "$TMP_INSTALL_SCRIPT" +} +trap cleanup EXIT + +# Detect execution context (URL vs local file) +detect_execution_context() { + # Check if script is being run from URL (piped via curl) or local file + if [[ "${BASH_SOURCE[0]}" == *"/dev/fd/"* ]] || [[ ! -f "${BASH_SOURCE[0]}" ]]; then + # Running from URL - download Python script from GitHub + echo "Detected execution from URL. Downloading Python script..." + if ! curl -fsSL "$GITHUB_BASE/bwsm_secret.py" -o "$TMP_PYTHON_SCRIPT"; then + echo "Error: Failed to download Python script from GitHub" >&2 + exit 1 + fi + chmod +x "$TMP_PYTHON_SCRIPT" + PYTHON_SCRIPT="$TMP_PYTHON_SCRIPT" + IS_URL_EXECUTION=true + else + # Running locally - use local Python script + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + PYTHON_SCRIPT="$SCRIPT_DIR/bwsm_secret.py" + IS_URL_EXECUTION=false + + if [[ ! -f "$PYTHON_SCRIPT" ]]; then + echo "Error: Python script not found at $PYTHON_SCRIPT" >&2 + exit 1 + fi + fi +} + +# Check for Python 3 and install if needed +ensure_python() { + if command -v python3 >/dev/null 2>&1; then + return 0 + fi + + echo "Python 3 not found. Installing..." + + if [[ "$IS_URL_EXECUTION" == "true" ]]; then + # Download install_python.sh from GitHub + if ! curl -fsSL "$GITHUB_BASE/install_python.sh" -o "$TMP_INSTALL_SCRIPT"; then + echo "Error: Failed to download install_python.sh from GitHub" >&2 + exit 1 + fi + chmod +x "$TMP_INSTALL_SCRIPT" + bash "$TMP_INSTALL_SCRIPT" install + else + # Use local install_python.sh + INSTALL_SCRIPT="$SCRIPT_DIR/install_python.sh" + if [[ ! -f "$INSTALL_SCRIPT" ]]; then + echo "Error: install_python.sh not found at $INSTALL_SCRIPT" >&2 + exit 1 + fi + bash "$INSTALL_SCRIPT" install + fi + + # Verify Python is now available + if ! command -v python3 >/dev/null 2>&1; then + echo "Error: Python 3 installation failed or python3 not in PATH" >&2 + exit 1 + fi +} + +# Check for bitwarden_sdk and install if needed +ensure_bitwarden_sdk() { + if python3 -c "import bitwarden_sdk" 2>/dev/null; then + return 0 + fi + + echo "bitwarden_sdk not found. Installing..." + + # Try python3 -m pip first, then pip3 + if python3 -m pip install bitwarden-sdk 2>/dev/null; then + return 0 + elif pip3 install bitwarden-sdk 2>/dev/null; then + return 0 + else + echo "Error: Failed to install bitwarden-sdk. Please install manually:" >&2 + echo " python3 -m pip install bitwarden-sdk" >&2 + exit 1 + fi +} + +# Main execution +main() { + # Parse subcommand from arguments + parse_subcommand "$@" + + detect_execution_context + ensure_python + ensure_bitwarden_sdk + + # Pass subcommand and remaining arguments to Python script + exec python3 "$PYTHON_SCRIPT" "$SUBCOMMAND" "${REMAINING_ARGS[@]}" +} + +main "$@"