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 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
@@ -0,0 +1,10 @@
1
+ """``python -m awsgetsts`` 엔트리 / Entry point for module invocation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -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}")