railiance-platform/scripts/survey-backup-iam.py

154 lines
4.9 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Survey Scaleway IAM names and the backup bucket policy.
Reads the bootstrap and scoped-backup pairs in memory. Writes names, ids and
the bucket policy only. Never prints a credential.
"""
from __future__ import annotations
import datetime as dt
import hashlib
import hmac
import json
import subprocess
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
RECEIPT = Path("/home/worsch/.local/state/railiance/receipts/backup-iam-survey.json")
STAGE = Path("/home/worsch/.local/state/railiance/receipts/backup-iam-survey.stage")
BUCKET = "railiance-platform-pg-backup"
HOST = "s3.nl-ams.scw.cloud"
def stage(text):
STAGE.parent.mkdir(mode=0o700, exist_ok=True)
STAGE.write_text(text + "\n")
STAGE.chmod(0o600)
def kv(path):
proc = subprocess.run(
["bao", "kv", "get", "-format=json", path],
capture_output=True,
)
if proc.returncode:
stage(f"kv_read_failed {path} {proc.returncode}")
raise SystemExit(41)
stage(f"kv_read_ok {path}")
return json.loads(proc.stdout)["data"]["data"]
def scaleway(secret, url):
request = urllib.request.Request(url, headers={"X-Auth-Token": secret})
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.status, json.loads(response.read().decode())
except urllib.error.HTTPError as exc:
return exc.code, None
def sign(access_key, secret_key, canonical_uri, query):
now = dt.datetime.now(dt.timezone.utc)
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
day = now.strftime("%Y%m%d")
payload = hashlib.sha256(b"").hexdigest()
canonical_query = urllib.parse.urlencode(sorted(query.items()))
canonical = "\n".join(
[
"GET",
canonical_uri,
canonical_query,
f"host:{HOST}",
f"x-amz-content-sha256:{payload}",
f"x-amz-date:{amz_date}",
"",
"host;x-amz-content-sha256;x-amz-date",
payload,
]
)
scope = f"{day}/nl-ams/s3/aws4_request"
to_sign = "\n".join(
["AWS4-HMAC-SHA256", amz_date, scope, hashlib.sha256(canonical.encode()).hexdigest()]
)
key = ("AWS4" + secret_key).encode()
for part in (day, "nl-ams", "s3", "aws4_request"):
key = hmac.new(key, part.encode(), hashlib.sha256).digest()
signature = hmac.new(key, to_sign.encode(), hashlib.sha256).hexdigest()
return {
"Authorization": (
"AWS4-HMAC-SHA256 "
f"Credential={access_key}/{scope}, "
"SignedHeaders=host;x-amz-content-sha256;x-amz-date, "
f"Signature={signature}"
),
"x-amz-date": amz_date,
"x-amz-content-sha256": payload,
}
def bucket_policy(access_key, secret_key):
query = {"policy": ""}
headers = sign(access_key, secret_key, f"/{BUCKET}", query)
url = f"https://{HOST}/{BUCKET}?policy"
request = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return response.status, json.loads(response.read().decode())
except urllib.error.HTTPError as exc:
return exc.code, None
def main():
boot = kv("platform/workloads/railiance/scaleway/bootstrap")
backup = kv("platform/workloads/railiance/backup/object-storage")
org = boot["DEFAULT_ORGANIZATION_ID"]
project = boot["DEFAULT_PROJECT_ID"]
app_status, applications = scaleway(
boot["SECRET_KEY"],
"https://api.scaleway.com/iam/v1alpha1/applications"
f"?organization_id={urllib.parse.quote(org)}&page_size=100",
)
policy_status, policies = scaleway(
boot["SECRET_KEY"],
"https://api.scaleway.com/iam/v1alpha1/policies"
f"?organization_id={urllib.parse.quote(org)}&page_size=100",
)
s3_status, document = bucket_policy(backup["ACCESS_KEY"], backup["SECRET_KEY"])
receipt = {
"organization_id": org,
"project_id": project,
"application_list_status": app_status,
"applications": [
{"id": item.get("id"), "name": item.get("name")}
for item in (applications or {}).get("applications", [])
],
"policy_list_status": policy_status,
"policies": [
{
"id": item.get("id"),
"name": item.get("name"),
"application_id": (item.get("application_id") or item.get("principal", {}).get("id")),
}
for item in (policies or {}).get("policies", [])
],
"bucket_policy_status": s3_status,
"bucket_policy": document,
}
RECEIPT.parent.mkdir(mode=0o700, exist_ok=True)
RECEIPT.write_text(json.dumps(receipt, indent=2) + "\n")
RECEIPT.chmod(0o600)
if __name__ == "__main__":
try:
main()
stage("ok")
except SystemExit:
raise
except Exception as exc:
stage("failed " + type(exc).__name__)
raise SystemExit(42) from None