tempura-client 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tempura_client-0.1.0/PKG-INFO +35 -0
- tempura_client-0.1.0/README.md +25 -0
- tempura_client-0.1.0/pyproject.toml +22 -0
- tempura_client-0.1.0/setup.cfg +4 -0
- tempura_client-0.1.0/src/tempura_client/__init__.py +6 -0
- tempura_client-0.1.0/src/tempura_client/__main__.py +8 -0
- tempura_client-0.1.0/src/tempura_client/app.py +12 -0
- tempura_client-0.1.0/src/tempura_client/config.py +61 -0
- tempura_client-0.1.0/src/tempura_client/remote.py +120 -0
- tempura_client-0.1.0/src/tempura_client/server.py +81 -0
- tempura_client-0.1.0/src/tempura_client.egg-info/PKG-INFO +35 -0
- tempura_client-0.1.0/src/tempura_client.egg-info/SOURCES.txt +17 -0
- tempura_client-0.1.0/src/tempura_client.egg-info/dependency_links.txt +1 -0
- tempura_client-0.1.0/src/tempura_client.egg-info/entry_points.txt +2 -0
- tempura_client-0.1.0/src/tempura_client.egg-info/requires.txt +2 -0
- tempura_client-0.1.0/src/tempura_client.egg-info/top_level.txt +2 -0
- tempura_client-0.1.0/src/tempura_shared/__init__.py +5 -0
- tempura_client-0.1.0/src/tempura_shared/credentials_file.py +128 -0
- tempura_client-0.1.0/src/tempura_shared/validators.py +22 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tempura-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP client that vends short-term AWS credentials via stdio transport
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: mcp[cli]
|
|
9
|
+
Requires-Dist: httpx
|
|
10
|
+
|
|
11
|
+
# tempura-client
|
|
12
|
+
|
|
13
|
+
A lightweight MCP (Model Context Protocol) client that vends short-term AWS
|
|
14
|
+
credentials via stdio transport.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install tempura-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
The package provides a `tempura-client` command that communicates over
|
|
25
|
+
stdin/stdout using the MCP JSON-RPC protocol. It is designed to be launched
|
|
26
|
+
by an MCP-aware agent or host application.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
tempura-client
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Dependencies
|
|
33
|
+
|
|
34
|
+
- `mcp[cli]` - Model Context Protocol SDK
|
|
35
|
+
- `httpx` - HTTP client for forwarding requests to the remote server
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# tempura-client
|
|
2
|
+
|
|
3
|
+
A lightweight MCP (Model Context Protocol) client that vends short-term AWS
|
|
4
|
+
credentials via stdio transport.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install tempura-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
The package provides a `tempura-client` command that communicates over
|
|
15
|
+
stdin/stdout using the MCP JSON-RPC protocol. It is designed to be launched
|
|
16
|
+
by an MCP-aware agent or host application.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
tempura-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Dependencies
|
|
23
|
+
|
|
24
|
+
- `mcp[cli]` - Model Context Protocol SDK
|
|
25
|
+
- `httpx` - HTTP client for forwarding requests to the remote server
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tempura-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP client that vends short-term AWS credentials via stdio transport"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp[cli]",
|
|
14
|
+
"httpx",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
tempura-client = "tempura_client.app:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["src"]
|
|
22
|
+
include = ["tempura_client*", "tempura_shared*"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Application entry point for the Tempura MCP Client.
|
|
2
|
+
|
|
3
|
+
Provides main() as the console-script target so that the client
|
|
4
|
+
can be launched via ``tempura-client`` or ``python -m tempura_client``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
"""Start the Tempura MCP Client stdio server."""
|
|
10
|
+
from tempura_client.server import mcp
|
|
11
|
+
|
|
12
|
+
mcp.run()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Client configuration loader.
|
|
2
|
+
|
|
3
|
+
Reads required environment variables and returns a typed config object.
|
|
4
|
+
Raises ConfigError if any required variable is missing or empty.
|
|
5
|
+
|
|
6
|
+
Requirements: 4.1, 4.2, 4.4, 4.5
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigError(Exception):
|
|
16
|
+
"""Raised when required client configuration is missing or invalid."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ClientConfig:
|
|
21
|
+
"""Immutable container for client configuration.
|
|
22
|
+
|
|
23
|
+
Fields:
|
|
24
|
+
api_key: The API Gateway API key value (from TEMPURA_API_KEY).
|
|
25
|
+
endpoint: The Server endpoint URL (from TEMPURA_ENDPOINT).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
api_key: str
|
|
29
|
+
endpoint: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_config() -> ClientConfig:
|
|
33
|
+
"""Load client configuration from environment variables.
|
|
34
|
+
|
|
35
|
+
Reads TEMPURA_API_KEY and TEMPURA_ENDPOINT from the process
|
|
36
|
+
environment. Raises ConfigError with a clear message identifying
|
|
37
|
+
the missing variable if either is unset or empty.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
ClientConfig with the validated values.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ConfigError: If TEMPURA_API_KEY or TEMPURA_ENDPOINT is
|
|
44
|
+
missing or empty.
|
|
45
|
+
"""
|
|
46
|
+
api_key = os.environ.get("TEMPURA_API_KEY", "")
|
|
47
|
+
if not api_key:
|
|
48
|
+
raise ConfigError(
|
|
49
|
+
"TEMPURA_API_KEY environment variable is required but "
|
|
50
|
+
"not set or empty. Set it to your API Gateway API key."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
endpoint = os.environ.get("TEMPURA_ENDPOINT", "")
|
|
54
|
+
if not endpoint:
|
|
55
|
+
raise ConfigError(
|
|
56
|
+
"TEMPURA_ENDPOINT environment variable is required but "
|
|
57
|
+
"not set or empty. Set it to the Tempura Server URL "
|
|
58
|
+
"(e.g. https://<api-id>.execute-api.<region>.amazonaws.com/prod/mcp)."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return ClientConfig(api_key=api_key, endpoint=endpoint)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Remote HTTP forwarder for the Tempura Client.
|
|
2
|
+
|
|
3
|
+
POSTs a plain JSON request to the deployed Tempura Server REST API
|
|
4
|
+
with the x-api-key header, parses the JSON response, and returns
|
|
5
|
+
credential fields. Never logs or surfaces the API key.
|
|
6
|
+
|
|
7
|
+
Requirements: 3.1, 3.2, 3.3, 3.4, 4.3, 4.6, 6.1, 6.2, 6.3, 6.4, 6.6
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RemoteError(Exception):
|
|
18
|
+
"""Raised when the Server returns an error or is unreachable."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class VendResult:
|
|
23
|
+
"""Immutable container for credentials returned by the Server.
|
|
24
|
+
|
|
25
|
+
Fields:
|
|
26
|
+
access_key_id: AWS access key ID.
|
|
27
|
+
secret_access_key: AWS secret access key.
|
|
28
|
+
session_token: AWS session token.
|
|
29
|
+
expiration: ISO-8601 expiration timestamp.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
access_key_id: str
|
|
33
|
+
secret_access_key: str
|
|
34
|
+
session_token: str
|
|
35
|
+
expiration: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def vend(account_id: str, api_key: str, endpoint: str) -> VendResult:
|
|
39
|
+
"""Forward a get_credentials request to the Tempura Server.
|
|
40
|
+
|
|
41
|
+
Sends a plain JSON POST to the Server endpoint with x-api-key
|
|
42
|
+
authentication. Maps HTTP status codes and Server error responses
|
|
43
|
+
to RemoteError. Never includes the API key in any exception.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
account_id: Validated 12-digit AWS account ID.
|
|
47
|
+
api_key: API Gateway key value (never logged/returned).
|
|
48
|
+
endpoint: Server HTTPS URL.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
VendResult with temporary credential fields.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
RemoteError: On authentication failure, rate limiting,
|
|
55
|
+
connectivity issues, or Server-reported errors.
|
|
56
|
+
"""
|
|
57
|
+
headers = {
|
|
58
|
+
"x-api-key": api_key,
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
}
|
|
61
|
+
body = {"account_id": account_id}
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
response = httpx.post(
|
|
65
|
+
endpoint,
|
|
66
|
+
headers=headers,
|
|
67
|
+
json=body,
|
|
68
|
+
timeout=30.0,
|
|
69
|
+
)
|
|
70
|
+
except httpx.TimeoutException:
|
|
71
|
+
raise RemoteError(
|
|
72
|
+
"Connection timed out reaching the Tempura Server. "
|
|
73
|
+
"Check that TEMPURA_ENDPOINT is correct and the "
|
|
74
|
+
"Server is reachable."
|
|
75
|
+
)
|
|
76
|
+
except httpx.ConnectError:
|
|
77
|
+
raise RemoteError(
|
|
78
|
+
"Unable to connect to the Tempura Server. "
|
|
79
|
+
"Check that TEMPURA_ENDPOINT is correct and the "
|
|
80
|
+
"Server is reachable."
|
|
81
|
+
)
|
|
82
|
+
except httpx.HTTPError:
|
|
83
|
+
raise RemoteError(
|
|
84
|
+
"Connectivity error reaching the Tempura Server. "
|
|
85
|
+
"Check that TEMPURA_ENDPOINT is correct and the "
|
|
86
|
+
"Server is reachable."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if response.status_code == 400:
|
|
90
|
+
data = response.json()
|
|
91
|
+
raise RemoteError(data.get("error", "Bad request"))
|
|
92
|
+
|
|
93
|
+
if response.status_code == 403:
|
|
94
|
+
raise RemoteError(
|
|
95
|
+
"Authentication failed. Verify that your "
|
|
96
|
+
"TEMPURA_API_KEY is valid and active."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if response.status_code == 429:
|
|
100
|
+
raise RemoteError(
|
|
101
|
+
"Rate limited by the Tempura Server. " "Please wait and try again."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if response.status_code == 500:
|
|
105
|
+
data = response.json()
|
|
106
|
+
raise RemoteError(data.get("error", "Server error"))
|
|
107
|
+
|
|
108
|
+
if response.status_code != 200:
|
|
109
|
+
raise RemoteError(
|
|
110
|
+
f"Unexpected response from the Tempura Server "
|
|
111
|
+
f"(HTTP {response.status_code})."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
creds = response.json()
|
|
115
|
+
return VendResult(
|
|
116
|
+
access_key_id=creds["access_key_id"],
|
|
117
|
+
secret_access_key=creds["secret_access_key"],
|
|
118
|
+
session_token=creds["session_token"],
|
|
119
|
+
expiration=creds["expiration"],
|
|
120
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""MCP Client proxy server.
|
|
2
|
+
|
|
3
|
+
Registers the get_credentials proxy tool over stdio transport.
|
|
4
|
+
Validates account_id locally, loads config, forwards to the remote
|
|
5
|
+
Server, and persists credentials to the host credentials file.
|
|
6
|
+
|
|
7
|
+
Requirements: 2.1, 2.2, 2.3, 2.5, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 6.5
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
from tempura_shared.credentials_file import write_profile
|
|
17
|
+
from tempura_shared.validators import validate_account_id
|
|
18
|
+
from tempura_client.config import ConfigError, load_config
|
|
19
|
+
from tempura_client.remote import RemoteError, vend
|
|
20
|
+
|
|
21
|
+
mcp = FastMCP("tempura-client")
|
|
22
|
+
|
|
23
|
+
_TARGET_PROFILE: str = "TargetReadOnly"
|
|
24
|
+
_CREDENTIALS_PATH: str = os.path.join(os.path.expanduser("~"), ".aws", "credentials")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mcp.tool()
|
|
28
|
+
def get_credentials(account_id: str) -> dict:
|
|
29
|
+
"""Get short-term AWS credentials for a target account.
|
|
30
|
+
|
|
31
|
+
Validates the account_id locally, forwards the request to the
|
|
32
|
+
deployed Tempura Server over HTTPS, and writes the returned
|
|
33
|
+
temporary credentials to the [TargetReadOnly] profile in
|
|
34
|
+
~/.aws/credentials.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
account_id: 12-digit AWS account identifier.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dictionary with status, profile, and expiration on success,
|
|
41
|
+
or {"error": "..."} on failure. Never echoes the secret
|
|
42
|
+
access key.
|
|
43
|
+
"""
|
|
44
|
+
# Step 1: Validate account_id before any remote call
|
|
45
|
+
try:
|
|
46
|
+
validated_id = validate_account_id(account_id)
|
|
47
|
+
except ValueError as exc:
|
|
48
|
+
return {"error": str(exc)}
|
|
49
|
+
|
|
50
|
+
# Step 2: Load config; fail fast if missing
|
|
51
|
+
try:
|
|
52
|
+
config = load_config()
|
|
53
|
+
except ConfigError as exc:
|
|
54
|
+
return {"error": str(exc)}
|
|
55
|
+
|
|
56
|
+
# Step 3: Forward to the remote Server
|
|
57
|
+
try:
|
|
58
|
+
creds = vend(validated_id, config.api_key, config.endpoint)
|
|
59
|
+
except RemoteError as exc:
|
|
60
|
+
return {"error": str(exc)}
|
|
61
|
+
|
|
62
|
+
# Step 4: Write credentials to the host file
|
|
63
|
+
try:
|
|
64
|
+
write_profile(
|
|
65
|
+
profile_name=_TARGET_PROFILE,
|
|
66
|
+
access_key_id=creds.access_key_id,
|
|
67
|
+
secret_access_key=creds.secret_access_key,
|
|
68
|
+
session_token=creds.session_token,
|
|
69
|
+
credentials_path=_CREDENTIALS_PATH,
|
|
70
|
+
)
|
|
71
|
+
except PermissionError as exc:
|
|
72
|
+
return {"error": str(exc)}
|
|
73
|
+
except OSError as exc:
|
|
74
|
+
return {"error": f"Failed to write credentials: {exc}"}
|
|
75
|
+
|
|
76
|
+
# Step 5: Return success without echoing the secret
|
|
77
|
+
return {
|
|
78
|
+
"status": "written",
|
|
79
|
+
"profile": _TARGET_PROFILE,
|
|
80
|
+
"expiration": creds.expiration,
|
|
81
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tempura-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP client that vends short-term AWS credentials via stdio transport
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: mcp[cli]
|
|
9
|
+
Requires-Dist: httpx
|
|
10
|
+
|
|
11
|
+
# tempura-client
|
|
12
|
+
|
|
13
|
+
A lightweight MCP (Model Context Protocol) client that vends short-term AWS
|
|
14
|
+
credentials via stdio transport.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install tempura-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
The package provides a `tempura-client` command that communicates over
|
|
25
|
+
stdin/stdout using the MCP JSON-RPC protocol. It is designed to be launched
|
|
26
|
+
by an MCP-aware agent or host application.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
tempura-client
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Dependencies
|
|
33
|
+
|
|
34
|
+
- `mcp[cli]` - Model Context Protocol SDK
|
|
35
|
+
- `httpx` - HTTP client for forwarding requests to the remote server
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/tempura_client/__init__.py
|
|
4
|
+
src/tempura_client/__main__.py
|
|
5
|
+
src/tempura_client/app.py
|
|
6
|
+
src/tempura_client/config.py
|
|
7
|
+
src/tempura_client/remote.py
|
|
8
|
+
src/tempura_client/server.py
|
|
9
|
+
src/tempura_client.egg-info/PKG-INFO
|
|
10
|
+
src/tempura_client.egg-info/SOURCES.txt
|
|
11
|
+
src/tempura_client.egg-info/dependency_links.txt
|
|
12
|
+
src/tempura_client.egg-info/entry_points.txt
|
|
13
|
+
src/tempura_client.egg-info/requires.txt
|
|
14
|
+
src/tempura_client.egg-info/top_level.txt
|
|
15
|
+
src/tempura_shared/__init__.py
|
|
16
|
+
src/tempura_shared/credentials_file.py
|
|
17
|
+
src/tempura_shared/validators.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Credentials file manager.
|
|
2
|
+
|
|
3
|
+
Read and write AWS credentials file profiles in INI format.
|
|
4
|
+
Requirements: 4.1, 4.2, 4.3, 4.4, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 8.3
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import configparser
|
|
8
|
+
import os
|
|
9
|
+
import stat
|
|
10
|
+
|
|
11
|
+
_DEFAULT_CREDENTIALS_PATH = os.path.join(os.path.expanduser("~"), ".aws", "credentials")
|
|
12
|
+
|
|
13
|
+
_SECURE_MODE = stat.S_IRUSR | stat.S_IWUSR # 0o600
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_profile(
|
|
17
|
+
profile_name: str = "MyAwesomeReadOnly",
|
|
18
|
+
credentials_path: str | None = None,
|
|
19
|
+
) -> dict[str, str]:
|
|
20
|
+
"""Read credentials for a named profile from the AWS credentials file.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
profile_name: The profile section name to read.
|
|
24
|
+
credentials_path: Path to the credentials file.
|
|
25
|
+
Defaults to ``~/.aws/credentials``.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Dictionary of key-value pairs from the profile section.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
FileNotFoundError: If the credentials file does not exist.
|
|
32
|
+
KeyError: If the named profile is not found in the file.
|
|
33
|
+
"""
|
|
34
|
+
path = credentials_path or _DEFAULT_CREDENTIALS_PATH
|
|
35
|
+
|
|
36
|
+
if not os.path.isfile(path):
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
f"AWS credentials file not found at {path}. "
|
|
39
|
+
"Please configure your credentials by running "
|
|
40
|
+
"'aws configure' or creating the file manually."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
config = configparser.RawConfigParser()
|
|
44
|
+
config.read(path)
|
|
45
|
+
|
|
46
|
+
if not config.has_section(profile_name):
|
|
47
|
+
raise KeyError(profile_name)
|
|
48
|
+
|
|
49
|
+
return dict(config.items(profile_name))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _check_permissions(path: str) -> None:
|
|
53
|
+
"""Verify the file has secure permissions (mode 600).
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
PermissionError: If the file is group/other readable
|
|
57
|
+
or not writable by the owner.
|
|
58
|
+
"""
|
|
59
|
+
file_stat = os.stat(path)
|
|
60
|
+
mode = stat.S_IMODE(file_stat.st_mode)
|
|
61
|
+
|
|
62
|
+
if mode & (stat.S_IRGRP | stat.S_IROTH):
|
|
63
|
+
raise PermissionError(
|
|
64
|
+
f"Credentials file {path} has insecure permissions "
|
|
65
|
+
f"(mode {oct(mode)}). Expected mode 0o600."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if not os.access(path, os.W_OK):
|
|
69
|
+
raise PermissionError(f"Credentials file {path} is not writable.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_profile(
|
|
73
|
+
profile_name: str,
|
|
74
|
+
access_key_id: str,
|
|
75
|
+
secret_access_key: str,
|
|
76
|
+
session_token: str | None = None,
|
|
77
|
+
credentials_path: str | None = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Write or update a profile in the AWS credentials file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
profile_name: The profile section name to write.
|
|
83
|
+
access_key_id: The AWS access key ID.
|
|
84
|
+
secret_access_key: The AWS secret access key.
|
|
85
|
+
session_token: Optional session token.
|
|
86
|
+
credentials_path: Path to the credentials file.
|
|
87
|
+
Defaults to ``~/.aws/credentials``.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
PermissionError: If the file is not writable or has
|
|
91
|
+
insecure permissions.
|
|
92
|
+
"""
|
|
93
|
+
path = credentials_path or _DEFAULT_CREDENTIALS_PATH
|
|
94
|
+
config = configparser.RawConfigParser()
|
|
95
|
+
file_exists = os.path.exists(path)
|
|
96
|
+
|
|
97
|
+
if file_exists:
|
|
98
|
+
_check_permissions(path)
|
|
99
|
+
config.read(path)
|
|
100
|
+
else:
|
|
101
|
+
parent = os.path.dirname(path)
|
|
102
|
+
if parent:
|
|
103
|
+
os.makedirs(parent, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
if not config.has_section(profile_name):
|
|
106
|
+
config.add_section(profile_name)
|
|
107
|
+
|
|
108
|
+
config.set(profile_name, "aws_access_key_id", access_key_id)
|
|
109
|
+
config.set(
|
|
110
|
+
profile_name,
|
|
111
|
+
"aws_secret_access_key",
|
|
112
|
+
secret_access_key,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if session_token is not None:
|
|
116
|
+
config.set(
|
|
117
|
+
profile_name,
|
|
118
|
+
"aws_session_token",
|
|
119
|
+
session_token,
|
|
120
|
+
)
|
|
121
|
+
elif config.has_option(profile_name, "aws_session_token"):
|
|
122
|
+
config.remove_option(profile_name, "aws_session_token")
|
|
123
|
+
|
|
124
|
+
with open(path, "w") as f:
|
|
125
|
+
config.write(f)
|
|
126
|
+
|
|
127
|
+
if not file_exists:
|
|
128
|
+
os.chmod(path, _SECURE_MODE)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Input validation functions for tempura.
|
|
2
|
+
|
|
3
|
+
Requirements: 2.1, 2.2, 2.3, 2.4
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
_ACCOUNT_ID_PATTERN = re.compile(r"^\d{12}$")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_account_id(account_id: str) -> str:
|
|
12
|
+
"""Validate and return a 12-digit AWS account ID string.
|
|
13
|
+
|
|
14
|
+
Raises ValueError if invalid.
|
|
15
|
+
"""
|
|
16
|
+
if not isinstance(account_id, str):
|
|
17
|
+
raise ValueError(
|
|
18
|
+
"account_id must be a string, " f"got {type(account_id).__name__}"
|
|
19
|
+
)
|
|
20
|
+
if not _ACCOUNT_ID_PATTERN.match(account_id):
|
|
21
|
+
raise ValueError("account_id must be exactly 12 digits, " f"got '{account_id}'")
|
|
22
|
+
return account_id
|