diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf2bbb7..5868d66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,15 +15,11 @@ jobs: strategy: matrix: platform: [ ubuntu-latest, macos-latest, windows-latest ] - python-version: [ 3.6, 3.7, 3.8, 3.9, "3.10" ] - exclude: - - platform: macos-latest + python-version: [ 3.8, 3.9, "3.10" ] + include: + - platform: ubuntu-20.04 python-version: 3.6 - - platform: macos-latest - python-version: 3.7 - - platform: windows-latest - python-version: 3.6 - - platform: windows-latest + - platform: ubuntu-latest python-version: 3.7 steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 5cb6157..ee8f801 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,6 @@ target/ .DS_Store # VS Code -.vscode/ \ No newline at end of file +.vscode/ + +.venv \ No newline at end of file diff --git a/README.rst b/README.rst index e701008..1e562ee 100644 --- a/README.rst +++ b/README.rst @@ -126,40 +126,42 @@ Additional variables can also be passed to aws-okta-processors ``authenticate`` as options or environment variables as outlined in the table below. ============= =============== ====================== ======================================== -Variable Option Environment Variable Description -============= =============== ====================== ======================================== -user --user AWS_OKTA_USER Okta user name -------------- --------------- ---------------------- ---------------------------------------- -password --pass AWS_OKTA_PASS Okta user password -------------- --------------- ---------------------- ---------------------------------------- -organization --organization AWS_OKTA_ORGANIZATION Okta FQDN for Organization -------------- --------------- ---------------------- ---------------------------------------- -application --application AWS_OKTA_APPLICATION Okta AWS application URL -------------- --------------- ---------------------- ---------------------------------------- -role --role AWS_OKTA_ROLE AWS Role ARN -------------- --------------- ---------------------- ---------------------------------------- -account_alias --account-alias AWS_OKTA_ACCOUNT_ALIAS AWS Account Filter -------------- --------------- ---------------------- ---------------------------------------- -region --region AWS_OKTA_REGION AWS Region -------------- --------------- ---------------------- ---------------------------------------- -duration --duration AWS_OKTA_DURATION Duration in seconds for AWS session -------------- --------------- ---------------------- ---------------------------------------- -key --key AWS_OKTA_KEY Key used in generating AWS session cache -------------- --------------- ---------------------- ---------------------------------------- -environment --environment Output command to set ENV variables -------------- --------------- ---------------------- ---------------------------------------- -silent --silent Silence Info output -------------- --------------- ---------------------- ---------------------------------------- -factor --factor AWS_OKTA_FACTOR MFA type. `push:okta`, `token:software:totp:okta`, `token:software:totp:google` and `token:hardware:yubico` are supported. -------------- --------------- ---------------------- ---------------------------------------- -no_okta_cache --no-okta-cache AWS_OKTA_NO_OKTA_CACHE Do not read okta cache -------------- --------------- ---------------------- ---------------------------------------- -no_aws_cache --no-aws-cache AWS_OKTA_NO_AWS_CACHE Do not read aws cache -------------- --------------- ---------------------- ---------------------------------------- -target_shell --target-shell AWS_OKTA_TARGET_SHELL Target shell to format export command -------------- --------------- ---------------------- ---------------------------------------- -sign_in_url --sign-in-url AWS_OKTA_SIGN_IN_URL AWS Sign In URL -============= =============== ====================== ======================================== +Variable Option Environment Variable Description +============== ================ ======================= ======================================== +user --user AWS_OKTA_USER Okta user name +-------------- ---------------- ----------------------- ---------------------------------------- +password --pass AWS_OKTA_PASS Okta user password +-------------- ---------------- ----------------------- ---------------------------------------- +organization --organization AWS_OKTA_ORGANIZATION Okta FQDN for Organization +-------------- ---------------- ----------------------- ---------------------------------------- +application --application AWS_OKTA_APPLICATION Okta AWS application URL +-------------- ---------------- ----------------------- ---------------------------------------- +role --role AWS_OKTA_ROLE AWS Role ARN +-------------- ---------------- ----------------------- ---------------------------------------- +secondary_role --secondary-role AWS_OKTA_SECONDARY_ROLE Secondary AWS Role ARN +-------------- ---------------- ----------------------- ---------------------------------------- +account_alias --account-alias AWS_OKTA_ACCOUNT_ALIAS AWS Account Filter +-------------- ---------------- ----------------------- ---------------------------------------- +region --region AWS_OKTA_REGION AWS Region +-------------- ---------------- ----------------------- ---------------------------------------- +duration --duration AWS_OKTA_DURATION Duration in seconds for AWS session +-------------- ---------------- ----------------------- ---------------------------------------- +key --key AWS_OKTA_KEY Key used in generating AWS session cache +-------------- ---------------- ----------------------- ---------------------------------------- +environment --environment Output command to set ENV variables +-------------- ---------------- ----------------------- ---------------------------------------- +silent --silent Silence Info output +-------------- ---------------- ----------------------- ---------------------------------------- +factor --factor AWS_OKTA_FACTOR MFA type. `push:okta`, `token:software:totp:okta`, `token:software:totp:google` and `token:hardware:yubico` are supported. +-------------- ---------------- ----------------------- ---------------------------------------- +no_okta_cache --no-okta-cache AWS_OKTA_NO_OKTA_CACHE Do not read okta cache +-------------- ---------------- ----------------------- ---------------------------------------- +no_aws_cache --no-aws-cache AWS_OKTA_NO_AWS_CACHE Do not read aws cache +-------------- ---------------- ----------------------- ---------------------------------------- +target_shell --target-shell AWS_OKTA_TARGET_SHELL Target shell to format export command +-------------- ---------------- ----------------------- ---------------------------------------- +sign_in_url --sign-in-url AWS_OKTA_SIGN_IN_URL AWS Sign In URL +============== ================ ======================= ======================================== ^^^^^^^^ Examples @@ -213,6 +215,16 @@ To clear all AWS session caches run:: $ rm ~/.aws/boto/cache/* +------------------------- +Assuming a Secondary Role +------------------------- + +If you can only assume a role from another role, you can assume both roles using ``--role`` and ``--secondary-role``. Use +``--role`` to specify the first role ARN, then ``--secondary-role`` to specify the role ARN assumed from ``--role``. + +Example:: + + aws-okta-processor authenticate --user jdoe ... --role arn:aws:iam::111111111:role/OpsUser --secondary-role arn:aws:iam::111111111:role/SecretsAdmin ----------------------------- Project or User Configuration diff --git a/aws_okta_processor/commands/authenticate.py b/aws_okta_processor/commands/authenticate.py index 2f2e7d6..eaaf989 100644 --- a/aws_okta_processor/commands/authenticate.py +++ b/aws_okta_processor/commands/authenticate.py @@ -12,6 +12,7 @@ -o , --organization= Okta organization domain. -a , --application= Okta application url. -r , --role= AWS role ARN. + --secondary-role Secondary AWS role ARN. -R , --region= AWS region name. -U , --sign-in-url= AWS Sign In URL. [default: https://signin.aws.amazon.com/saml] @@ -52,6 +53,7 @@ "--organization": "AWS_OKTA_ORGANIZATION", "--application": "AWS_OKTA_APPLICATION", "--role": "AWS_OKTA_ROLE", + "--secondary-role": "AWS_OKTA_SECONDARY_ROLE", "--region": "AWS_OKTA_REGION", "--sign-in-url": "AWS_OKTA_SIGN_IN_URL", "--duration": "AWS_OKTA_DURATION", @@ -71,6 +73,7 @@ "AWS_OKTA_ORGANIZATION": "organization", "AWS_OKTA_APPLICATION": "application", "AWS_OKTA_ROLE": "role", + "AWS_OKTA_SECONDARY_ROLE": "secondary-role", "AWS_OKTA_REGION": "region", "AWS_OKTA_SIGN_IN_URL": "sign_in_url", "AWS_OKTA_DURATION": "duration", diff --git a/aws_okta_processor/core/fetcher.py b/aws_okta_processor/core/fetcher.py index d9474d8..4f20f7a 100644 --- a/aws_okta_processor/core/fetcher.py +++ b/aws_okta_processor/core/fetcher.py @@ -133,7 +133,7 @@ def _get_credentials(self): region_name=self._configuration["AWS_OKTA_REGION"] ) - aws_roles, saml_assertion, _application_url, _user, _organization = self._get_app_roles() + aws_roles, saml_assertion, _application_url, user, _organization = self._get_app_roles() aws_role = prompt.get_item( items=aws_roles, @@ -153,9 +153,27 @@ def _get_credentials(self): DurationSeconds=int(self._configuration["AWS_OKTA_DURATION"]) ) + if self._configuration.get('AWS_OKTA_SECONDARY_ROLE', None) is not None: + role_session_name = user + secondary_role_arn = self._configuration['AWS_OKTA_SECONDARY_ROLE'] + + print_tty(f"Assuming secondary role {secondary_role_arn}") + credentials = response['Credentials'] + client = boto3.client( + 'sts', + aws_access_key_id=credentials['AccessKeyId'], + aws_secret_access_key=credentials["SecretAccessKey"], + aws_session_token=credentials["SessionToken"], + region_name=self._configuration["AWS_OKTA_REGION"], + ) + response = client.assume_role( + RoleArn=secondary_role_arn, + DurationSeconds=int(self._configuration["AWS_OKTA_DURATION"]), + RoleSessionName=role_session_name, + ) + expiration = (response['Credentials']['Expiration'] .isoformat().replace("+00:00", "Z")) - response['Credentials']['Expiration'] = expiration return response diff --git a/tests/core/test_fetcher.py b/tests/core/test_fetcher.py index 234617d..7f1a149 100644 --- a/tests/core/test_fetcher.py +++ b/tests/core/test_fetcher.py @@ -156,3 +156,74 @@ def assume_role_side_effect(*args, **kwargs): call('[ 3 ] Role-One', indents=1), call('Selection: ', newline=False) ]) + + @patch("boto3.client") + @patch('aws_okta_processor.core.fetcher.print_tty') + @patch('aws_okta_processor.core.fetcher.prompt.print_tty') + @patch('aws_okta_processor.core.fetcher.prompt.input_tty', return_value='1') + @patch('aws_okta_processor.core.fetcher.Okta') + def test_fetcher_should_assume_secondary_role( + self, + mock_okta, + mock_prompt, + mock_prompt_print_tty, + mock_print_tty, + mock_client + ): + + self.OPTIONS["--secondary-role"] = "arn:aws:iam::1:role/Role-Two" + + def assume_role_saml_side_effect(*args, **kwargs): + if kwargs['RoleArn'] == 'arn:aws:iam::1:role/Role-One': + return { + 'Credentials': { + 'AccessKeyId': 'test-key1', + 'SecretAccessKey': 'test-secret1', + 'SessionToken': 'test-token1', + 'Expiration': datetime(2020, 4, 17, 12, 0, 0, 0) + } + } + raise RuntimeError('invalid RoleArn') + + def assume_role_side_effect(*args, **kwargs): + if kwargs['RoleArn'] == 'arn:aws:iam::1:role/Role-Two': + return { + 'Credentials': { + 'AccessKeyId': 'test-key2', + 'SecretAccessKey': 'test-secret2', + 'SessionToken': 'test-token2', + 'Expiration': datetime(2020, 4, 17, 13, 0, 0, 0) + } + } + raise RuntimeError('invalid RoleArn') + + self.OPTIONS["--pass"] = 'testpass' + + mock_c = mock.Mock() + mock_c.assume_role_with_saml.side_effect = assume_role_saml_side_effect + mock_c.assume_role.side_effect = assume_role_side_effect + mock_okta().get_saml_response.return_value = SAML_RESPONSE + mock_client.return_value = mock_c + + authenticate = Authenticate(self.OPTIONS) + fetcher = SAMLFetcher(authenticate, cache={}) + + creds = fetcher.fetch_credentials() + self.assertDictEqual({ + 'AccessKeyId': 'test-key2', + 'Expiration': '2020-04-17T13:00:00', + 'SecretAccessKey': 'test-secret2', + 'SessionToken': 'test-token2' + }, creds) + + self.assertEqual(7, mock_prompt_print_tty.call_count) + + MagicMock.assert_has_calls(mock_prompt_print_tty, [ + call('Select AWS Role:'), + call('Account: 1', indents=0), + call('[ 1 ] Role-One', indents=1), + call('[ 2 ] Role-Two', indents=1), + call('Account: 2', indents=0), + call('[ 3 ] Role-One', indents=1), + call('Selection: ', newline=False) + ])