updsts 0.8.9__py3-none-any.whl
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.
- updsts/__init__.py +1 -0
- updsts/__main__.py +71 -0
- updsts/awsutil.py +308 -0
- updsts/cmd_handler.py +81 -0
- updsts/cmdparam.py +90 -0
- updsts/logutil.py +109 -0
- updsts/mcp_impl.py +102 -0
- updsts/mcp_server.py +147 -0
- updsts/py.typed +0 -0
- updsts/upcred.py +177 -0
- updsts-0.8.9.dist-info/METADATA +268 -0
- updsts-0.8.9.dist-info/RECORD +15 -0
- updsts-0.8.9.dist-info/WHEEL +4 -0
- updsts-0.8.9.dist-info/entry_points.txt +2 -0
- updsts-0.8.9.dist-info/licenses/LICENSE +21 -0
updsts/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# encoding: utf-8-sig
|
updsts/__main__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# encoding: utf-8-sig
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
|
|
8
|
+
from .cmdparam import *
|
|
9
|
+
from .cmd_handler import *
|
|
10
|
+
from .logutil import get_logger
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------------------
|
|
13
|
+
def main():
|
|
14
|
+
argp = ArgumentParser(prog="updsts",
|
|
15
|
+
description="Get/Update profile of the aws sts security tokens.")
|
|
16
|
+
# Register common arguments
|
|
17
|
+
common = ArgumentParser(add_help=False)
|
|
18
|
+
common.add_argument(
|
|
19
|
+
'-v',
|
|
20
|
+
'--verbose',
|
|
21
|
+
type=int,
|
|
22
|
+
default=0,
|
|
23
|
+
choices=[0, 1, 2],
|
|
24
|
+
metavar='LEVEL',
|
|
25
|
+
help='Set the verbosity level (0: normal, 1: verbose, 2: debug).'
|
|
26
|
+
)
|
|
27
|
+
user_home = os.path.expanduser("~")
|
|
28
|
+
common.add_argument(
|
|
29
|
+
'-c',
|
|
30
|
+
'--credential_file',
|
|
31
|
+
type=str,
|
|
32
|
+
default=None,
|
|
33
|
+
help='Path to the credetial file where profiles stored. (default: ~/.aws/credentials)'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
subparsers = argp.add_subparsers(dest='command',
|
|
37
|
+
help='Available commands')
|
|
38
|
+
# Register subcommands
|
|
39
|
+
register_sub_get(subparsers, handle_get, parent_parser=common)
|
|
40
|
+
register_sub_list(subparsers, handle_list, parent_parser=common)
|
|
41
|
+
register_sub_mcp(subparsers, handle_mcp, parent_parser=common)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Parse the command line arguments
|
|
45
|
+
args = argp.parse_args()
|
|
46
|
+
# If no command is specified, show help
|
|
47
|
+
if args.command is None:
|
|
48
|
+
argp.print_help()
|
|
49
|
+
else:
|
|
50
|
+
# Initialize the logger with the specified verbosity level
|
|
51
|
+
_ = get_logger(verbose_level=args.verbose)
|
|
52
|
+
# Execute the handler for the specified command
|
|
53
|
+
if hasattr(args, 'handler'):
|
|
54
|
+
args.handler(args)
|
|
55
|
+
else:
|
|
56
|
+
argp.print_help()
|
|
57
|
+
|
|
58
|
+
except FileNotFoundError:
|
|
59
|
+
print("Error: Secrets file not found. Please ensure the file exists or specify a valid path.")
|
|
60
|
+
except PermissionError:
|
|
61
|
+
print("Error: Permission denied when accessing the credential file. Check your permissions.")
|
|
62
|
+
except KeyError as e:
|
|
63
|
+
print(f"Error: Profile name '{e}' is not found in the credential file. Please check the profile name.")
|
|
64
|
+
except ValueError as e:
|
|
65
|
+
print(f"Error: {e}")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print(f"Exception: {e}")
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------------------
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
updsts/awsutil.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# encoding: utf-8-sig
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import boto3
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
|
9
|
+
from configparser import ConfigParser, NoSectionError, NoOptionError
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Dict, Any, Callable
|
|
12
|
+
|
|
13
|
+
from .logutil import get_logger
|
|
14
|
+
from .upcred import CredentialUpdater
|
|
15
|
+
|
|
16
|
+
# ----------------------------------------------------------------------------
|
|
17
|
+
def mask_string(s: str,
|
|
18
|
+
unmask_chars_head: int = 4,
|
|
19
|
+
unmask_chars_tail: int = 4,
|
|
20
|
+
max_strlen = 16) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Mask the middle characters of the input string `s`, leaving head and tail visible.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
s (str): The input string to mask
|
|
26
|
+
unmask_chars_head (int, optional): Number of characters to show at the beginning. Defaults to 4.
|
|
27
|
+
unmask_chars_tail (int, optional): Number of characters to show at the end. Defaults to 4.
|
|
28
|
+
max_strlen (int, optional): Maximum display length before truncation. Defaults to 16.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: Masked string with the following behavior:
|
|
32
|
+
- If string length <= unmask_chars_head + unmask_chars_tail: returns the string as is
|
|
33
|
+
- If string length > max_strlen: shows head + masked chars + tail + ' (total_length chars)'
|
|
34
|
+
- Otherwise: shows head + masked middle + tail
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
mask_string("abc", 4, 4) -> "abc"
|
|
38
|
+
mask_string("abcdefgh", 4, 0) -> "abcd****"
|
|
39
|
+
mask_string("abcdefgh", 4, 2) -> "abcd**gh"
|
|
40
|
+
mask_string("very_long_string_example", 4, 4, 16) -> "very********mple (24 chars)"
|
|
41
|
+
"""
|
|
42
|
+
if not s:
|
|
43
|
+
return ''
|
|
44
|
+
|
|
45
|
+
# Strip whitespace and newlines from input
|
|
46
|
+
s = s.strip()
|
|
47
|
+
str_len = len(s)
|
|
48
|
+
if str_len <= unmask_chars_head + unmask_chars_tail:
|
|
49
|
+
return s
|
|
50
|
+
|
|
51
|
+
tail_part = s[-unmask_chars_tail:] if unmask_chars_tail > 0 else ''
|
|
52
|
+
|
|
53
|
+
# Handle long strings
|
|
54
|
+
if str_len > max_strlen:
|
|
55
|
+
truncate_len = max_strlen
|
|
56
|
+
middle_mask_len = truncate_len - unmask_chars_head - unmask_chars_tail
|
|
57
|
+
if middle_mask_len <= 0:
|
|
58
|
+
return s[:unmask_chars_head] + tail_part + f' ({str_len} chars)'
|
|
59
|
+
masked_part = '*' * middle_mask_len
|
|
60
|
+
return s[:unmask_chars_head] + masked_part + tail_part + f' ({str_len} chars)'
|
|
61
|
+
|
|
62
|
+
# Normal masking for strings within max_strlen
|
|
63
|
+
middle_mask_len = str_len - unmask_chars_head - unmask_chars_tail
|
|
64
|
+
masked_part = '*' * middle_mask_len
|
|
65
|
+
return s[:unmask_chars_head] + masked_part + tail_part
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ----------------------------------------------------------------------------
|
|
69
|
+
def get_credential_file_path(credential_file: str | None = None) -> Path:
|
|
70
|
+
"""
|
|
71
|
+
Get the path to the AWS credentials file.
|
|
72
|
+
Args:
|
|
73
|
+
credential_file (str | None, optional): Path to the AWS credentials file.
|
|
74
|
+
If None, the default location (~/.aws/credentials) is used. Defaults to None.
|
|
75
|
+
Returns:
|
|
76
|
+
credential_file_path (Path): Path object of the credential file.
|
|
77
|
+
"""
|
|
78
|
+
cred_path = credential_file.strip() if credential_file is not None else credential_file
|
|
79
|
+
if not cred_path:
|
|
80
|
+
user_home = os.path.expanduser("~")
|
|
81
|
+
credential_file_path = Path(user_home) / ".aws" / "credentials"
|
|
82
|
+
else:
|
|
83
|
+
credential_file_path = Path(cred_path)
|
|
84
|
+
logger = get_logger()
|
|
85
|
+
logger.debug(f"Using credential file: {credential_file_path}")
|
|
86
|
+
return credential_file_path
|
|
87
|
+
|
|
88
|
+
# ----------------------------------------------------------------------------
|
|
89
|
+
def get_sts_token(profile_name: str,
|
|
90
|
+
totp_token: str,
|
|
91
|
+
duration_seconds: int = 3600,
|
|
92
|
+
credential_file: str | None = None) -> Optional[Dict[str, Any]]:
|
|
93
|
+
"""
|
|
94
|
+
Get temporary STS token using MFA.
|
|
95
|
+
Args:
|
|
96
|
+
profile_name (str): The profile name in the AWS credentials file.
|
|
97
|
+
totp_token (str): The TOTP token of the registerd MFA device.
|
|
98
|
+
credential_file (str | None, optional): Path to the AWS credentials file.
|
|
99
|
+
If None, the default location (~/.aws/credentials) is used. Defaults to None.
|
|
100
|
+
Returns:
|
|
101
|
+
Optional[Dict[str, Any]]: A dictionary containing the temporary STS credentials
|
|
102
|
+
if successful, None otherwise.
|
|
103
|
+
Raises:
|
|
104
|
+
Exception: If there is an error reading the credentials file or obtaining the STS token.
|
|
105
|
+
"""
|
|
106
|
+
logger = get_logger()
|
|
107
|
+
|
|
108
|
+
if totp_token is None or len(totp_token) == 0:
|
|
109
|
+
raise ValueError("TOTP token is required to obtain STS token.")
|
|
110
|
+
|
|
111
|
+
credential_file = get_credential_file_path(credential_file)
|
|
112
|
+
if not credential_file.exists():
|
|
113
|
+
raise FileNotFoundError(f"Credential file '{credential_file}' does not exist.")
|
|
114
|
+
config = ConfigParser()
|
|
115
|
+
config.read(credential_file)
|
|
116
|
+
try:
|
|
117
|
+
access_key = config.get(profile_name, 'aws_access_key_id')
|
|
118
|
+
secret_key = config.get(profile_name, 'aws_secret_access_key')
|
|
119
|
+
mfa_arn = config.get(profile_name, 'mfa_device_arn')
|
|
120
|
+
except (NoOptionError) as e:
|
|
121
|
+
raise Exception(f"Profile '{profile_name}' is missing required options: {e}")
|
|
122
|
+
except (NoSectionError) as e:
|
|
123
|
+
raise Exception(f"Error reading profile '{profile_name}' from credentials file: {e}")
|
|
124
|
+
|
|
125
|
+
logger.debug(f"Using profile '{profile_name}' with access key '{access_key}' and MFA device ARN '{mfa_arn}'")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Read proxy settings from environment variables
|
|
129
|
+
proxies = {}
|
|
130
|
+
http_proxy = os.environ.get('http_proxy') or os.environ.get('HTTP_PROXY')
|
|
131
|
+
https_proxy = os.environ.get('https_proxy') or os.environ.get('HTTPS_PROXY')
|
|
132
|
+
if http_proxy:
|
|
133
|
+
proxies['http'] = http_proxy
|
|
134
|
+
logger.debug(f"Using HTTP proxy: {http_proxy}")
|
|
135
|
+
if https_proxy:
|
|
136
|
+
proxies['https'] = https_proxy
|
|
137
|
+
logger.debug(f"Using HTTPS proxy: {https_proxy}")
|
|
138
|
+
|
|
139
|
+
# Create a session with explicit credentials
|
|
140
|
+
session = boto3.Session(
|
|
141
|
+
aws_access_key_id=access_key,
|
|
142
|
+
aws_secret_access_key=secret_key
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Create STS client with proxy configuration if available
|
|
146
|
+
client_config = {}
|
|
147
|
+
if proxies:
|
|
148
|
+
client_config['proxies'] = proxies
|
|
149
|
+
|
|
150
|
+
sts_client = session.client('sts', **client_config)
|
|
151
|
+
response = sts_client.get_session_token(DurationSeconds=duration_seconds,
|
|
152
|
+
SerialNumber=mfa_arn,
|
|
153
|
+
TokenCode=totp_token)
|
|
154
|
+
credentials = response['Credentials']
|
|
155
|
+
logger.info(f"Successfully obtained temporary STS credentials for profile '{profile_name}'")
|
|
156
|
+
|
|
157
|
+
# Convert expiration time from UTC to local timezone
|
|
158
|
+
expiration_utc = credentials['Expiration']
|
|
159
|
+
# AWS returns naive datetime in UTC, so we need to make it timezone-aware
|
|
160
|
+
if expiration_utc.tzinfo is None:
|
|
161
|
+
expiration_utc = expiration_utc.replace(tzinfo=timezone.utc)
|
|
162
|
+
# Convert to local timezone
|
|
163
|
+
expiration_local = expiration_utc.astimezone()
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
'AccessKeyId': credentials['AccessKeyId'],
|
|
167
|
+
'SecretAccessKey': credentials['SecretAccessKey'],
|
|
168
|
+
'SessionToken': credentials['SessionToken'],
|
|
169
|
+
'Expiration': expiration_local.isoformat()
|
|
170
|
+
}
|
|
171
|
+
except (BotoCoreError, ClientError) as e:
|
|
172
|
+
logger.error(f"Error obtaining STS token: {e}")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
# ----------------------------------------------------------------------------
|
|
176
|
+
def get_profile_info(profile_name: str,
|
|
177
|
+
credential_file: str | None = None,
|
|
178
|
+
secret_mask: bool = False,
|
|
179
|
+
ctx: ConfigParser = None) -> Optional[Dict[str, str]]:
|
|
180
|
+
"""
|
|
181
|
+
Get the credencials from the AWS credentials file for the specified profile.
|
|
182
|
+
Args:
|
|
183
|
+
profile_name (str): The profile name in the AWS credentials file.
|
|
184
|
+
credential_file (str | None, optional): Path to the AWS credentials file.
|
|
185
|
+
If None, the default location (~/.aws/credentials) is used. Defaults to None.
|
|
186
|
+
Returns:
|
|
187
|
+
Optional[Dict[str, str]]: A dictionary containing secret details
|
|
188
|
+
if successful, None otherwise.
|
|
189
|
+
Raises:
|
|
190
|
+
Exception: If there is an error reading the profile from the credentials file.
|
|
191
|
+
"""
|
|
192
|
+
logger = get_logger()
|
|
193
|
+
config = ctx
|
|
194
|
+
if config is None:
|
|
195
|
+
credential_file = get_credential_file_path(credential_file)
|
|
196
|
+
if not credential_file.exists():
|
|
197
|
+
raise FileNotFoundError(f"Credential file '{credential_file}' does not exist.")
|
|
198
|
+
config = ConfigParser()
|
|
199
|
+
config.read(credential_file)
|
|
200
|
+
item_dic = None
|
|
201
|
+
try:
|
|
202
|
+
item_dic = {
|
|
203
|
+
'aws_access_key_id': '(not defined)',
|
|
204
|
+
'aws_secret_access_key': '(not defined)',
|
|
205
|
+
'aws_session_token': '(not defined)',
|
|
206
|
+
'mfa_device_arn': '(not defined)',
|
|
207
|
+
'totp_secret_name': '(not defined)',
|
|
208
|
+
'expiration_datetime': '(not defined)'
|
|
209
|
+
}
|
|
210
|
+
for key, value in config.items(profile_name):
|
|
211
|
+
# Remove whitespace and newlines from log output
|
|
212
|
+
raw_sensitive_value = value.strip() if value else value
|
|
213
|
+
logger.debug(f"Profile '{profile_name}': {key} = {raw_sensitive_value}")
|
|
214
|
+
# Mask secret key and session token if required
|
|
215
|
+
if key == 'aws_access_key_id':
|
|
216
|
+
secret_key_id = mask_string(raw_sensitive_value) if (raw_sensitive_value and secret_mask) else raw_sensitive_value
|
|
217
|
+
item_dic[key] = secret_key_id
|
|
218
|
+
elif key == 'aws_secret_access_key':
|
|
219
|
+
secret_key = mask_string(raw_sensitive_value) if (raw_sensitive_value and secret_mask) else raw_sensitive_value
|
|
220
|
+
item_dic[key] = secret_key
|
|
221
|
+
elif key == 'aws_session_token':
|
|
222
|
+
session_token = mask_string(raw_sensitive_value) if (raw_sensitive_value and secret_mask) else raw_sensitive_value
|
|
223
|
+
item_dic[key] = session_token
|
|
224
|
+
else:
|
|
225
|
+
item_dic[key] = raw_sensitive_value
|
|
226
|
+
item_dic['profile_name'] = profile_name
|
|
227
|
+
return item_dic
|
|
228
|
+
except (NoSectionError) as e:
|
|
229
|
+
logger.error(f"Error reading profile '{profile_name}' from credentials file: {e}")
|
|
230
|
+
except (NoOptionError) as e:
|
|
231
|
+
logger.error(f"Profile '{profile_name}' is missing required options: {e}")
|
|
232
|
+
return item_dic
|
|
233
|
+
|
|
234
|
+
# ----------------------------------------------------------------------------
|
|
235
|
+
def get_profile_list(credential_file: str | None = None,
|
|
236
|
+
secret_mask: bool = False) -> list[dict[str, str]]:
|
|
237
|
+
"""
|
|
238
|
+
Get the list of credencials from the AWS credentials file.
|
|
239
|
+
Args:
|
|
240
|
+
credential_file (str | None, optional): Path to the AWS credentials file.
|
|
241
|
+
If None, the default location (~/.aws/credentials) is used. Defaults to None.
|
|
242
|
+
Returns:
|
|
243
|
+
Optional[list[Dict[str, str]]]: A list of dictionaries containing secret details
|
|
244
|
+
if successful, None otherwise.
|
|
245
|
+
Raises:
|
|
246
|
+
Exception: If there is an error reading the profiles file.
|
|
247
|
+
"""
|
|
248
|
+
logger = get_logger()
|
|
249
|
+
credential_file = get_credential_file_path(credential_file)
|
|
250
|
+
if not credential_file.exists():
|
|
251
|
+
raise FileNotFoundError(f"Credential file '{credential_file}' does not exist.")
|
|
252
|
+
config = ConfigParser()
|
|
253
|
+
config.read(credential_file)
|
|
254
|
+
profiles = []
|
|
255
|
+
for section in config.sections():
|
|
256
|
+
item_dic = get_profile_info(profile_name=section,
|
|
257
|
+
credential_file=credential_file,
|
|
258
|
+
secret_mask=secret_mask,
|
|
259
|
+
ctx=config)
|
|
260
|
+
if item_dic:
|
|
261
|
+
profiles.append(item_dic)
|
|
262
|
+
|
|
263
|
+
if not profiles:
|
|
264
|
+
logger.info("No valid profiles found in the credentials file.")
|
|
265
|
+
else:
|
|
266
|
+
logger.info(f"Found {len(profiles)} profiles in the credentials file.")
|
|
267
|
+
return profiles
|
|
268
|
+
|
|
269
|
+
# ----------------------------------------------------------------------------
|
|
270
|
+
def update_credentials(profile_name: str,
|
|
271
|
+
totp_token: str,
|
|
272
|
+
duration: int = 3600,
|
|
273
|
+
sts_profile_name: str | None = None,
|
|
274
|
+
target_key: str | None = None,
|
|
275
|
+
cred_file: str | os.PathLike | None = None,
|
|
276
|
+
message_handler: Callable[[str], None] | None = None) -> dict[str, str] | None:
|
|
277
|
+
"""
|
|
278
|
+
Update the AWS credentials file with new STS tokens.
|
|
279
|
+
message_handler receives user-facing progress messages (e.g. print for CLI).
|
|
280
|
+
Leave it None in MCP mode so that nothing is written to stdout.
|
|
281
|
+
"""
|
|
282
|
+
logger = get_logger()
|
|
283
|
+
ret = None
|
|
284
|
+
try:
|
|
285
|
+
sts_credentials = get_sts_token(profile_name=profile_name,
|
|
286
|
+
totp_token=totp_token,
|
|
287
|
+
credential_file=cred_file,
|
|
288
|
+
duration_seconds=duration)
|
|
289
|
+
if sts_credentials:
|
|
290
|
+
target_key = profile_name if target_key is None else target_key
|
|
291
|
+
credential_file_path = Path(cred_file) if cred_file else get_credential_file_path()
|
|
292
|
+
updater = CredentialUpdater(credential_path=credential_file_path)
|
|
293
|
+
updater.set_target_tag_name(target_key)
|
|
294
|
+
updater.set_credentials(sts_credentials)
|
|
295
|
+
updater.set_sts_profile_name(sts_profile_name)
|
|
296
|
+
updater.set_message_handler(message_handler)
|
|
297
|
+
ret = updater.update_credential_file()
|
|
298
|
+
# The result may be returned to the LLM via MCP, so never expose the raw access key id.
|
|
299
|
+
if ret and ret.get('aws_access_key_id'):
|
|
300
|
+
ret['aws_access_key_id'] = mask_string(ret['aws_access_key_id'])
|
|
301
|
+
logger.info(f"STS Credentials for profile '{profile_name}' updated successfully.")
|
|
302
|
+
else:
|
|
303
|
+
logger.error("Failed to retrieve STS credentials.")
|
|
304
|
+
raise Exception("Failed to retrieve STS credentials.")
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Error: {e}")
|
|
307
|
+
raise
|
|
308
|
+
return ret
|
updsts/cmd_handler.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# encoding: utf-8-sig
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from argparse import ArgumentParser
|
|
7
|
+
|
|
8
|
+
from .logutil import get_logger
|
|
9
|
+
from .cmdparam import *
|
|
10
|
+
from .awsutil import *
|
|
11
|
+
from .mcp_server import *
|
|
12
|
+
|
|
13
|
+
# ----------------------------------------------------------------------------
|
|
14
|
+
def handle_get(args):
|
|
15
|
+
"""
|
|
16
|
+
Handle the 'get' command to generate a TOTP token for a given secret name.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
args (argparse.Namespace): Parsed command line arguments.
|
|
20
|
+
"""
|
|
21
|
+
profile_name = args.profile_name
|
|
22
|
+
totp_token = args.totp_token
|
|
23
|
+
cred_file = args.credential_file if args.credential_file else None
|
|
24
|
+
sts_profile_name = args.sts_profile_name if hasattr(args, 'sts_profile_name') and args.sts_profile_name else None
|
|
25
|
+
duration = args.duration if args.duration else 3600
|
|
26
|
+
sts_profile_name = args.sts_profile_name if hasattr(args, 'sts_profile_name') and args.sts_profile_name else None
|
|
27
|
+
target_key = args.target_key if hasattr(args, 'target_key') and args.target_key else None
|
|
28
|
+
|
|
29
|
+
ret = update_credentials(profile_name=profile_name,
|
|
30
|
+
totp_token=totp_token,
|
|
31
|
+
duration=duration,
|
|
32
|
+
sts_profile_name=sts_profile_name,
|
|
33
|
+
target_key=target_key,
|
|
34
|
+
cred_file=cred_file,
|
|
35
|
+
message_handler=print)
|
|
36
|
+
if ret:
|
|
37
|
+
print(f"STS Credentials of profile '{profile_name}' updated successfully.")
|
|
38
|
+
print(f"The temporary credential({ret.get('updated_profile_name', '')}) will expire at: {ret.get('aws_token_expiration', '')}")
|
|
39
|
+
|
|
40
|
+
# ----------------------------------------------------------------------------
|
|
41
|
+
def handle_list(args):
|
|
42
|
+
"""
|
|
43
|
+
Handle the 'list' command to display all registered secrets.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
args (argparse.Namespace): Parsed command line arguments.
|
|
47
|
+
"""
|
|
48
|
+
credential_file = args.credential_file if args.credential_file else None
|
|
49
|
+
profiles = get_profile_list(credential_file=credential_file,
|
|
50
|
+
secret_mask=True)
|
|
51
|
+
if not profiles:
|
|
52
|
+
print("No profiles found.")
|
|
53
|
+
else:
|
|
54
|
+
for prof in profiles:
|
|
55
|
+
print(f"Profile Name: {prof['profile_name']}")
|
|
56
|
+
print(f" Access Key ID : {prof['aws_access_key_id']}")
|
|
57
|
+
print(f" Secret Key : {prof['aws_secret_access_key']}")
|
|
58
|
+
print(f" Session Token : {prof['aws_session_token']}")
|
|
59
|
+
print(f" Expiration DateTime : {prof['expiration_datetime']}")
|
|
60
|
+
print(f" MFA Device ARN : {prof['mfa_device_arn']}")
|
|
61
|
+
print(f" TOTP Secret Name : {prof['totp_secret_name']}")
|
|
62
|
+
print()
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------------------
|
|
65
|
+
def handle_mcp(args):
|
|
66
|
+
"""
|
|
67
|
+
Handle the 'mcp' command to run the MCP server.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
args (argparse.Namespace): Parsed command line arguments.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
run_mcp = args.mcp_server if args.mcp_server else False
|
|
74
|
+
if run_mcp:
|
|
75
|
+
# If the MCP server flag is set, run the MCP server
|
|
76
|
+
run_as_mcp_server()
|
|
77
|
+
pass
|
|
78
|
+
else:
|
|
79
|
+
# Otherwise, run the MCP server test
|
|
80
|
+
disp_tools()
|
|
81
|
+
pass
|
updsts/cmdparam.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# encoding: utf-8-sig
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
# ----------------------------------------------------------------------------
|
|
6
|
+
def register_sub_get(subparsers,
|
|
7
|
+
handle_get: callable,
|
|
8
|
+
parent_parser: argparse.ArgumentParser):
|
|
9
|
+
"""
|
|
10
|
+
Register the 'update' subcommand to the argument parser.
|
|
11
|
+
"""
|
|
12
|
+
get_parser = subparsers.add_parser(
|
|
13
|
+
'get',
|
|
14
|
+
help='Get/Update new sts token',
|
|
15
|
+
description='Get/Update new sts token.',
|
|
16
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
17
|
+
parents=[parent_parser]
|
|
18
|
+
)
|
|
19
|
+
get_parser.add_argument(
|
|
20
|
+
'-n',
|
|
21
|
+
'--profile_name',
|
|
22
|
+
type=str,
|
|
23
|
+
required=True,
|
|
24
|
+
help='Profile name to get sts secret key.'
|
|
25
|
+
)
|
|
26
|
+
get_parser.add_argument(
|
|
27
|
+
'-sn',
|
|
28
|
+
'--sts_profile_name',
|
|
29
|
+
type=str,
|
|
30
|
+
required=False,
|
|
31
|
+
default=None,
|
|
32
|
+
help='STS Profile name in the AWS credentials file.'
|
|
33
|
+
)
|
|
34
|
+
get_parser.add_argument(
|
|
35
|
+
'-t',
|
|
36
|
+
'--totp_token',
|
|
37
|
+
type=str,
|
|
38
|
+
required=True,
|
|
39
|
+
help='MFA TOTP token of the user'
|
|
40
|
+
)
|
|
41
|
+
get_parser.add_argument(
|
|
42
|
+
'-d',
|
|
43
|
+
'--duration',
|
|
44
|
+
type=int,
|
|
45
|
+
required=False,
|
|
46
|
+
default=3600,
|
|
47
|
+
help='Duration seconds of the sts token (default: 3600)'
|
|
48
|
+
)
|
|
49
|
+
get_parser.set_defaults(handler=handle_get)
|
|
50
|
+
return subparsers
|
|
51
|
+
|
|
52
|
+
# ----------------------------------------------------------------------------
|
|
53
|
+
def register_sub_list(subparsers,
|
|
54
|
+
handle_list: callable,
|
|
55
|
+
parent_parser: argparse.ArgumentParser):
|
|
56
|
+
"""
|
|
57
|
+
Register the 'list' subcommand to the argument parser.
|
|
58
|
+
"""
|
|
59
|
+
list_parser = subparsers.add_parser(
|
|
60
|
+
'list',
|
|
61
|
+
help='List all profile names',
|
|
62
|
+
description='List all profile names.',
|
|
63
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
64
|
+
parents=[parent_parser]
|
|
65
|
+
)
|
|
66
|
+
# Set the function to handle the 'list' command
|
|
67
|
+
list_parser.set_defaults(handler=handle_list)
|
|
68
|
+
return subparsers
|
|
69
|
+
|
|
70
|
+
# ----------------------------------------------------------------------------
|
|
71
|
+
def register_sub_mcp(subparsers,
|
|
72
|
+
handle_mcp: callable,
|
|
73
|
+
parent_parser: argparse.ArgumentParser):
|
|
74
|
+
"""
|
|
75
|
+
Register the 'mcp' subcommand to the argument parser.
|
|
76
|
+
"""
|
|
77
|
+
mcp_parser = subparsers.add_parser(
|
|
78
|
+
'mcp',
|
|
79
|
+
help='Subcommand for mcp functions',
|
|
80
|
+
description='Manage TOTP secrets using MCP tools.',
|
|
81
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
82
|
+
parents=[parent_parser]
|
|
83
|
+
)
|
|
84
|
+
mcp_parser.add_argument(
|
|
85
|
+
"--mcp-server",
|
|
86
|
+
action="store_true",
|
|
87
|
+
help="Run as MCP server"
|
|
88
|
+
)
|
|
89
|
+
mcp_parser.set_defaults(handler=handle_mcp)
|
|
90
|
+
return subparsers
|
updsts/logutil.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# encoding: utf-8-sig
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
# globals
|
|
10
|
+
logger_obj: Optional[logging.Logger] = None
|
|
11
|
+
is_initialized: bool = False
|
|
12
|
+
|
|
13
|
+
DEFAULT_LOGGER_NAME = "awssts"
|
|
14
|
+
DEFALT_LOG_LEVEL = logging.WARNING
|
|
15
|
+
DEFAULT_FILE_LEVEL = logging.WARNING
|
|
16
|
+
DEFAULT_CONSOLE_LEVEL = logging.WARNING
|
|
17
|
+
|
|
18
|
+
# ----------------------------------------------------------------------------
|
|
19
|
+
def get_with_init(
|
|
20
|
+
log_file: str | None = None,
|
|
21
|
+
*,
|
|
22
|
+
level: int = DEFALT_LOG_LEVEL,
|
|
23
|
+
file_level: int = DEFAULT_FILE_LEVEL,
|
|
24
|
+
console_level: int = DEFAULT_CONSOLE_LEVEL,
|
|
25
|
+
force: bool = False
|
|
26
|
+
) -> logging.Logger:
|
|
27
|
+
"""
|
|
28
|
+
Initialize the logger with the specified configuration.
|
|
29
|
+
If the logger is already initialized, it will return the existing logger
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
log_file (str | None, optional): Defaults to None.
|
|
33
|
+
level (int, optional): Defaults to DEFAULT_LEVEL.
|
|
34
|
+
file_level (int, optional): Defaults to DEFAULT_FILE_LEVEL.
|
|
35
|
+
console_level (int, optional): Defaults to DEFAULT_CONSOLE_LEVEL.
|
|
36
|
+
force (bool, optional): Defaults to False.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
logging.Logger: Logger instance for the mktotp module.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
if log_file is None:
|
|
43
|
+
user_home = os.path.expanduser("~")
|
|
44
|
+
log_dir = Path(user_home) / ".awscm" / "log"
|
|
45
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
46
|
+
log_file = log_dir / "awscm.log"
|
|
47
|
+
else:
|
|
48
|
+
# make the directory for the specified path if it does not exist
|
|
49
|
+
try:
|
|
50
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
51
|
+
except OSError:
|
|
52
|
+
# If the directory cannot be created, we just ignore it
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
global logger_obj, is_initialized
|
|
56
|
+
if not is_initialized:
|
|
57
|
+
logger_obj = logging.getLogger(DEFAULT_LOGGER_NAME)
|
|
58
|
+
logger_obj.setLevel(level=level)
|
|
59
|
+
formatter = logging.Formatter(
|
|
60
|
+
'%(asctime)s [%(name)s] [%(levelname)s] %(message)s'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
65
|
+
file_handler.setFormatter(formatter)
|
|
66
|
+
file_handler.setLevel(file_level)
|
|
67
|
+
logger_obj.addHandler(file_handler)
|
|
68
|
+
except (OSError, PermissionError):
|
|
69
|
+
# If file handler creation fails, we just skip it and only use console handler
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
73
|
+
console_handler.setFormatter(formatter)
|
|
74
|
+
console_handler.setLevel(console_level)
|
|
75
|
+
logger_obj.addHandler(console_handler)
|
|
76
|
+
|
|
77
|
+
is_initialized = True
|
|
78
|
+
return logger_obj
|
|
79
|
+
|
|
80
|
+
# ----------------------------------------------------------------------------
|
|
81
|
+
def get_logger(verbose_level: int | None = None) -> logging.Logger:
|
|
82
|
+
"""
|
|
83
|
+
Module-level shortcut to get the logger.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
logging.Logger: Logger instance for the mktotp module.
|
|
87
|
+
"""
|
|
88
|
+
global logger_obj, is_initialized
|
|
89
|
+
|
|
90
|
+
# If logger is already initialized and no verbose_level is specified,
|
|
91
|
+
# return the existing logger instance
|
|
92
|
+
if is_initialized and verbose_level is None:
|
|
93
|
+
return logger_obj
|
|
94
|
+
|
|
95
|
+
logObj = None
|
|
96
|
+
if verbose_level is not None:
|
|
97
|
+
if verbose_level == 0:
|
|
98
|
+
logObj = get_with_init(level=logging.WARNING)
|
|
99
|
+
elif verbose_level == 1:
|
|
100
|
+
logObj = get_with_init(level=logging.INFO)
|
|
101
|
+
elif verbose_level == 2:
|
|
102
|
+
logObj = get_with_init(level=logging.DEBUG)
|
|
103
|
+
else:
|
|
104
|
+
# If no verbose level is specified, use the default level
|
|
105
|
+
logObj = get_with_init()
|
|
106
|
+
return logObj
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
__all__ = ["get_logger"]
|