summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-04-17 23:42:02 +0200
committerFlorian Dold <florian@dold.me>2023-04-17 23:42:11 +0200
commitae6d881a9fd56598005f90963609291d5fa5bdb3 (patch)
treed3097a1652ec0edaae03df3c14ae1ec8d159dfcc
parentdd24c764f70b39820622fa274ee71c87fc5e93bf (diff)
downloaddeployment-ae6d881a9fd56598005f90963609291d5fa5bdb3.tar.gz
deployment-ae6d881a9fd56598005f90963609291d5fa5bdb3.tar.bz2
deployment-ae6d881a9fd56598005f90963609291d5fa5bdb3.zip
splitops
-rw-r--r--splitops/README.md40
-rwxr-xr-xsplitops/splitops143
2 files changed, 183 insertions, 0 deletions
diff --git a/splitops/README.md b/splitops/README.md
new file mode 100644
index 0000000..a349ce3
--- /dev/null
+++ b/splitops/README.md
@@ -0,0 +1,40 @@
+# splitops
+
+Splitops is a script to allow execution of commands only after the approval of
+multiple users.
+
+It is intended to be used with OpenSSH by specifiying it as the "command" option
+for authorized users in `~/.ssh/authorized_keys`.
+
+For example, consider following `authorized_keys` file for the user `root`:
+
+```
+command="/bin/splitops --user=alice" [... key of alice ...]
+command="/bin/splitops --user=bob" [... key of bob ...]
+```
+
+This allows Alice and Bob to jointly run commands:
+
+```
+bob$ ssh root@server propose rm -rf /opt/something
+authenticated as: bob
+requested command: ['rm', '-rf', '/opt/something']
+assigned id: ccafbd
+
+bob$ ssh root@server approve ccafbd
+
+alice$ ssh root@server get
+{'cmd': ['rm', '-rf', '/opt/something'], 'request_id': 'ccafbd'}
+
+
+alice$ ssh root@server approve ccafbd
+
+bob$ ssh root@server run ccafbd
+==stdout==
+...
+====
+==stderr==
+...
+====
+exit status: 0
+```
diff --git a/splitops/splitops b/splitops/splitops
new file mode 100755
index 0000000..8e5414c
--- /dev/null
+++ b/splitops/splitops
@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+
+"""
+This script is intended to be used as a SSH command wrapper.
+
+It allows users to propose a command that should be run.
+The command will only be executed after a threshold of
+other users has approved the command.
+"""
+
+import os
+import shlex
+import sys
+import json
+from pathlib import Path
+import uuid
+from dataclasses import dataclass
+import subprocess
+
+# Approval threshold, including the approval
+# of the proposer.
+APPROVAL_THRESHOLD = 2
+
+cmdpath = Path.home() / "cmd.json"
+
+def write_cmd(d):
+ with open(cmdpath, "w") as f:
+ f.write(json.dumps(d))
+
+def read_cmd():
+ try:
+ with open(cmdpath, "r") as f:
+ return json.load(f)
+ except FileNotFoundError:
+ return None
+
+def propose(cmd):
+ request_id = uuid.uuid4().hex.lower()[0:6]
+ for x in cmd:
+ if not x.isascii():
+ print("requested command not ascii")
+ sys.exit(4)
+ print(f"requested command: {cmd}")
+ write_cmd({"cmd": cmd, "request_id": request_id})
+ print(f"assigned id: {request_id}")
+
+def approve(my_user, request_id):
+ print(f"approving command {request_id} as {my_user}")
+ d = read_cmd()
+ if d is None:
+ print("no command proposed")
+ sys.exit(1)
+ if d["request_id"] != request_id:
+ print("request ID does not match")
+ sys.exit(1)
+ approved_by = d.get("approved_by", [])
+ if my_user not in approved_by:
+ approved_by.append(my_user)
+ d["approved_by"] = approved_by
+ write_cmd(d)
+
+def run(request_id):
+ print(f"running command with ID {request_id}")
+ d = read_cmd()
+ if d is None:
+ print("no command proposed")
+ sys.exit(1)
+ if d["request_id"] != request_id:
+ print("request ID does not match")
+ sys.exit(1)
+ approved_by = d.get("approved_by", [])
+ num_approvals = len(approved_by)
+ if num_approvals < APPROVAL_THRESHOLD:
+ print(f"not enough approvals, got {num_approvals} but need {APPROVAL_THRESHOLD}")
+ sys.exit(1)
+ if d.get("executed", False):
+ print("command has already been executed once, please request again")
+ sys.exit(1)
+ cmd = d["cmd"]
+ d["executed"] = True
+ # Mark as executed, can only execute once!
+ write_cmd(d)
+ print("running command", cmd)
+ res = subprocess.run(cmd, capture_output=True, encoding="utf-8")
+ print(f"==stdout==\n{res.stdout}====")
+ print(f"==stderr==\n{res.stderr}====")
+ print(f"exit code: {res.returncode}")
+ # FIXME: Write log to disk?
+
+
+def usage():
+ print("Commands:")
+ print(" whoami: Check authentication.")
+ print(" propose CMD...: Propose a new command.")
+ print(" get: Get the currently proposed command.")
+ print(" approve CMDID: Approve a command.")
+ print(" run CMDID: Run a sufficiently approved command.")
+ print(" discard: Discard the currently proposed command.")
+ sys.exit(1)
+
+def die(msg):
+ printf(msg)
+ sys.exit(2)
+
+def main():
+ if len(sys.argv) != 2:
+ die("unexpected usage")
+ user = sys.argv[1]
+ os_user = os.environ["USER"]
+ print(f"authenticated as: {user}")
+ inner_cmd = os.environ.get("SSH_ORIGINAL_COMMAND")
+ if inner_cmd is None:
+ print("no command provided, try help")
+ sys.exit(3)
+ inner_args = shlex.split(inner_cmd)
+ if len(inner_args) < 1:
+ usage()
+ subcommand = inner_args[0]
+ if subcommand == "discard":
+ cmdpath.unlink()
+ elif subcommand == "whoami":
+ print(f"you are {user} on {os_user}")
+ elif subcommand == "propose":
+ propose(inner_args[1:])
+ elif subcommand == "get":
+ print(read_cmd())
+ elif subcommand == "help":
+ usage()
+ elif subcommand == "run":
+ if len(inner_args) != 2:
+ usage()
+ run(inner_args[1])
+ elif subcommand == "approve":
+ if len(inner_args) != 2:
+ usage()
+ approve(user, inner_args[1])
+ else:
+ print(f"unknown subcommand {subcommand}")
+ usage()
+
+if __name__ == '__main__':
+ main()
+