Skip to content

Commit fe0472e

Browse files
committed
add multi access key funcitionality
1 parent a265f8e commit fe0472e

14 files changed

+285
-152
lines changed

app/aws/credentials.py

+27-20
Original file line numberDiff line numberDiff line change
@@ -62,29 +62,29 @@ def _get_client(profile_name: str, service: str, timeout: int = None, retries: i
6262
return session.client(service)
6363

6464

65-
def has_access_key() -> Result:
65+
def has_access_key(access_key: str) -> Result:
6666
logger.info('has access key')
6767
result = Result()
6868
credentials_file = _load_credentials_file()
6969

70-
if not credentials_file.has_section('access-key'):
71-
error_text = 'could not find profile \'access-key\' in .aws/credentials'
70+
if not credentials_file.has_section(access_key):
71+
error_text = f'could not find access-key \'{access_key}\' in .aws/credentials'
7272
result.error(error_text)
7373
logger.warning(error_text)
7474
return result
7575
result.set_success()
7676
return result
7777

7878

79-
def check_access_key() -> Result:
79+
def check_access_key(access_key: str) -> Result:
8080
logger.info('check access key')
81-
access_key_result = has_access_key()
81+
access_key_result = has_access_key(access_key=access_key)
8282
if not access_key_result.was_success:
8383
return access_key_result
8484

8585
result = Result()
8686
try:
87-
client = _get_client('access-key', 'sts', timeout=2, retries=2)
87+
client = _get_client(access_key, 'sts', timeout=2, retries=2)
8888
client.get_caller_identity()
8989
except ClientError:
9090
error_text = 'access key is not valid'
@@ -123,14 +123,14 @@ def check_session() -> Result:
123123
return result
124124

125125

126-
def fetch_session_token(mfa_token: str) -> Result:
126+
def fetch_session_token(access_key: str, mfa_token: str) -> Result:
127127
result = Result()
128128
credentials_file = _load_credentials_file()
129129
logger.info('fetch session-token')
130130
profile = 'session-token'
131131

132132
try:
133-
secrets = _get_session_token(mfa_token)
133+
secrets = _get_session_token(access_key=access_key, mfa_token=mfa_token)
134134
except ClientError:
135135
error_text = 'could not fetch session token'
136136
result.error(error_text)
@@ -183,11 +183,10 @@ def fetch_role_credentials(user_name: str, profile_group: ProfileGroup) -> Resul
183183

184184
def _remove_unused_profiles(credentials_file, profile_group: ProfileGroup):
185185
used_profiles = profile_group.list_profile_names()
186-
used_profiles.append('access-key')
187186
used_profiles.append('session-token')
188187

189188
for profile in credentials_file.sections():
190-
if profile not in used_profiles:
189+
if profile not in used_profiles and not profile.startswith('access-key'):
191190
credentials_file.remove_section(profile)
192191
return credentials_file
193192

@@ -225,16 +224,24 @@ def _remove_unused_configs(config_file: configparser, profile_group: ProfileGrou
225224
return config_file
226225

227226

228-
def set_access_key(key_id: str, access_key: str) -> None:
227+
def set_access_key(key_name: str, key_id: str, key_secret: str) -> None:
229228
credentials_file = _load_credentials_file()
230-
profile = 'access-key'
231-
if not credentials_file.has_section(profile):
232-
credentials_file.add_section(profile)
233-
credentials_file.set(profile, 'aws_access_key_id', key_id)
234-
credentials_file.set(profile, 'aws_secret_access_key', access_key)
229+
if not credentials_file.has_section(key_name):
230+
credentials_file.add_section(key_name)
231+
credentials_file.set(key_name, 'aws_access_key_id', key_id)
232+
credentials_file.set(key_name, 'aws_secret_access_key', key_secret)
235233
_write_credentials_file(credentials_file)
236234

237235

236+
def get_access_key_list() -> list:
237+
credentials_file = _load_credentials_file()
238+
access_key_list = []
239+
for profile in credentials_file.sections():
240+
if profile.startswith('access-key'):
241+
access_key_list.append(profile)
242+
return access_key_list
243+
244+
238245
def get_access_key_id():
239246
credentials_file = _load_credentials_file()
240247
return credentials_file.get('access-key', 'aws_access_key_id')
@@ -256,8 +263,8 @@ def _add_profile_config(option_file: configparser, profile: str, region: str) ->
256263
option_file.set(config_name, 'output', 'json')
257264

258265

259-
def get_user_name() -> str:
260-
client = _get_client('access-key', 'sts')
266+
def get_user_name(access_key) -> str:
267+
client = _get_client(access_key, 'sts')
261268
identity = client.get_caller_identity()
262269
return _extract_user_from_identity(identity)
263270

@@ -266,8 +273,8 @@ def _extract_user_from_identity(identity):
266273
return identity['Arn'].split('/')[-1]
267274

268275

269-
def _get_session_token(mfa_token) -> dict:
270-
client = _get_client('access-key', 'sts')
276+
def _get_session_token(access_key: str, mfa_token: str) -> dict:
277+
client = _get_client(access_key, 'sts')
271278

272279
identity = client.get_caller_identity()
273280
duration = 43200 # 12 * 60 * 60

app/cli/cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def rotate_access_key(self):
6969
def set_access_key(self):
7070
key_id = getpass(prompt='Key ID: ')
7171
access_key = getpass(prompt='Secret Access Key: ')
72-
self.core.set_access_key(key_id=key_id, access_key=access_key)
72+
self.core.set_access_key(key_id=key_id, key_secret=access_key)
7373
self._info('key was successfully rotated')
7474

7575
@staticmethod

app/core/config.py

+36-4
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,54 @@
22

33
from app.core import files
44

5+
_default_access_key = 'access-key'
6+
57

68
class Config:
79
def __init__(self):
810
self.profile_groups: Dict[ProfileGroup] = {}
11+
self.access_keys: List[str] = [] # TODO is this still needed?
912
self.valid = False
1013
self.error = False
1114

1215
self.mfa_shell_command = None
16+
self.default_access_key = None
1317

1418
def load_from_disk(self):
1519
config = files.load_config()
1620
self.mfa_shell_command = config.get('mfa_shell_command', None)
21+
access_key = config.get('default_access_key', None)
1722

1823
accounts = files.load_accounts()
19-
self.set_accounts(accounts)
24+
self.set_accounts(accounts, access_key)
2025

2126
def save_to_disk(self):
2227
files.save_accounts_file(self.to_dict())
2328
files.save_config_file({
2429
'mfa_shell_command': self.mfa_shell_command,
30+
'default_access_key': self.default_access_key,
2531
})
2632

27-
def set_accounts(self, accounts: dict):
33+
def set_accounts(self, accounts: dict, access_key: str):
34+
if not access_key:
35+
self.default_access_key = _default_access_key
36+
else:
37+
self.default_access_key = access_key
38+
self.access_keys.append(self.default_access_key)
39+
2840
for group_name, group_data in accounts.items():
29-
self.profile_groups[group_name] = ProfileGroup(group_name, group_data)
41+
profile_group = ProfileGroup(name=group_name,
42+
group=group_data,
43+
default_access_key=self.default_access_key)
44+
self.profile_groups[group_name] = profile_group
45+
if profile_group.access_key:
46+
self.access_keys.append(profile_group.access_key)
47+
3048
self.validate()
3149

50+
def set_mfa_shell_command(self, mfa_shell_command: str):
51+
self.mfa_shell_command = mfa_shell_command
52+
3253
def validate(self) -> None:
3354
valid = False
3455
error = ''
@@ -58,11 +79,13 @@ def to_dict(self):
5879

5980

6081
class ProfileGroup:
61-
def __init__(self, name, group: dict):
82+
def __init__(self, name, group: dict, default_access_key: str):
6283
self.name: str = name
6384
self.team: str = group.get('team', None)
6485
self.region: str = group.get('region', None)
6586
self.color: str = group.get('color', None)
87+
self.default_access_key = default_access_key
88+
self.access_key: str = group.get('access_key', None)
6689
self.profiles: List[Profile] = []
6790
self.type: str = group.get("type", "aws") # only aws (default) & gcp as values are allowed
6891

@@ -76,6 +99,8 @@ def validate(self) -> (bool, str):
7699
return False, f'{self.name} has no region'
77100
if not self.color:
78101
return False, f'{self.name} has no color'
102+
if self.access_key and not self.access_key.startswith('access-key'):
103+
return False, f'access-key {self.access_key} must have the prefix \"access-key\"'
79104
if self.type == "aws" and len(self.profiles) == 0:
80105
return False, f'aws "{self.name}" has no profiles'
81106
for profile in self.profiles:
@@ -95,13 +120,20 @@ def list_profile_names(self):
95120
def get_default_profile(self):
96121
return next((profile for profile in self.profiles if profile.default), None)
97122

123+
def get_access_key(self):
124+
if self.access_key:
125+
return self.access_key
126+
return self.default_access_key
127+
98128
def to_dict(self):
99129
result_dict = {
100130
'color': self.color,
101131
'team': self.team,
102132
'region': self.region,
103133
'profiles': [profile.to_dict() for profile in self.profiles],
104134
}
135+
if self.access_key != self.default_access_key:
136+
result_dict['access_key'] = self.access_key
105137
if self.type != "aws":
106138
result_dict["type"] = self.type
107139
return result_dict

app/core/core.py

+26-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import sys
23
from typing import Optional, Callable
34

45
from app.aws import credentials, iam
@@ -16,27 +17,28 @@ def __init__(self):
1617
self.config: Config = Config()
1718
self.config.load_from_disk()
1819
self.active_profile_group: ProfileGroup = None
19-
self.empty_profile_group: ProfileGroup = ProfileGroup('logout', {})
20+
self.empty_profile_group: ProfileGroup = ProfileGroup('logout', {}, '')
2021
self.region_override: str = None
2122

2223
def login(self, profile_group: ProfileGroup, mfa_callback: Callable) -> Result:
2324
result = Result()
2425
logger.info(f'start login {profile_group.name}')
2526
self.active_profile_group = profile_group
27+
access_key = profile_group.get_access_key()
2628

27-
access_key_result = credentials.check_access_key()
29+
access_key_result = credentials.check_access_key(access_key=access_key)
2830
if not access_key_result.was_success:
2931
return access_key_result
3032

3133
session_result = credentials.check_session()
3234
if session_result.was_error:
3335
return session_result
3436
if not session_result.was_success:
35-
renew_session_result = self._renew_session(mfa_callback)
37+
renew_session_result = self._renew_session(access_key=access_key, mfa_callback=mfa_callback)
3638
if not renew_session_result.was_success:
3739
return renew_session_result
3840

39-
user_name = credentials.get_user_name()
41+
user_name = credentials.get_user_name(access_key=access_key)
4042
role_result = credentials.fetch_role_credentials(user_name, profile_group)
4143
if not role_result.was_success:
4244
return role_result
@@ -51,7 +53,6 @@ def login(self, profile_group: ProfileGroup, mfa_callback: Callable) -> Result:
5153
return result
5254

5355
def login_gcp(self, profile_group: ProfileGroup) -> Result:
54-
5556
result = Result()
5657
self.active_profile_group = profile_group
5758
logger.info('gcp login detected')
@@ -129,23 +130,28 @@ def get_profile_group_list(self):
129130
def get_active_profile_color(self):
130131
return self.active_profile_group.color
131132

132-
@staticmethod
133-
def rotate_access_key() -> Result:
133+
def rotate_access_key(self, key_name: str) -> Result:
134134
result = Result()
135135
logger.info('initiate key rotation')
136136
logger.info('check access key')
137-
access_key_result = credentials.check_access_key()
137+
access_key_result = credentials.check_access_key(access_key=key_name)
138138
if not access_key_result.was_success:
139139
return access_key_result
140140

141+
logger.info(f'check if access key {key_name} is in use and can be rotated')
142+
if not self.active_profile_group or self.active_profile_group.get_access_key() != key_name:
143+
result = Result()
144+
result.error(f'Please login with a profile that uses \'{key_name}\' first')
145+
return result
146+
141147
logger.info('check session')
142148
check_session_result = credentials.check_session()
143149
if not check_session_result.was_success:
144150
check_session_result.error('Access Denied. Please log first')
145151
return check_session_result
146152

147153
logger.info('create key')
148-
user = credentials.get_user_name()
154+
user = credentials.get_user_name(key_name)
149155
create_access_key_result = iam.create_access_key(user)
150156
if not create_access_key_result.was_success:
151157
return create_access_key_result
@@ -155,8 +161,9 @@ def rotate_access_key() -> Result:
155161
iam.delete_iam_access_key(user, previous_access_key_id)
156162

157163
logger.info('save key')
158-
credentials.set_access_key(key_id=create_access_key_result.payload['AccessKeyId'],
159-
access_key=create_access_key_result.payload['SecretAccessKey'])
164+
credentials.set_access_key(key_name=key_name,
165+
key_id=create_access_key_result.payload['AccessKeyId'],
166+
key_secret=create_access_key_result.payload['SecretAccessKey'])
160167

161168
result.set_success()
162169
return result
@@ -173,7 +180,7 @@ def edit_config(self, config: Config) -> Result:
173180
result.set_success()
174181
return result
175182

176-
def _renew_session(self, mfa_callback: Callable) -> Result:
183+
def _renew_session(self, access_key: str, mfa_callback: Callable) -> Result:
177184
logger.info('renew session')
178185
logger.info('get mfa from console')
179186
token = mfa.fetch_mfa_token_from_shell(self.config.mfa_shell_command)
@@ -184,7 +191,7 @@ def _renew_session(self, mfa_callback: Callable) -> Result:
184191
result = Result()
185192
result.error('invalid mfa token')
186193
return result
187-
session_result = credentials.fetch_session_token(token)
194+
session_result = credentials.fetch_session_token(access_key=access_key, mfa_token=token)
188195
return session_result
189196

190197
@staticmethod
@@ -193,5 +200,9 @@ def _handle_support_files(profile_group: ProfileGroup):
193200
files.write_active_group_file(profile_group.name)
194201

195202
@staticmethod
196-
def set_access_key(key_id, access_key):
197-
credentials.set_access_key(key_id=key_id, access_key=access_key)
203+
def set_access_key(key_name: str, key_id: str, access_key: str):
204+
credentials.set_access_key(key_name=key_name, key_id=key_id, key_secret=access_key)
205+
206+
@staticmethod
207+
def get_access_key_list():
208+
return credentials.get_access_key_list()

app/core/result.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from datetime import datetime
1+
import logging
2+
3+
logger = logging.getLogger('logsmith')
24

35

46
class Result:
@@ -15,6 +17,6 @@ def add_payload(self, content):
1517
self.payload = content
1618

1719
def error(self, message):
20+
logger.error(message)
1821
self.was_error = True
1922
self.error_message = message
20-
timestamp = datetime.now().strftime('%H:%M:%S')

0 commit comments

Comments
 (0)