"""Utility script to provision S3 read-only credentials for the Streamlit app. This helper creates (or reuses) an IAM user and optional role that have `s3:ListBucket` / `s3:GetObject` access to the `chaptive-rag` bucket. It then prints the access key and secret so they can be set as environment variables for the Streamlit app. Usage: ```bash python streamlit/scripts/create_iam_role.py \ --entity-name streamlit-cache-reader \ --bucket chaptive-rag ``` The AWS account credentials used to run this script must already have IAM permissions to create users/roles, attach policies, and create access keys. """ from __future__ import annotations import argparse import json import sys from typing import Any, Dict, Optional import boto3 from botocore.exceptions import ClientError S3_POLICY_NAME = "StreamlitS3ReadOnly" DEFAULT_ENTITY_NAME = "streamlit-cache-reader" DEFAULT_BUCKET = "chaptly-rag" def _build_policy(bucket: str) -> Dict[str, Any]: resource_arn = f"arn:aws:s3:::{bucket}" object_arn = f"{resource_arn}/*" return { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": resource_arn, }, { "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": object_arn, }, ], } def ensure_user(iam, user_name: str) -> None: try: iam.get_user(UserName=user_name) print(f"Reusing IAM user '{user_name}'.") except ClientError as exc: if exc.response["Error"]["Code"] == "NoSuchEntity": iam.create_user(UserName=user_name) print(f"Created IAM user '{user_name}'.") else: raise def attach_inline_policy(iam, user_name: str, bucket: str) -> None: policy_doc = json.dumps(_build_policy(bucket)) iam.put_user_policy(UserName=user_name, PolicyName=S3_POLICY_NAME, PolicyDocument=policy_doc) print(f"Attached inline S3 read policy '{S3_POLICY_NAME}' to user '{user_name}'.") def create_access_key(iam, user_name: str) -> Dict[str, str]: response = iam.create_access_key(UserName=user_name) access_key = response["AccessKey"] return { "AWS_ACCESS_KEY_ID": access_key["AccessKeyId"], "AWS_SECRET_ACCESS_KEY": access_key["SecretAccessKey"], } def ensure_role(iam, role_name: str, bucket: str, assume_policy: Dict[str, Any]) -> None: try: iam.get_role(RoleName=role_name) print(f"Reusing IAM role '{role_name}'.") except ClientError as exc: if exc.response["Error"]["Code"] == "NoSuchEntity": iam.create_role(RoleName=role_name, AssumeRolePolicyDocument=json.dumps(assume_policy)) print(f"Created IAM role '{role_name}'.") else: raise policy_doc = json.dumps(_build_policy(bucket)) iam.put_role_policy(RoleName=role_name, PolicyName=S3_POLICY_NAME, PolicyDocument=policy_doc) print(f"Attached inline S3 read policy '{S3_POLICY_NAME}' to role '{role_name}'.") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Provision S3 read-only IAM credentials for Streamlit.") parser.add_argument("--entity-name", default=DEFAULT_ENTITY_NAME, help="IAM user/role name to create or reuse.") parser.add_argument("--bucket", default=DEFAULT_BUCKET, help="S3 bucket name (default: chaptive-rag).") parser.add_argument( "--create-role", action="store_true", help="Also create an IAM role with the same name and inline policy (optional).", ) parser.add_argument( "--role-trust", default=None, help="Path to a JSON file describing the assume-role trust policy (required if --create-role).", ) return parser.parse_args() def main() -> None: args = parse_args() iam = boto3.client("iam") ensure_user(iam, args.entity_name) attach_inline_policy(iam, args.entity_name, args.bucket) credentials = create_access_key(iam, args.entity_name) print("\nAdd the following values to your Streamlit secrets:") for key, value in credentials.items(): print(f"{key} = {value}") print("AWS_REGION = ap-southeast-1") print(f"CHAPTIVE_S3_BUCKET = {args.bucket}") if args.create_role: if not args.role_trust: raise SystemExit("--role-trust JSON file is required when --create-role is set.") with open(args.role_trust, "r", encoding="utf-8") as handle: assume_policy = json.load(handle) ensure_role(iam, args.entity_name, args.bucket, assume_policy) print( "\nIAM role created. Attach this role to an EC2/Lambda/Streamlit Cloud runner and expose temporary credentials via environment variables." ) if __name__ == "__main__": try: main() except ClientError as exc: print(f"AWS error: {exc}") sys.exit(1) except KeyboardInterrupt: print("Aborted by user.") sys.exit(130)