#!/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