1
ewconfig/scripts/1p-bridge
2024-11-09 14:11:40 -05:00

132 lines
3.8 KiB
Python
Executable File

#! /usr/bin/env python3
"""
# `1p-bridge`: 1password bridge
This script can expose the `op` cli utility as a web service.
Callers can then pass a 1password service token through an HTTP request,
and are able to read secrets from 1password.
This allows the use of 1password as a basic secret store for self-hosted applications.
```
HTTP routes:
/whoami
GET: Returns the current user's information
/read/<path:uri>
GET: Reads the item at the specified URI
```
"""
import argparse
import sys
import os
import logging
import shutil
import subprocess
import json
from flask import Flask, request, Response
from typing import Optional, List, Tuple
app = Flask(__name__)
logger = logging.getLogger(__name__)
def __parse_service_token() -> Optional[str]:
bearer_token = request.headers.get("Authorization")
if bearer_token is None or not bearer_token.startswith("Bearer "):
return None
return bearer_token[7:]
def __op_execute(op_cmd: List[str], bearer_token: str) -> Tuple[str, int]:
# Edit the environment to include the bearer token
current_environment = os.environ.copy()
current_environment["OP_SERVICE_ACCOUNT_TOKEN"] = bearer_token
# Build the full command
cmd = ["op"]
cmd.extend(op_cmd)
cmd.append("--format=json")
# Spawn the onepassword CLI to run the command
op_proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=current_environment,
)
# If stderr contains errors, return them
if op_proc.stderr:
error_lines = op_proc.stderr.read().decode("utf-8").splitlines()
logger.error("1P CLI returned errors. Passing through to caller.")
return json.dumps({"error": error_lines}), 500
# Return the result
return op_proc.stdout.read().decode("utf-8"), 200
@app.route("/whoami", methods=["GET"])
def who_am_i():
# Read the bearer token from the Authorization header
token = __parse_service_token()
if not token:
return jsonify({"error": "No bearer token provided"}), 401
# Spawn the onepassword CLI to query the current user;
raw_json, http_code = __op_execute(["user", "get", "--me"], token)
# Build the response
response = Response(raw_json, content_type="application/json", status=http_code)
return response
@app.route("/read/<path:uri>", methods=["GET"])
def read_item(uri: str):
# Read the bearer token from the Authorization header
token = __parse_service_token()
if not token:
return jsonify({"error": "No bearer token provided"}), 401
# Reconstruct the URI into 1p format
uri = "op://" + uri
# Spawn the onepassword CLI to query the item
raw_json, http_code = __op_execute(["read", uri], token)
# Build the response
response = Response(raw_json, content_type="application/json", status=http_code)
return response
def main() -> int:
# Handle program arguments
ap = argparse.ArgumentParser(prog="", description="")
ap.add_argument(
"--bind", help="Address to bind to", default="unix:///tmp/1password-bridge.sock"
)
ap.add_argument("--port", help="Port to bind to", default=80, type=int)
ap.add_argument(
"-v", "--verbose", help="Enable verbose logging", action="store_true"
)
args = ap.parse_args()
# Configure logging
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s: %(message)s",
)
# If we can't access the `op` commandline utility, we can't do anything
if shutil.which("op") is None:
logger.error("1Password CLI not found. Please install it and try again.")
return 1
# Start up the server
app.run(host=args.bind, port=args.port)
return 0
if __name__ == "__main__":
sys.exit(main())