| """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) |
|
|