Skip to content

Commit a58f15a

Browse files
committed
Merge branch 'master' into dev
2 parents 84b2ee2 + 20246ed commit a58f15a

5 files changed

Lines changed: 44 additions & 1 deletion

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,11 @@ gimme-aws-creds can retrieve temporary Alibaba Cloud RAM credentials using the s
539539

540540
1. **Okta Identity Engine** - Alibaba Cloud support requires the OIE Device Authorization flow. The Okta Classic flow is not supported.
541541
2. **Optional Alibaba Cloud SDK** - Install the optional dependency group:
542+
542543
```bash
543544
pip install "gimme-aws-creds[alicloud]"
544545
```
546+
545547
Without this extra, gimme-aws-creds will refuse to enable Alibaba Cloud and report a clear error pointing to the install command.
546548
3. **Okta application configuration** - In your Okta org, configure:
547549
- An Alibaba Cloud SAML app with the appropriate Alibaba Cloud RAM role mappings.
@@ -614,6 +616,7 @@ The authentication policies on the OIDC Native Application and the AWS/Alibaba C
614616

615617
**`Alibaba Cloud is enabled but optional SDK packages are not installed`**
616618
You set `enable_alicloud = True` (or passed `--enable-alicloud`) but did not install the optional extra. Run:
619+
617620
```bash
618621
pip install "gimme-aws-creds[alicloud]"
619622
```

gimme_aws_creds/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ def update_config_file(self):
325325
okta_username = Okta username
326326
aws_default_duration = Default AWS or Alibaba Cloud session duration in seconds (default: 3600)
327327
preferred_mfa_type = (optional, Okta Classic only) Select this MFA device type automatically
328+
preferred_mfa_factor_id = (optional, Okta Classic only) Pin a specific Okta factor by id (overrides preferred_mfa_type when matched)
328329
include_path - (optional) includes the full role path to the role name for profile
329330
enable_keychain = (optional, Okta Classic only) enable the use of the system keychain to store the user's password
330331
enable_alicloud = (optional, OIE only) y/n - use Native-to-Web SSO scope for Alibaba Cloud RAM
@@ -350,6 +351,7 @@ def update_config_file(self):
350351
'resolve_aws_alias': 'n',
351352
'include_path': 'n',
352353
'preferred_mfa_type': '',
354+
'preferred_mfa_factor_id': '',
353355
'remember_device': 'n',
354356
'aws_default_duration': '3600',
355357
'output_format': 'export',

gimme_aws_creds/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,9 @@ def okta(self):
779779
if self.conf_dict.get('preferred_mfa_provider'):
780780
okta.set_preferred_mfa_provider(self.conf_dict['preferred_mfa_provider'])
781781

782+
if self.conf_dict.get('preferred_mfa_factor_id'):
783+
okta.set_preferred_mfa_factor_id(self.conf_dict['preferred_mfa_factor_id'])
784+
782785
if self.conf_dict.get('duo_universal_factor'):
783786
okta.set_duo_universal_factor(self.conf_dict.get('duo_universal_factor'))
784787

gimme_aws_creds/okta_classic.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __init__(self, gac_ui, okta_org_url, verify_ssl_certs=True, device_token=Non
7676
self._password = None
7777
self._preferred_mfa_type = None
7878
self._preferred_mfa_provider = None
79+
self._preferred_mfa_factor_id = None
7980
self._duo_universal_factor = 'Duo Push'
8081
self._mfa_code = None
8182
self._remember_device = None
@@ -113,6 +114,9 @@ def set_preferred_mfa_type(self, preferred_mfa_type):
113114
def set_preferred_mfa_provider(self, preferred_mfa_provider):
114115
self._preferred_mfa_provider = preferred_mfa_provider
115116

117+
def set_preferred_mfa_factor_id(self, preferred_mfa_factor_id):
118+
self._preferred_mfa_factor_id = preferred_mfa_factor_id
119+
116120
def set_mfa_code(self, mfa_code):
117121
self._mfa_code = mfa_code
118122

@@ -813,6 +817,17 @@ def _choose_factor(self, factors):
813817
else:
814818
preferred_factors = preferred_factors_with_preferred_provider
815819

820+
# Pin to a specific factor by its Okta factor id. Overrides the type/provider
821+
# filters above when matched, since the id uniquely identifies one factor.
822+
if self._preferred_mfa_factor_id is not None:
823+
factor_id_matches = [item for item in factors if item.get('id') == self._preferred_mfa_factor_id]
824+
if factor_id_matches:
825+
preferred_factors = factor_id_matches
826+
else:
827+
self.ui.notify('Preferred factor id {!r} not found among {} enrolled factors.'.format(
828+
self._preferred_mfa_factor_id, len(factors)
829+
))
830+
816831
if len(preferred_factors) == 1:
817832
factor_name = self._build_factor_name(preferred_factors[0])
818833
self.ui.info(factor_name + ' selected')
@@ -827,7 +842,7 @@ def _choose_factor(self, factors):
827842
for i, factor in enumerate(factors):
828843
factor_name = self._build_factor_name(factor)
829844
if factor_name != "":
830-
self.ui.info('[{}] {}'.format(i, factor_name))
845+
self.ui.info('[{}] {} (id: {})'.format(i, factor_name, factor.get('id')))
831846
selection = self._get_user_int_factor_choice(len(factors))
832847

833848
# make sure the choice is valid

tests/test_okta_classic_client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,26 @@ def test_choose_non_number_factor_totp(self, mock_input):
11501150
with self.assertRaises(errors.GimmeAWSCredsExitBase):
11511151
result = self.client._choose_factor(self.factor_list)
11521152

1153+
def test_choose_factor_pinned_by_id(self):
1154+
"""Pinning preferred_mfa_factor_id auto-selects the matching factor without prompting"""
1155+
self.client.set_preferred_mfa_factor_id(self.webauthn_factor['id'])
1156+
result = self.client._choose_factor(self.factor_list)
1157+
self.assertEqual(result, self.webauthn_factor)
1158+
1159+
@patch('builtins.input', return_value='0')
1160+
def test_choose_factor_pinned_by_id_miss_falls_back_to_prompt(self, mock_input):
1161+
"""An unmatched factor id falls through to the interactive picker rather than failing"""
1162+
self.client.set_preferred_mfa_factor_id('no_such_factor_id')
1163+
result = self.client._choose_factor(self.factor_list)
1164+
self.assertEqual(result, self.sms_factor)
1165+
1166+
def test_choose_factor_pinned_by_id_overrides_type(self):
1167+
"""A pinned factor id wins over preferred_mfa_type when both are set"""
1168+
self.client.set_preferred_mfa_type('push')
1169+
self.client.set_preferred_mfa_factor_id(self.webauthn_factor['id'])
1170+
result = self.client._choose_factor(self.factor_list)
1171+
self.assertEqual(result, self.webauthn_factor)
1172+
11531173
def test_build_factor_name_sms(self):
11541174
""" Test building a display name for SMS"""
11551175
result = self.client._build_factor_name(self.sms_factor)

0 commit comments

Comments
 (0)