awsgetsts 0.1.0__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.
- awsgetsts/__init__.py +11 -0
- awsgetsts/__main__.py +10 -0
- awsgetsts/aws_config.py +224 -0
- awsgetsts/cli.py +608 -0
- awsgetsts/config.py +126 -0
- awsgetsts/lang.py +95 -0
- awsgetsts/logger.py +50 -0
- awsgetsts/mfa.py +10 -0
- awsgetsts/setup.py +491 -0
- awsgetsts/sts.py +79 -0
- awsgetsts-0.1.0.dist-info/METADATA +203 -0
- awsgetsts-0.1.0.dist-info/RECORD +16 -0
- awsgetsts-0.1.0.dist-info/WHEEL +5 -0
- awsgetsts-0.1.0.dist-info/entry_points.txt +2 -0
- awsgetsts-0.1.0.dist-info/licenses/LICENSE +21 -0
- awsgetsts-0.1.0.dist-info/top_level.txt +1 -0
awsgetsts/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""awsgetsts — AWS STS 임시 자격증명 발급 유틸리티.
|
|
2
|
+
|
|
3
|
+
awsgetsts is a small utility that fetches AWS STS session tokens
|
|
4
|
+
(optionally with MFA/TOTP) and writes them to ``~/.aws/config``
|
|
5
|
+
as ``profile sts-<name>`` entries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__all__ = ["__version__"]
|
awsgetsts/__main__.py
ADDED
awsgetsts/aws_config.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""~/.aws/config 관련 유틸 / Helpers for ~/.aws/config.
|
|
2
|
+
|
|
3
|
+
- 프로필 존재 여부 확인 / profile existence checks
|
|
4
|
+
- 토큰 만료 검사 / expiration check for existing sts-<name> profiles
|
|
5
|
+
- INI 파일 병합 저장 / merge-write INI sections
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import configparser
|
|
11
|
+
import datetime
|
|
12
|
+
from datetime import timedelta, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Iterable, List, Optional
|
|
15
|
+
|
|
16
|
+
from dateutil.tz import tzutc
|
|
17
|
+
|
|
18
|
+
from .logger import log
|
|
19
|
+
|
|
20
|
+
AWS_CONFIG_PATH = Path.home() / ".aws" / "config"
|
|
21
|
+
AWS_CREDENTIALS_PATH = Path.home() / ".aws" / "credentials"
|
|
22
|
+
KST = timezone(timedelta(hours=9), name="KST")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def convert_to_kst(dt: datetime.datetime) -> datetime.datetime:
|
|
26
|
+
"""UTC → KST 변환 / Convert to Korea Standard Time."""
|
|
27
|
+
if dt.tzinfo is None:
|
|
28
|
+
dt = dt.replace(tzinfo=tzutc())
|
|
29
|
+
return dt.astimezone(KST)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_ini(path: Path) -> configparser.ConfigParser:
|
|
33
|
+
parser = configparser.ConfigParser()
|
|
34
|
+
if path.exists():
|
|
35
|
+
parser.read(path)
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def list_aws_profiles(
|
|
40
|
+
config_path: Path = AWS_CONFIG_PATH,
|
|
41
|
+
credentials_path: Path = AWS_CREDENTIALS_PATH,
|
|
42
|
+
) -> List[str]:
|
|
43
|
+
"""~/.aws 에 설정된 모든 프로필 이름 반환 / List AWS profiles."""
|
|
44
|
+
profiles: set[str] = set()
|
|
45
|
+
|
|
46
|
+
cfg = _read_ini(config_path)
|
|
47
|
+
for section in cfg.sections():
|
|
48
|
+
name = section.replace("profile ", "") if section != "default" else "default"
|
|
49
|
+
profiles.add(name)
|
|
50
|
+
|
|
51
|
+
creds = _read_ini(credentials_path)
|
|
52
|
+
for section in creds.sections():
|
|
53
|
+
profiles.add(section)
|
|
54
|
+
|
|
55
|
+
return sorted(profiles)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def profile_exists(
|
|
59
|
+
profile_name: str,
|
|
60
|
+
config_path: Path = AWS_CONFIG_PATH,
|
|
61
|
+
credentials_path: Path = AWS_CREDENTIALS_PATH,
|
|
62
|
+
) -> bool:
|
|
63
|
+
"""지정 프로필이 존재하는지 확인 / Check whether a profile is configured."""
|
|
64
|
+
cfg = _read_ini(config_path)
|
|
65
|
+
section = "default" if profile_name == "default" else f"profile {profile_name}"
|
|
66
|
+
if cfg.has_section(section):
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
creds = _read_ini(credentials_path)
|
|
70
|
+
return creds.has_section(profile_name)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def existing_token_valid(
|
|
74
|
+
profile_name: str,
|
|
75
|
+
config_path: Path = AWS_CONFIG_PATH,
|
|
76
|
+
now: Optional[datetime.datetime] = None,
|
|
77
|
+
) -> bool:
|
|
78
|
+
"""기존 sts-<name> 프로필이 아직 유효한지 검사 / Is the stored token still valid?"""
|
|
79
|
+
section = f"profile sts-{profile_name}"
|
|
80
|
+
|
|
81
|
+
cfg = _read_ini(config_path)
|
|
82
|
+
if not cfg.has_section(section):
|
|
83
|
+
log.debug(f"section {section} not found")
|
|
84
|
+
return False
|
|
85
|
+
if not cfg.has_option(section, "expiration"):
|
|
86
|
+
log.debug(f"expiration missing in {section}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
expiration_str = cfg.get(section, "expiration")
|
|
90
|
+
try:
|
|
91
|
+
expiration = datetime.datetime.fromisoformat(expiration_str)
|
|
92
|
+
except ValueError as e:
|
|
93
|
+
log.warn(f"invalid expiration in {section}: {e}")
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
current = now or datetime.datetime.now(tz=expiration.tzinfo)
|
|
97
|
+
kst = convert_to_kst(expiration)
|
|
98
|
+
if current >= expiration:
|
|
99
|
+
# 타임스탬프 없이 강조 / plain print for visibility
|
|
100
|
+
print(f"[{section}] 토큰 만료됨 (만료: {kst}) - 새 토큰 발급 필요")
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
print(f"[{section}] 토큰 유효함 (만료 예정일: {kst})")
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def token_info(
|
|
108
|
+
profile_name: str,
|
|
109
|
+
config_path: Path = AWS_CONFIG_PATH,
|
|
110
|
+
now: Optional[datetime.datetime] = None,
|
|
111
|
+
) -> tuple[str, Optional[datetime.datetime]]:
|
|
112
|
+
"""sts-<name> 토큰 상태 조회 / Inspect stored STS token.
|
|
113
|
+
|
|
114
|
+
Returns ``(state, expiration)`` where state is one of
|
|
115
|
+
``"none"``, ``"invalid"``, ``"expired"``, ``"valid"``.
|
|
116
|
+
"""
|
|
117
|
+
section = f"profile sts-{profile_name}"
|
|
118
|
+
cfg = _read_ini(config_path)
|
|
119
|
+
if not cfg.has_section(section) or not cfg.has_option(section, "expiration"):
|
|
120
|
+
return ("none", None)
|
|
121
|
+
try:
|
|
122
|
+
expiration = datetime.datetime.fromisoformat(cfg.get(section, "expiration"))
|
|
123
|
+
except ValueError:
|
|
124
|
+
return ("invalid", None)
|
|
125
|
+
current = now or datetime.datetime.now(tz=expiration.tzinfo)
|
|
126
|
+
return ("valid" if current < expiration else "expired", expiration)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def write_sts_sections(
|
|
130
|
+
sections: Iterable[tuple[str, dict]],
|
|
131
|
+
config_path: Path = AWS_CONFIG_PATH,
|
|
132
|
+
add_cli_pager: bool = True,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""STS 세션을 ~/.aws/config 에 병합 저장 / Merge STS sections into ~/.aws/config.
|
|
135
|
+
|
|
136
|
+
``sections`` yields ``(section_name, key_value_dict)``. The section
|
|
137
|
+
name is written verbatim (e.g. ``"profile sts-dev"``). When
|
|
138
|
+
``add_cli_pager`` is True, ``cli_pager = `` is appended to each
|
|
139
|
+
written section (disables the AWS CLI pager).
|
|
140
|
+
"""
|
|
141
|
+
existing = _read_ini(config_path)
|
|
142
|
+
|
|
143
|
+
for section_name, kv in sections:
|
|
144
|
+
if not existing.has_section(section_name):
|
|
145
|
+
existing.add_section(section_name)
|
|
146
|
+
for key, value in kv.items():
|
|
147
|
+
existing.set(section_name, key, value)
|
|
148
|
+
if add_cli_pager:
|
|
149
|
+
existing.set(section_name, "cli_pager", "")
|
|
150
|
+
|
|
151
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
153
|
+
existing.write(f)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def list_sts_sections(config_path: Path = AWS_CONFIG_PATH) -> List[str]:
|
|
157
|
+
"""~/.aws/config 의 'profile sts-*' 섹션 이름 목록 / List sts-* sections."""
|
|
158
|
+
parser = _read_ini(config_path)
|
|
159
|
+
return [s for s in parser.sections() if s.startswith("profile sts-")]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def remove_sts_sections(
|
|
163
|
+
section_names: Iterable[str],
|
|
164
|
+
config_path: Path = AWS_CONFIG_PATH,
|
|
165
|
+
) -> int:
|
|
166
|
+
"""지정된 섹션들을 ~/.aws/config 에서 제거 / Remove sections; return count."""
|
|
167
|
+
if not config_path.exists():
|
|
168
|
+
return 0
|
|
169
|
+
parser = _read_ini(config_path)
|
|
170
|
+
removed = 0
|
|
171
|
+
for name in section_names:
|
|
172
|
+
if parser.has_section(name):
|
|
173
|
+
parser.remove_section(name)
|
|
174
|
+
removed += 1
|
|
175
|
+
if removed:
|
|
176
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
177
|
+
parser.write(f)
|
|
178
|
+
return removed
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def upsert_base_profile(
|
|
182
|
+
profile_name: str,
|
|
183
|
+
access_key_id: str,
|
|
184
|
+
secret_access_key: str,
|
|
185
|
+
region: str = "ap-northeast-2",
|
|
186
|
+
output: str = "json",
|
|
187
|
+
config_path: Path = AWS_CONFIG_PATH,
|
|
188
|
+
credentials_path: Path = AWS_CREDENTIALS_PATH,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""base AWS 프로필 신규/갱신 / Create or update a base AWS profile.
|
|
191
|
+
|
|
192
|
+
~/.aws/credentials 에는 access_key / secret_key,
|
|
193
|
+
~/.aws/config 에는 region / output 을 저장합니다.
|
|
194
|
+
Writes access keys to ~/.aws/credentials and region/output to ~/.aws/config.
|
|
195
|
+
Both files are chmod'd to 0600.
|
|
196
|
+
"""
|
|
197
|
+
creds = _read_ini(credentials_path)
|
|
198
|
+
if not creds.has_section(profile_name):
|
|
199
|
+
creds.add_section(profile_name)
|
|
200
|
+
creds.set(profile_name, "aws_access_key_id", access_key_id)
|
|
201
|
+
creds.set(profile_name, "aws_secret_access_key", secret_access_key)
|
|
202
|
+
|
|
203
|
+
credentials_path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
with credentials_path.open("w", encoding="utf-8") as f:
|
|
205
|
+
creds.write(f)
|
|
206
|
+
try:
|
|
207
|
+
credentials_path.chmod(0o600)
|
|
208
|
+
except OSError as e:
|
|
209
|
+
log.warn(f"chmod 0600 failed for {credentials_path}: {e}")
|
|
210
|
+
|
|
211
|
+
cfg = _read_ini(config_path)
|
|
212
|
+
section = "default" if profile_name == "default" else f"profile {profile_name}"
|
|
213
|
+
if not cfg.has_section(section):
|
|
214
|
+
cfg.add_section(section)
|
|
215
|
+
cfg.set(section, "region", region)
|
|
216
|
+
cfg.set(section, "output", output)
|
|
217
|
+
|
|
218
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
220
|
+
cfg.write(f)
|
|
221
|
+
try:
|
|
222
|
+
config_path.chmod(0o600)
|
|
223
|
+
except OSError as e:
|
|
224
|
+
log.warn(f"chmod 0600 failed for {config_path}: {e}")
|