├── .gitignore
├── .pylintrc
├── .travis.yml
├── LICENSE
├── README.md
├── RELEASE.md
├── lint.sh
├── okta_aws.sh
├── okta_aws
├── __init__.py
├── __main__.py
├── exceptions.py
└── okta_aws.py
├── release.sh
├── setup.cfg
├── setup.py
└── tests
├── __init__.py
├── conftest.py
├── data
├── .gitignore
├── okta_auth_password_expired.json
├── okta_auth_success.json
├── okta_aws.toml
├── saml_assertion.txt
├── saml_assertion_decoded.txt
├── saml_assertion_page.html
└── sts_creds.json
├── test_aws_assume_role.py
├── test_friendly_interval.py
├── test_get_arns.py
├── test_get_saml_assertion.py
└── test_log_in_to_okta.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *,cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 |
93 | # Rope project settings
94 | .ropeproject
95 |
96 | # mkdocs documentation
97 | /site
98 |
99 | # pytest
100 | .pytest_cache
101 |
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 | [MESSAGES CONTROL]
2 |
3 | disable=no-self-use
4 |
5 | # Good variable names which should always be accepted, separated by a comma
6 | good-names=i, # Indices
7 | j,
8 | k,
9 | fh, # File handle
10 | m, # Regex match
11 | v, # Value (used with k for key)
12 | kv, # Key/Value pairs
13 | r, # Result from requests.get
14 | e, # exception
15 | _
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.6"
4 | install:
5 | - pip install -e .
6 | - pip install pytest-datadir
7 | script:
8 | - pytest
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Okta AWS tool
2 |
3 | [](https://travis-ci.org/chef/okta_aws)
4 |
5 | This tool is for accessing the AWS API for AWS accounts you normally log
6 | into via okta. Normally, when you log in to an account via okta, you are
7 | assigned an IAM role, and don't have an actual user within AWS. This means you
8 | don't have any API keys you can use to access the AWS API via the command
9 | line.
10 |
11 | This tool will prompt you for your Okta credentials, and generate temporary
12 | API keys you can use to access the AWS API with.
13 |
14 | ## Installation
15 |
16 | Okta_aws requires Python 3 to run.
17 |
18 | ### macOS
19 |
20 | If you have [Homebrew](https://brew.sh), you can use it to install `okta_aws`:
21 |
22 | brew tap chef/okta_aws
23 | brew install okta_aws
24 |
25 | To do the same thing in one step, you can run:
26 |
27 | brew install chef/okta_aws/okta_aws
28 |
29 | ### Linux (Ubuntu)
30 |
31 | Due to a [bug](https://github.com/aws/aws-cli/issues/3820) in the
32 | version of the AWS CLI found in Ubuntu packages, you will need to use
33 | the AWS CLI installed via `pip` instead (`okta_aws` currently
34 | interacts with the AWS API via the CLI).
35 |
36 | Once you have installed the newer AWS CLI, you can install `okta_aws`
37 | using `pip`:
38 |
39 | sudo apt-get remove awscli
40 | sudo apt-get install python3 python3-pip
41 | sudo python3 -m pip install --upgrade awscli
42 | sudo python3 -m pip install --upgrade okta_aws
43 |
44 |
45 | ### Pip
46 |
47 | Alternatively, you can install via `pip`:
48 |
49 | pip install okta_aws
50 |
51 | ### Source
52 |
53 | To install from source, clone this repository and run:
54 |
55 | python setup.py install
56 |
57 | ## Setup
58 |
59 | Make a file in your home directory called `.okta_aws.toml`:
60 |
61 | [general]
62 | username="yourusername"
63 | okta_server="yourcompany.okta.com"
64 |
65 | The values are as follows:
66 |
67 | * `username`: your okta username.
68 | * `okta_server`: the okta domain your company uses. It is usually something
69 | like `yourcompanyname.okta.com`.
70 |
71 | There are some optional settings too:
72 |
73 | * `short_profile_names` - okta_aws will fetch a list of AWS accounts you have
74 | been assigned directly from okta, and will use the name in okta as the
75 | profile name referred to by the AWS tools. However, the name of the
76 | application in okta is often verbose. With this option turned on (it
77 | defaults to true), the profile names will be shortened into something easier
78 | to type. For example 'MyCompany Engineering (all devs) AWS' will become
79 | `mycompany-engineering`. The exact rules are:
80 | * Any trailing 'AWS' suffix, if present, is removed
81 | * Anything in parentheses is stripped
82 | * Everything is converted to lowercaase
83 | * Spaces are stripped and replaced with dashes
84 | * `cookie_file` - the location where the okta session cookie is stored. This
85 | defaults to `~/.okta_aws_cookie`.
86 | * `session_duration` - How long to request that the AWS temporary credentials
87 | should be valid for. This defaults to `3600` (1 hour), but you can choose a
88 | shorter or longer value up to `43200` (12 hours).
89 | * Note: in order to choose a session length longer than 1 hour, you need to
90 | configure the role in AWS to allow longer sessions. In the IAM console,
91 | find the role and exit the `Maximum CLI/API session duration` setting.
92 | * `role_arn` - the ARN or name of the role to assume. This only needs to be
93 | set if you have more than one role and are prompted to select which role to
94 | assume when you run okta_aws.
95 |
96 | Each of these settings can be set per-profile. To do this, create a new
97 | section in the configuration file with the name of the profile, and put your
98 | per-profile settings here. For example, in order to use a longer session
99 | length for the `mycompany-dev` profile, add this to your `~/.okta_aws.toml`
100 | file:
101 |
102 | ```
103 | [mycompany-dev]
104 | session_duration = 43200
105 | ```
106 |
107 | Or, if you are prompted when logging into the `mycompany-staging` profile
108 | which role you want to use, then you can configure a default role as follows:
109 |
110 | ```
111 | [mycompany-staging]
112 | role_arn = "Okta_PowerUser"
113 | # Alternatively you can specify the full ARN
114 | # role_arn = "arn:aws:iam::1234567890:role/Okta_PowerUser"
115 | ```
116 |
117 | ### Profile aliases
118 |
119 | Sometimes, the profile name obtained from okta doesn't match the profile name
120 | you want to use in your AWS credentials file (for example, you might have a
121 | specific profile name hardcoded in scripts). In these cases, you can configure
122 | a profile name to be an _alias_ of another. To do this, add an `[aliases]`
123 | section to your `.okta_aws.toml` file. For example, if `okta_aws --list` shows
124 | an available profile of `companyname-engineering` but you have
125 | `engineering` configured as a profile name in your scripts, you can do:
126 |
127 | ```
128 | [aliases]
129 | engineering = "companyname-engineering"
130 | ```
131 |
132 | Then, you just set `AWS_PROFILE` to `engineering`, or pass `engineering` as an
133 | argument to okta_aws, and it will log in with the `companyname-engineering`
134 | profile, while storing the credentials in an `engineering` profile in your
135 | `~/.aws/credentials` file.
136 |
137 | If you want to configure profile specific settings for a profile that has an
138 | alias, you can configure them under either the profile name itself, or the
139 | alias. If you configure the settings under the 'real' name of the profile,
140 | then those settings will also be used if you refer to the profile by its
141 | alias. If you configure them under the alias, then they will only take effect
142 | if you refer to the profile by its alias.
143 |
144 | ### GovCloud
145 |
146 | If the profile name includes 'govcloud', then okta_aws will use the appropriate
147 | region for fetching govcloud credentials (us-gov-east-1).
148 |
149 | ## Usage
150 |
151 | Run `okta_aws PROFILENAME`, or run `okta_aws` without any arguments and
152 | okta_aws will use the `AWS_PROFILE` environment variable if you have it set.
153 |
154 | To fetch credentials for all profiles you have access to, run `okta_aws --all`.
155 |
156 | To list the available profiles, run `okta_aws --list`.
157 |
158 | The first time you run `okta_aws`, you will be prompted for your okta username
159 | and password. On subsequent runs, if you are still logged into okta and your
160 | session hasn't expired, then you won't have to log in again.
161 |
162 | Once you have entered your okta username and password, a temporary token will
163 | be obtained for you and stored in your AWS credentials file. You can then use
164 | the aws api as normal, passing in the profile name you gave to okta_aws.
165 |
166 | If you have been assigned multiple possible roles when the aws account was set
167 | up for you in okta, then you will be prompted which one of them you want to
168 | use (e.g. Okta_PowerUser or Okta_AdminAccess). Select which one you want from
169 | the menu if prompted. You can also configure a default role to assume in
170 | `~/.okta_aws.toml`.
171 |
172 | The AWS token you receive will only last for an hour. To get a new token,
173 | re-run okta_aws.
174 |
175 | ### Automatically refreshing the token
176 |
177 | You can run okta_aws a second time to retrieve a new token before the old one
178 | expires. If you wish, you can run one of the following to automatically refresh
179 | the token once every 55 minutes (allowing some grace period before the token
180 | expires):
181 |
182 | while true; do okta_aws PROFILENAME; sleep 3300; done
183 | while true; do okta_aws --all; sleep 3300; done
184 |
185 | okta_aws will run without prompting for anything one you have logged in and,
186 | if necessary, configured a default profile in `~/.okta_aws.toml`.
187 |
188 | Users of zsh may find this `oh-my-zsh` plugin useful for shell-integrated
189 | auto-refresh: [okta-aws plugin for zsh](https://gist.github.com/irvingpop/8e4e3bc63497be3432e695a52ef885f0)
190 |
191 | ## Troubleshooting
192 |
193 | If you're having issues running okta_aws, there's a few things to check:
194 | `okta_aws --version` - should match most recent
195 | `python --version` - should be 3.X
196 |
197 | We also recently saw someone where the error looked like this, user had environment variables set that put python 3.6 directories first in the PATH, when python 3.7 was installed. Removing those lines from `.bash_profile` fixed the issue:
198 | `-bash: /Library/Frameworks/Python.framework/Versions/3.6/bin/okta_aws: /usr/local/Cellar/okta_aws/0.5.3/libexec/bin/python3.7: bad interpreter: No such file or directory`
199 |
200 | ## Similar projects
201 |
202 | * [okta-aws-cli-assume-role](https://github.com/oktadeveloper/okta-aws-cli-assume-role) - Okta's own tool
203 | * [oktaauth](https://github.com/ThoughtWorksInc/oktaauth) - Python library for working with okta
204 | * [segmentio/aws-okta](https://github.com/segmentio/aws-okta) - uses assumerole
205 | to connect to multiple aws accounts while signing into a primary aws account
206 | in okta.
207 | * [aws-vault](https://github.com/99designs/aws-vault) - tool for securely storing aws credentials
208 | * [okta_aws_login](https://github.com/nimbusscale/okta_aws_login)
209 | * [okta-awscli](https://github.com/jmhale/okta-awscli)
210 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Making a new release
2 |
3 | ## Prerequisites
4 |
5 | * Create an account on and make sure you have access to the
6 | okta_aws package. Ask an existing maintainer for access if you don't have
7 | it.
8 | * Make sure you have push access to the `chef/okta_aws` and
9 | `chef/homebrew-okta_aws` repositories.
10 | * Install dependencies: python3, twine (`brew install python3;
11 | pip3 install twine`)
12 |
13 | ## Making the release
14 |
15 | Run `./release.sh NEW_VERSION_NUMBER`. This will:
16 |
17 | * Edit okta_aws and bump the version number at the top (in the `__VERSION__`
18 | variable)
19 | * Commit changes to git
20 | * Tag the commit `git tag v$(./okta_aws --version)`
21 | * Push the changes `git push --tags origin master`
22 | * Remove any existing builds `rm -rf dist`
23 | * Run `python ./setup.py sdist` to build a source package
24 | * Run `twine upload dist/*` to upload the package to pypi.
25 |
26 | To update the homebrew formula, clone
27 | and run the `release.sh` script in
28 | that repository. This will:
29 |
30 | * Install okta_aws and homebrew-pypi-poet to a virtualenv
31 | * Generate a formula with `poet -f okta_aws`
32 | * Patch the generated formula to add local customizations
33 | * Commit and push the changes to homebrew-okta-aws
34 |
--------------------------------------------------------------------------------
/lint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Quick script to lint okta_aws
3 | # Serves mostly as a reminder of which tools are used.
4 |
5 | # Installation of tools on a mac:
6 | # brew install flake8
7 | # pip3 install pylint
8 |
9 | flake8
10 | pylint okta_aws
11 |
--------------------------------------------------------------------------------
/okta_aws.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Script to run okta_aws directly from the git repository for use during
3 | # development
4 | python3 -m okta_aws "$@"
5 |
--------------------------------------------------------------------------------
/okta_aws/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2017 Chef Software
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
--------------------------------------------------------------------------------
/okta_aws/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | #
3 | # Copyright 2017 Chef Software
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | """The main entry point for the program."""
17 |
18 | from okta_aws import okta_aws
19 |
20 |
21 | def main(args=None):
22 | """The main entry point for the program."""
23 | try:
24 | okta_aws.OktaAWS(args).run()
25 | except KeyboardInterrupt:
26 | print("Exiting...")
27 |
28 |
29 | if __name__ == "__main__":
30 | main()
31 |
--------------------------------------------------------------------------------
/okta_aws/exceptions.py:
--------------------------------------------------------------------------------
1 | class Error(Exception):
2 | "Base exception class for okta_aws"
3 | pass
4 |
5 |
6 | class LoginError(Error):
7 | "Error logging in to okta"
8 | def __init__(self, message):
9 | self.message = message
10 |
11 |
12 | class AssumeRoleError(Error):
13 | "Error running aws assume-role-with-saml"
14 | def __init__(self, message):
15 | self.message = message
16 |
--------------------------------------------------------------------------------
/okta_aws/okta_aws.py:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2017 Chef Software
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | import argparse
17 | import base64
18 | import getpass
19 | import html
20 | import logging
21 | import os
22 | import re
23 | import shutil
24 | import subprocess
25 | import sys
26 | import xml.etree.ElementTree as ET
27 |
28 | import requests
29 | import toml
30 | import boto3
31 |
32 | from okta_aws import exceptions
33 |
34 |
35 | __VERSION__ = '0.7.0'
36 |
37 |
38 | class OktaAWS(object):
39 | def __init__(self, argv=None):
40 | """Initialize the program
41 |
42 | argv - command line arguments (or None to use sys.argv)
43 | """
44 | self.args = self.parse_args(argv)
45 | self.profile = self.args.profile
46 |
47 | def parse_args(self, argv):
48 | """Parses command line arguments using argparse
49 |
50 | argv - command line arguments (or None to use sys.argv)
51 | """
52 | parser = argparse.ArgumentParser(
53 | description='Generates temporary AWS credentials for an AWS'
54 | ' account you access through okta.')
55 | parser.add_argument('profile', nargs='?',
56 | default=os.getenv("AWS_PROFILE") or "default",
57 | help='The AWS profile you want credentials for')
58 | parser.add_argument('--config', '-c', default='~/.okta_aws.toml',
59 | help='Path to the configuration file')
60 | parser.add_argument('--no-cookies', '-n', action='store_true',
61 | help="Don't use or save okta session cookie")
62 | parser.add_argument('--debug', '-d', action='store_true',
63 | help='Show debug output')
64 | parser.add_argument('--quiet', '-q', action='store_true',
65 | help='Only show error messages')
66 | parser.add_argument('--list', '-l', action='store_true',
67 | help="Don't assume a role, list assigned "
68 | "applications in okta")
69 | parser.add_argument('--all', '-a', action='store_true',
70 | help='Assume a role in all assigned accounts')
71 | parser.add_argument('--role_arn', '-r',
72 | help='Role name or ARN to assume')
73 | parser.add_argument('--version', '-v', action='version',
74 | version=__VERSION__,
75 | help='Show version of okta_aws and exit')
76 | parser.add_argument('--setup', '-s', action='store_true',
77 | help="Set up a config file for okta_aws")
78 | return parser.parse_args(argv)
79 |
80 | def setup_logging(self):
81 | """Sets up logging based on whether debugging is enabled or not"""
82 | if self.args.debug:
83 | logging.basicConfig(
84 | format='%(asctime)s %(levelname)s %(message)s',
85 | level=logging.DEBUG)
86 | elif self.args.quiet:
87 | logging.basicConfig(format='%(message)s', level=logging.ERROR)
88 | else:
89 | logging.basicConfig(format='%(message)s', level=logging.INFO)
90 |
91 | if not self.args.debug:
92 | logging.getLogger('boto3').setLevel(logging.ERROR)
93 | logging.getLogger('botocore').setLevel(logging.ERROR)
94 |
95 | def preflight_checks(self):
96 | """Performs some checks to ensure that the program can be run
97 | successfully. If these checks fail, an explanation is given as well as
98 | a hint for how the user can fix the problem.
99 | """
100 | errors = []
101 | # AWS cli
102 | if shutil.which("aws") is None:
103 | errors.append("The AWS CLI (the 'aws' command) cannot be found. "
104 | "see http://docs.aws.amazon.com/cli/latest/"
105 | "userguide/installing.html for information on "
106 | "installing it.")
107 | if errors:
108 | print("Preflight check failed")
109 | print("======================")
110 | for e in errors:
111 | print("* %s" % e)
112 | sys.exit(1)
113 |
114 | def interactive_setup(self, config_file):
115 | """Performs first-time setup for users who haven't set up a config
116 | file yet, by asking some simple questions.
117 | """
118 | try:
119 | toml_config = toml.load(os.path.expanduser(config_file))
120 | except FileNotFoundError:
121 | toml_config = {}
122 | toml_config.setdefault('general', {})
123 |
124 | default_username = toml_config['general'].get(
125 | 'username', getpass.getuser())
126 | default_server = toml_config['general'].get(
127 | 'okta_server', 'example.okta.com')
128 |
129 | print("Okta AWS initial setup")
130 | print("======================")
131 | print()
132 | username = input("Enter your okta username [%s]: " % default_username)
133 | if username == "":
134 | username = default_username
135 |
136 | okta_server = input("Enter your okta domain [%s]: " % default_server)
137 | if okta_server == "":
138 | okta_server = default_server
139 |
140 | print()
141 | print("Creating/updating %s" % config_file)
142 | toml_config['general']['username'] = username
143 | toml_config['general']['okta_server'] = okta_server
144 | with open(os.path.expanduser(config_file), "w") as fh:
145 | toml.dump(toml_config, fh)
146 |
147 | print("Setup complete. You can now log in with okta_aws PROFILENAME.")
148 | print("Hint: you can use 'okta_aws --list' to see which profiles "
149 | "you can use.")
150 |
151 | def load_config(self, config_file):
152 | """Loads the config file and returns a dictionary containing its
153 | contents.
154 |
155 | config_file - path to the configuration file to load
156 | """
157 | try:
158 | config = toml.load(os.path.expanduser(config_file))
159 | except FileNotFoundError:
160 | self.interactive_setup(config_file)
161 | sys.exit(0)
162 |
163 | config.setdefault('general', {})
164 | config.setdefault('aliases', {})
165 |
166 | required_config_options = [
167 | 'username',
168 | 'okta_server'
169 | ]
170 |
171 | missing_options = [k for k in required_config_options
172 | if k not in config['general']]
173 | if missing_options:
174 | logging.error("Missing required configuration settings: %s",
175 | ', '.join(missing_options))
176 | sys.exit(1)
177 |
178 | # Default configuration values
179 | config['general'].setdefault('cookie_file', '~/.okta_aws_cookie')
180 | config['general'].setdefault('short_profile_names', True)
181 | config['general'].setdefault('session_duration', 3600)
182 |
183 | config['general']['cookie_file'] = os.path.expanduser(
184 | config['general']['cookie_file'])
185 |
186 | return config
187 |
188 | def get_config(self, key, default=None):
189 | """Obtain a profile specific configuration value, falling back to the
190 | general config or a default value"""
191 | try:
192 | return self.config[self.profile][key]
193 | except KeyError:
194 | try:
195 | real_profile = self.config['aliases'][self.profile]
196 | return self.config[real_profile][key]
197 | except KeyError:
198 | return self.config['general'].get(key, default)
199 |
200 | def choose_from_menu(self, choices, prompt="Select an option: "):
201 | """Present an interactive menu of choices for the user to pick from.
202 |
203 | Returns the index into the list of the selected item.
204 |
205 | choices - a list of options to choose from
206 | prompt - the prompt to show to the user
207 | """
208 | for idx, value in enumerate(choices):
209 | print("%2d) %s" % (idx + 1, value))
210 | response = 0
211 | while response < 1 or response > len(choices):
212 | try:
213 | response = int(input(prompt))
214 | except ValueError:
215 | # If we enter something invalid, just go through the
216 | # loop again.
217 | pass
218 | return response - 1
219 |
220 | def select_role(self, arns):
221 | """Returns the role to use from a list of principal/role arn pairs,
222 | based on either a configuration option, user selecting from a menu,
223 | or simply returning the only arn pair in the list if there is only
224 | one.
225 |
226 | arns - a list of arn pairs (each pair should be a princpal/role arn)
227 | """
228 | selected = None
229 | if len(arns) > 1:
230 | # Get role via config, but allow commandline override
231 | role_arn = self.get_config('role_arn') or self.args.role_arn
232 | if role_arn is not None:
233 | # First check to see if we configured a default role
234 | logging.debug("Looking for configured role: %s", role_arn)
235 | for arn in arns:
236 | # Use endswith here so we can provide just a role name
237 | # instead of the full ARN.
238 | if arn[1].endswith(role_arn):
239 | selected = arn
240 | if selected is None:
241 | # We either didn't configure a default role or the configured
242 | # default role didn't match any available roles. Ask the user
243 | # to pick one.
244 | print("Available roles")
245 | response = self.choose_from_menu(
246 | [arn[1].split('/')[-1] for arn in arns],
247 | "Select role to log in with: ")
248 | selected = arns[response]
249 | else:
250 | selected = arns[0]
251 | return selected
252 |
253 | def get_arns(self, saml_assertion):
254 | """Extracts the available principal/role ARNS for a user given a
255 | base64 encoded SAML assertion returned by okta.
256 |
257 | saml_assertion - the saml asssertion given by okta, base64 encoded.
258 | """
259 | parsed = ET.fromstring(base64.b64decode(saml_assertion))
260 | # Horrible xpath expression to dig into the ARNs
261 | elems = parsed.findall(
262 | ".//{urn:oasis:names:tc:SAML:2.0:assertion}Attribute["
263 | "@Name='https://aws.amazon.com/SAML/Attributes/Role']//*")
264 | # text contains Principal ARN, Role ARN separated by a comma
265 | arns = [e.text.split(",", 1) for e in elems]
266 | selected = self.select_role(arns)
267 | # Returns principal_arn, role_arn
268 | logging.debug("Principal ARN: %s", selected[0])
269 | logging.debug("Role ARN: %s", selected[1])
270 | return selected
271 |
272 | def aws_assume_role(self, principal_arn, role_arn, assertion, duration):
273 | """Gets temporary credentials from aws. Returns a dictionary
274 | containing the temporary credentials.
275 |
276 | principal_arn - the principal_arn (obtained from saml assertion)
277 | role_arn - the arn of the role to assume (obtained from saml
278 | assertion)
279 | assertion - the saml assertion itself (base64 encoded)
280 | duration - how long to request the credentials be valid for in
281 | seconds. This can't be longer than AWS allows (3600 by
282 | default, may be configured to be as long as 43200)
283 | """
284 |
285 | # Override AWS_PROFILE so boto3 doesn't complain if we have it set
286 | # to a new profile that doesn't yet exist. This is needed because
287 | # boto3 will use environment variables if you don't pass in a profile,
288 | # but will complain if you do pass in a profile that doesn't exist.
289 | oldenv = os.environ
290 | if 'AWS_PROFILE' in os.environ:
291 | del os.environ['AWS_PROFILE']
292 | if 'AWS_DEFAULT_PROFILE' in os.environ:
293 | del os.environ['AWS_DEFAULT_PROFILE']
294 | if 'govcloud' in self.profile:
295 | region_name = 'us-gov-east-1'
296 | else:
297 | region_name = 'us-east-1'
298 |
299 | client = boto3.client('sts', region_name=region_name)
300 |
301 | # And restore them once more
302 | if 'AWS_PROFILE' in oldenv:
303 | os.environ['AWS_PROFILE'] = oldenv['AWS_PROFILE']
304 | if 'AWS_DEFAULT_PROFILE' in oldenv:
305 | os.environ['AWS_DEFAULT_PROFILE'] = oldenv['AWS_DEFAULT_PROFILE']
306 |
307 | aws_creds = client.assume_role_with_saml(
308 | RoleArn=role_arn,
309 | PrincipalArn=principal_arn,
310 | SAMLAssertion=assertion,
311 | DurationSeconds=duration
312 | )
313 |
314 | if 'Credentials' not in aws_creds:
315 | logging.debug("aws_creds json is: %s" % aws_creds)
316 | raise exceptions.AssumeRoleError("Credentials key not in returned"
317 | " json")
318 |
319 | return aws_creds['Credentials']
320 |
321 | def set_aws_config(self, profile, key, value):
322 | """Sets a single AWS configuration option. Used to store the temporary
323 | credentials in ~/.aws/credentials.
324 |
325 | profile - the profile to set the configuration option under
326 | key - the option to change
327 | value - the value to change it to
328 | """
329 | # Override AWS_PROFILE so aws sts doesn't complain if we have it set
330 | # to a new profile that doesn't yet exist
331 | newenv = os.environ.copy()
332 | if 'AWS_PROFILE' in newenv:
333 | del newenv['AWS_PROFILE']
334 | if 'AWS_DEFAULT_PROFILE' in newenv:
335 | del newenv['AWS_DEFAULT_PROFILE']
336 |
337 | subprocess.call([shutil.which("aws"), "configure", "set",
338 | "--profile", profile, key, value],
339 | env=newenv)
340 |
341 | def store_aws_creds_in_profile(self, profile, aws_creds):
342 | """Stores the temporary AWS credentials in ~/.aws/credentials.
343 |
344 | profile - the profile to store the credentials under
345 | aws_creds - a dictionary containing the credentials returned from AWS
346 | """
347 | self.set_aws_config(profile, "aws_access_key_id",
348 | aws_creds['AccessKeyId'])
349 | self.set_aws_config(profile, "aws_secret_access_key",
350 | aws_creds['SecretAccessKey'])
351 | self.set_aws_config(profile, "aws_session_token",
352 | aws_creds['SessionToken'])
353 |
354 | def is_logged_in(self, session_id):
355 | """Checks to see if a given okta session ID is still valid. Will return
356 | false if the session has expired and we are no longer logged in to
357 | okta.
358 |
359 | session_id - the session token that we are verifying
360 | """
361 | logging.debug("Verifying if we are already logged in")
362 | r = requests.get("https://%s/api/v1/sessions/me" %
363 | self.get_config('okta_server'),
364 | cookies={"sid": session_id})
365 | logged_in = r.status_code == 200
366 | logging.debug("Logged in: %s", logged_in)
367 | return logged_in
368 |
369 | def verify_totp_factor(self, url, statetoken):
370 | """Verifies the totp factor passcode, returning a single use session
371 | token that can be exchanged for a long lived session ID.
372 |
373 | url - the totp factor verification url
374 | statetoken - the state token provided when verifying totp factor
375 | """
376 | passcode = input("Enter your passcode: ")
377 | r = requests.post(url,
378 | json={
379 | "stateToken": statetoken,
380 | "passCode": passcode
381 | })
382 | if r.status_code == 403:
383 | raise exceptions.LoginError("Incorrect passcode")
384 | if r.status_code != 200:
385 | logging.debug(r.text)
386 | raise exceptions.LoginError(
387 | "Login request returned HTTP status %s" % r.status_code)
388 | return r.json()
389 |
390 | def log_in_to_okta(self, password):
391 | """Logs in to okta using the authn API, returning a single use session
392 | token that can be exchanged for a long lived session ID.
393 |
394 | password - the user's okta password
395 | """
396 | r = requests.post(
397 | "https://%s/api/v1/authn" % self.get_config('okta_server'),
398 | json={
399 | "username": self.get_config('username'),
400 | "password": password
401 | })
402 | if r.status_code == 401:
403 | raise exceptions.LoginError("Incorrect password")
404 | if r.status_code != 200:
405 | logging.debug(r.text)
406 | raise exceptions.LoginError(
407 | "Login request returned HTTP status %s" % r.status_code)
408 | session_data = r.json()
409 | if 'status' not in session_data:
410 | logging.error(session_data)
411 | raise exceptions.LoginError(
412 | "Unknown error (missing status field in response)")
413 | if session_data['status'] == 'MFA_REQUIRED':
414 | logging.debug('MFA Required')
415 | statetoken = session_data["stateToken"]
416 | for factor in session_data["_embedded"]["factors"]:
417 | # TODO - Add other factors
418 | if factor["factorType"] == "token:software:totp":
419 | url = factor["_links"]["verify"]["href"]
420 | session_data = self.verify_totp_factor(url, statetoken)
421 | if session_data['status'] != 'SUCCESS':
422 | raise exceptions.LoginError(
423 | session_data['status'].title().replace('_', ' '))
424 | if 'sessionToken' not in session_data:
425 | logging.debug(session_data)
426 | raise exceptions.LoginError("Missing session token")
427 | return session_data['sessionToken']
428 |
429 | def get_session_id(self, session_token):
430 | """Returns a (long lived) session ID given a (single use) session
431 | token.
432 |
433 | session_token - the single use token returned when logging in to okta
434 | """
435 | r = requests.post(
436 | "https://%s/api/v1/sessions" % self.get_config('okta_server'),
437 | json={"sessionToken": session_token})
438 | if r.status_code != 200:
439 | logging.debug(r.text)
440 | return None
441 | return r.json()['id']
442 |
443 | def get_assigned_applications(self, session_id):
444 | """Queries okta to get a list of AWS applications that have been
445 | assigned to the user. Returns a dictionary mapping the profile names
446 | to log in URLs for each assigned application.
447 |
448 | session_id - the okta session ID needed to make api calls
449 | """
450 | # TODO - proper pagination on this
451 | logging.debug("Getting assigned application links from okta")
452 | r = requests.get("https://%s/api/v1/users/me/appLinks?limit=1000" %
453 | self.get_config('okta_server'),
454 | cookies={"sid": session_id})
455 | if r.status_code != 200:
456 | logging.error("Error getting assigned application list")
457 | logging.debug(r.text)
458 | return None
459 | applinks = {i['label']: i['linkUrl'] for i in r.json()
460 | if i['appName'] == 'amazon_aws'}
461 | return applinks
462 |
463 | def shorten_appnames(self, applinks):
464 | """Converts long application names such as
465 | 'Company Engineering AWS (dev use)' to something suitable for use in
466 | an aws profile such as 'company-engineering'.
467 |
468 | applinks - a dictionary mapping application names to application links.
469 | """
470 | logging.debug("Shortening application names")
471 | newapplinks = {}
472 | for k, v in applinks.items():
473 | newk = re.sub(" *AWS$", "", k) # Remove AWS suffix
474 | newk = re.sub(r" *\(.*\)", "", newk) # Remove anything in parens
475 | newk = newk.lower()
476 | newk = re.sub(" +", "-", newk)
477 | newapplinks[newk] = v
478 | logging.debug("%s => %s", k, newk)
479 | return newapplinks
480 |
481 | def get_saml_assertion(self, session_id, app_url):
482 | """Sends a request to the application link, and extracts a SAML
483 | assertion from the response.
484 |
485 | session_id - okta session ID needed to make api calls
486 | app_url - The URL used to log in to the okta application
487 | """
488 | r = requests.post(app_url, cookies={"sid": session_id})
489 |
490 | if r.status_code != 200:
491 | logging.error("Error getting saml assertion. HTML response %s",
492 | r.status_code)
493 | return None
494 |
495 | match = re.search(r'= 3600:
510 | return "%.2g hours" % (seconds / 3600.0)
511 | elif seconds == 60:
512 | return "1 minute"
513 | return "%.2g minutes" % (seconds / 60.0)
514 |
515 | def fetch_credentials(self, applinks, session_id):
516 | """Performs the various steps needed to actually get a set of
517 | temporary credentials and store them. Doesn't return anything, but
518 | temporary credentials should be stored in ~/.aws/credentials by the
519 | time this method has finished.
520 |
521 | applinks - a mapping of profile names to application links
522 | session_id - okta session ID needed to make API calls
523 | """
524 | # Resolve any profile alias and store it in real_profile
525 | real_profile = self.config['aliases'].get(self.profile, self.profile)
526 |
527 | if real_profile not in applinks:
528 | alias_msg = ""
529 | if real_profile != self.profile:
530 | alias_msg = " (an alias that resolved to %s)" % real_profile
531 | print("ERROR: %s%s isn't a valid profile name" % (
532 | self.profile, alias_msg))
533 | print("Valid profiles:", ', '.join(list(applinks.keys())))
534 | sys.exit(1)
535 |
536 | saml_assertion = self.get_saml_assertion(
537 | session_id, applinks[real_profile])
538 | if saml_assertion is None:
539 | logging.error("Problem getting SAML assertion")
540 | sys.exit(1)
541 |
542 | principal_arn, role_arn = self.get_arns(saml_assertion)
543 |
544 | logging.info("Assuming AWS role %s...", role_arn.split("/")[-1])
545 | session_duration = self.get_config('session_duration')
546 | try:
547 | aws_creds = self.aws_assume_role(principal_arn, role_arn,
548 | saml_assertion, session_duration)
549 | except exceptions.AssumeRoleError as e:
550 | logging.error("Unable to get temporary credentials: %s", e)
551 | sys.exit(1)
552 | self.store_aws_creds_in_profile(self.profile, aws_creds)
553 | logging.info("Temporary credentials stored in profile %s",
554 | self.profile)
555 | logging.info("Credentials expire in %s",
556 | self.friendly_interval(session_duration))
557 |
558 | def run(self):
559 | """Main entry point for the application after parsing command line
560 | arguments."""
561 | self.setup_logging()
562 |
563 | self.preflight_checks()
564 |
565 | if self.args.setup:
566 | self.interactive_setup(self.args.config)
567 | sys.exit(0)
568 |
569 | self.config = self.load_config(self.args.config)
570 |
571 | if not self.args.no_cookies:
572 | if os.path.exists(self.get_config('cookie_file')):
573 | logging.debug("Loading session ID from %s",
574 | self.get_config('cookie_file'))
575 | with open(self.get_config('cookie_file')) as fh:
576 | session_id = fh.read().rstrip("\n")
577 | # Support old cookie file format
578 | if session_id.startswith('#LWP-Cookies-2.0'):
579 | logging.debug("Converting old cookie file format")
580 | m = re.search(r'sid="([^"]*)"', session_id)
581 | if m:
582 | logging.debug("Found session ID in old cookies")
583 | session_id = m.group(1)
584 | else:
585 | logging.debug("Didn't find session ID in cookies")
586 | session_id = None
587 | if session_id is not None \
588 | and not self.is_logged_in(session_id):
589 | session_id = None
590 | else:
591 | session_id = None
592 | else:
593 | session_id = None
594 |
595 | if session_id is None:
596 | print("Okta Username:", self.get_config('username'))
597 | password = ""
598 | while password == "":
599 | password = getpass.getpass("Okta Password: ")
600 | sys.stdout.flush()
601 |
602 | try:
603 | onetimetoken = self.log_in_to_okta(password)
604 | except exceptions.LoginError as e:
605 | logging.error("Error logging into okta: %s", e.message)
606 | sys.exit(1)
607 |
608 | session_id = self.get_session_id(onetimetoken)
609 | if not self.args.no_cookies:
610 | logging.debug("Saving session cookie to %s",
611 | self.get_config('cookie_file'))
612 | with open(self.get_config('cookie_file'), 'w') as fh:
613 | fh.write(session_id)
614 |
615 | applinks = self.get_assigned_applications(session_id)
616 | if self.get_config('short_profile_names'):
617 | applinks = self.shorten_appnames(applinks)
618 |
619 | if self.args.all:
620 | for profile in applinks.keys():
621 | print("Fetching credentials for:", profile)
622 | self.profile = profile
623 | self.fetch_credentials(applinks, session_id)
624 | sys.exit(0)
625 |
626 | if self.args.list:
627 | print("Available profiles:")
628 | reverse_aliases = {}
629 | for k, v in self.config['aliases'].items():
630 | reverse_aliases.setdefault(v, []).append(k)
631 | for profile in applinks.keys():
632 | if profile in reverse_aliases:
633 | print("%s (Aliases: %s)" % (profile, ', '.join(
634 | reverse_aliases[profile])))
635 | else:
636 | print(profile)
637 |
638 | sys.exit(0)
639 |
640 | self.fetch_credentials(applinks, session_id)
641 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | VERSION="$1"
3 |
4 | VERSION_FILE="okta_aws/okta_aws.py"
5 |
6 | if [[ -z $VERSION ]]; then
7 | echo "Usage: $0 VERSION"
8 | echo "Current version: $(grep ^__VERSION__ $VERSION_FILE | awk '{print $NF}')"
9 | exit 1
10 | fi
11 |
12 | # Make sure required commands are installed first
13 | for i in twine python git; do
14 | if ! command -v twine >/dev/null; then
15 | echo "You need to have $i installed. Please install it and try"
16 | echo "running this again. See RELEASE.md for more information"
17 | exit 1
18 | fi
19 | done
20 |
21 | echo "=> Bumping version in okta_aws to $VERSION"
22 | sed -i "" "s/^__VERSION__ = .*$/__VERSION__ = '$VERSION'/" "$VERSION_FILE"
23 |
24 | echo "=> Committing to git"
25 | git add "$VERSION_FILE"
26 | git commit -m "Release v$VERSION"
27 |
28 | echo "=> Tagging release"
29 | git tag "v$VERSION"
30 |
31 | echo "=> Building source distribution"
32 | rm -rf dist/
33 | python ./setup.py sdist
34 |
35 | echo "=> Changes about to be pushed"
36 | git --no-pager show HEAD
37 |
38 | echo "About to push changes to git. Press Enter to continue, or ^C to quit"
39 | read -r
40 |
41 | echo "=> Pushing to git"
42 | git push --tags origin master
43 |
44 | echo "About to upload release to pypi. Press Enter to continue, or ^C to quit"
45 | read -r
46 |
47 | echo "=> Uploading release to pypi"
48 | twine upload dist/*
49 |
50 | echo "=> Done"
51 |
52 | echo "Don't forget to update the homebrew formula as well by running the"
53 | echo "release.sh script in the homebrew-okta_aws repo."
54 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [aliases]
2 | test=pytest
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | from setuptools import setup
4 |
5 |
6 | def find_version():
7 | with open(os.path.join(os.path.dirname(__file__),
8 | 'okta_aws/okta_aws.py')) as fh:
9 | content = fh.read()
10 | m = re.search(r"^__VERSION__ = ['\"]([^'\"]*)['\"]", content, re.M)
11 | if m:
12 | return m.group(1)
13 | raise RuntimeError("Unable to find version string.")
14 |
15 |
16 | setup(
17 | name='okta_aws',
18 | version=find_version(),
19 | description='Use the AWS API via an account using Okta',
20 | author='Mark Harrison',
21 | author_email='mharrison@chef.io',
22 | license='Apache-2.0',
23 | packages=['okta_aws'],
24 | entry_points={"console_scripts": ['okta_aws=okta_aws.__main__:main']},
25 | url='https://github.com/chef/okta_aws',
26 | python_requires='>=3',
27 | setup_requires=['pytest-runner'],
28 | tests_require=['pytest', 'pytest_datadir'],
29 | install_requires=['requests>=2.21.0', 'toml>=0.10.0', 'boto3>=1.9.93']
30 | )
31 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chef/okta_aws/6baa3826435fc42751fd30ab5b77e0a9adfc493b/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from okta_aws import okta_aws
4 |
5 |
6 | @pytest.fixture
7 | def oa():
8 | return okta_aws.OktaAWS([])
9 |
--------------------------------------------------------------------------------
/tests/data/.gitignore:
--------------------------------------------------------------------------------
1 | *_real.*
2 |
--------------------------------------------------------------------------------
/tests/data/okta_auth_password_expired.json:
--------------------------------------------------------------------------------
1 | {"stateToken":"1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ123456","expiresAt":"2018-05-15T00:00:00.000Z","status":"PASSWORD_EXPIRED","_embedded":{"user":{"id":"00000000000000000000","profile":{"login":"fakey@chef.io","firstName":"Frederick","lastName":"Akey","locale":"en","timeZone":"America/Los_Angeles"}},"policy":{"complexity":{"minLength":8,"minLowerCase":1,"minUpperCase":1,"minNumber":1,"minSymbol":0,"excludeUsername":true},"age":{"minAgeMinutes":0,"historyCount":0}}},"_links":{"next":{"name":"changePassword","href":"https://chef.okta.com/api/v1/authn/credentials/change_password","hints":{"allow":["POST"]}},"cancel":{"href":"https://chef.okta.com/api/v1/authn/cancel","hints":{"allow":["POST"]}}}}
2 |
--------------------------------------------------------------------------------
/tests/data/okta_auth_success.json:
--------------------------------------------------------------------------------
1 | {"expiresAt":"2018-05-15T00:00:00.000Z","status":"SUCCESS","sessionToken":"1234567890ABCDEFGHIJKLMNO","_embedded":{"user":{"id":"00000000000000000000","profile":{"login":"fakey","firstName":"Frederick","lastName":"Akey","locale":"en","timeZone":"America/Los_Angeles"}}}}
2 |
--------------------------------------------------------------------------------
/tests/data/okta_aws.toml:
--------------------------------------------------------------------------------
1 | # Sample config file
2 | [general]
3 | username="fakey"
4 | okta_server="example.okta.com"
5 |
--------------------------------------------------------------------------------
/tests/data/saml_assertion.txt:
--------------------------------------------------------------------------------
1 | PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHNhbWwycDpSZXNwb25zZSB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9zaWduaW4uYXdzLmFtYXpvbi5jb20vc2FtbCIgSUQ9ImlkMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NSIgSXNzdWVJbnN0YW50PSIyMDE4LTA0LTIxVDAwOjAwOjAwLjAwMFoiIFZlcnNpb249IjIuMCI+CiAgPHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vRkFLRTwvc2FtbDI6SXNzdWVyPgogIDxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogICAgPGRzOlNpZ25lZEluZm8+CiAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+CiAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjaWQxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ0Ij4KICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4KICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPgogICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyI+CiAgICAgICAgICAgIDxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIFByZWZpeExpc3Q9InhzIi8+CiAgICAgICAgICA8L2RzOlRyYW5zZm9ybT4KICAgICAgICA8L2RzOlRyYW5zZm9ybXM+CiAgICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPgogICAgICAgIDxkczpEaWdlc3RWYWx1ZT5Sa0ZMUlFvPTwvZHM6RGlnZXN0VmFsdWU+CiAgICAgIDwvZHM6UmVmZXJlbmNlPgogICAgPC9kczpTaWduZWRJbmZvPgogICAgPGRzOlNpZ25hdHVyZVZhbHVlPlJrRkxSUW89PC9kczpTaWduYXR1cmVWYWx1ZT4KICAgIDxkczpLZXlJbmZvPgogICAgICA8ZHM6WDUwOURhdGE+CiAgICAgICAgPGRzOlg1MDlDZXJ0aWZpY2F0ZT4KICAgICAgICAgICAgUmtGTFJRbz0KICAgICAgICA8L2RzOlg1MDlDZXJ0aWZpY2F0ZT4KICAgICAgPC9kczpYNTA5RGF0YT4KICAgIDwvZHM6S2V5SW5mbz4KICA8L2RzOlNpZ25hdHVyZT4KICA8c2FtbDJwOlN0YXR1cyB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+CiAgICA8c2FtbDJwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPgogIDwvc2FtbDJwOlN0YXR1cz4KICA8c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJpZDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDUiIElzc3VlSW5zdGFudD0iMjAxOC0wNC0yMVQwMDowMDowMC4wMDBaIiBWZXJzaW9uPSIyLjAiPgogICAgPHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vRkFLRTwvc2FtbDI6SXNzdWVyPgogICAgPGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+CiAgICAgIDxkczpTaWduZWRJbmZvPgogICAgICAgIDxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz4KICAgICAgICA8ZHM6UmVmZXJlbmNlIFVSST0iI2lkMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NSI+CiAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+CiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPgogICAgICAgICAgICAgIDxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIFByZWZpeExpc3Q9InhzIi8+CiAgICAgICAgICAgIDwvZHM6VHJhbnNmb3JtPgogICAgICAgICAgPC9kczpUcmFuc2Zvcm1zPgogICAgICAgICAgPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPgogICAgICAgICAgPGRzOkRpZ2VzdFZhbHVlPlJrRkxSUW89PC9kczpEaWdlc3RWYWx1ZT4KICAgICAgICA8L2RzOlJlZmVyZW5jZT4KICAgICAgPC9kczpTaWduZWRJbmZvPgogICAgICA8ZHM6U2lnbmF0dXJlVmFsdWU+UmtGTFJRbz08L2RzOlNpZ25hdHVyZVZhbHVlPgogICAgICA8ZHM6S2V5SW5mbz4KICAgICAgICA8ZHM6WDUwOURhdGE+CiAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPlJrRkxSUW89PC9kczpYNTA5Q2VydGlmaWNhdGU+CiAgICAgICAgPC9kczpYNTA5RGF0YT4KICAgICAgPC9kczpLZXlJbmZvPgogICAgPC9kczpTaWduYXR1cmU+CiAgICA8c2FtbDI6U3ViamVjdCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+CiAgICAgIDxzYW1sMjpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZCI+ZmFrZXVzZXI8L3NhbWwyOk5hbWVJRD4KICAgICAgPHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj4KICAgICAgICA8c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDE4LTA0LTIxVDAwOjAwOjAwLjAwMFoiIFJlY2lwaWVudD0iaHR0cHM6Ly9zaWduaW4uYXdzLmFtYXpvbi5jb20vc2FtbCIvPgogICAgICA8L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24+CiAgICA8L3NhbWwyOlN1YmplY3Q+CiAgICA8c2FtbDI6Q29uZGl0aW9ucyB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgTm90QmVmb3JlPSIyMDE4LTA0LTIxVDAwOjAwOjAwLjAwMFoiIE5vdE9uT3JBZnRlcj0iMjAxOC0wNC0yMVQwMDowMDowMC4wMDBaIj4KICAgICAgPHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPHNhbWwyOkF1ZGllbmNlPnVybjphbWF6b246d2Vic2VydmljZXM8L3NhbWwyOkF1ZGllbmNlPgogICAgICA8L3NhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICA8L3NhbWwyOkNvbmRpdGlvbnM+CiAgICA8c2FtbDI6QXV0aG5TdGF0ZW1lbnQgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIEF1dGhuSW5zdGFudD0iMjAxOC0wNC0yMVQwMDowMDowMC4wMDBaIiBTZXNzaW9uSW5kZXg9ImlkMTUyNDM1NDQ3Nzc0MC4xNTc2NjMzMDkxIj4KICAgICAgPHNhbWwyOkF1dGhuQ29udGV4dD4KICAgICAgICA8c2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPgogICAgICA8L3NhbWwyOkF1dGhuQ29udGV4dD4KICAgIDwvc2FtbDI6QXV0aG5TdGF0ZW1lbnQ+CiAgICA8c2FtbDI6QXR0cmlidXRlU3RhdGVtZW50IHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj4KICAgICAgPHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJodHRwczovL2F3cy5hbWF6b24uY29tL1NBTUwvQXR0cmlidXRlcy9Sb2xlIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVyaSI+CiAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+YXJuOmF3czppYW06OjAxMjM0NTY3ODkwMTpzYW1sLXByb3ZpZGVyL09LVEEsYXJuOmF3czppYW06OjAxMjM0NTY3ODkwMTpyb2xlL09rdGFfQWRtaW5pc3RyYXRvckFjY2Vzczwvc2FtbDI6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDI6QXR0cmlidXRlPgogICAgICA8c2FtbDI6QXR0cmlidXRlIE5hbWU9Imh0dHBzOi8vYXdzLmFtYXpvbi5jb20vU0FNTC9BdHRyaWJ1dGVzL1JvbGVTZXNzaW9uTmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZmFrZXVzZXI8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWwyOkF0dHJpYnV0ZT4KICAgICAgPHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJodHRwczovL2F3cy5hbWF6b24uY29tL1NBTUwvQXR0cmlidXRlcy9TZXNzaW9uRHVyYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPjQzMjAwPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sMjpBdHRyaWJ1dGU+CiAgICA8L3NhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudD4KICA8L3NhbWwyOkFzc2VydGlvbj4KPC9zYW1sMnA6UmVzcG9uc2U+Cg==
2 |
--------------------------------------------------------------------------------
/tests/data/saml_assertion_decoded.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 | http://www.okta.com/FAKE
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | RkFLRQo=
17 |
18 |
19 | RkFLRQo=
20 |
21 |
22 |
23 | RkFLRQo=
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | http://www.okta.com/FAKE
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | RkFLRQo=
46 |
47 |
48 | RkFLRQo=
49 |
50 |
51 | RkFLRQo=
52 |
53 |
54 |
55 |
56 | fakeuser
57 |
58 |
59 |
60 |
61 |
62 |
63 | urn:amazon:webservices
64 |
65 |
66 |
67 |
68 | urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
69 |
70 |
71 |
72 |
73 | arn:aws:iam::012345678901:saml-provider/OKTA,arn:aws:iam::012345678901:role/Okta_AdministratorAccess
74 |
75 |
76 | fakeuser
77 |
78 |
79 | 43200
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/tests/data/saml_assertion_page.html:
--------------------------------------------------------------------------------
1 |
2 | My Company - Signing in...
3 |
4 |
5 |
6 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tests/data/sts_creds.json:
--------------------------------------------------------------------------------
1 | {
2 | "Credentials": {
3 | "AccessKeyId": "ASIAABCDEFG123456789",
4 | "SecretAccessKey": "hOUlRBNYBVlR05jXRBXbntDc/F56FkPsj+Gd/mzP",
5 | "SessionToken": "uMvpvBlJ2kL3JRsJiktoC3FJq3aVCmyKOcX+mtrJCc5UIkFsozZD7aGxISsAELQelXCEMVRhef37BG8dsI8cd2Khz+xIcWdwCxwOfFqQEWDFltsI3Sfbz0cdT9mtDG6DvmT3vTFB5DdeuRJzTvZFXCi7msKCGWmOpSpWc9PrZIOVV5ad84mgsa3N0ccvCEtJ/7rdzZL/y9cQfZLmb0+VbuVTL76Siyw9zt7sMIjDSOtGBOXf1wcgO2m7NU37wRK3Ux4nvttTtrKeZ7f3DuDzsH61OrLcg98sz0XDW3Brl31dhrqdVORIOv9SEpaRewDSX/wl74DVElH9VQ0fOHdsD6MNFnKl+NEAiYd4vZMjk1U8/e4GAWZIF/EHaQxgHU3NBsWvRYsTQUPombgR81BXm9Quh9aj",
6 | "Expiration": "2018-04-24T00:00:00Z"
7 | },
8 | "AssumedRoleUser": {
9 | "AssumedRoleId": "ABCDEFGHIJ12345678901:fakeuser",
10 | "Arn": "arn:aws:sts::123456789012:assumed-role/Okta_AdministratorAccess/fakeuser"
11 | },
12 | "Subject": "fakeuser",
13 | "SubjectType": "unspecified",
14 | "Issuer": "http://www.okta.com/abcdefghijklmnopq1d8",
15 | "Audience": "https://signin.aws.amazon.com/saml",
16 | "NameQualifier": "DNBr1wUt6enAZ9ia69kV8WW7ASY="
17 | }
18 |
--------------------------------------------------------------------------------
/tests/test_aws_assume_role.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=invalid-name,missing-docstring
2 | import pytest
3 | import json
4 |
5 | from okta_aws import exceptions
6 |
7 | from unittest.mock import patch
8 |
9 |
10 | @patch('boto3.client')
11 | def test_aws_assume_role(patched_client, oa, shared_datadir):
12 | with open("%s/sts_creds.json" % shared_datadir, 'rb') as fh:
13 | patched_client('sts').assume_role_with_saml.return_value = \
14 | json.load(fh)
15 |
16 | with open("%s/saml_assertion.txt" % shared_datadir) as fh:
17 | assertion = fh.read()
18 |
19 | credentials = oa.aws_assume_role(
20 | 'arn:aws:iam::012345678901:saml-provider/OKTA',
21 | 'arn:aws:iam::012345678901:role/Okta_AdministratorAccess',
22 | assertion,
23 | 3600)
24 |
25 | assert credentials['AccessKeyId'] == 'ASIAABCDEFG123456789'
26 | assert credentials['SecretAccessKey'] == \
27 | 'hOUlRBNYBVlR05jXRBXbntDc/F56FkPsj+Gd/mzP'
28 | assert credentials['SessionToken'] == \
29 | 'uMvpvBlJ2kL3JRsJiktoC3FJq3aVCmyKOcX+mtrJCc5UIkFsozZD7aGxISsAE' \
30 | 'LQelXCEMVRhef37BG8dsI8cd2Khz+xIcWdwCxwOfFqQEWDFltsI3Sfbz0cdT9' \
31 | 'mtDG6DvmT3vTFB5DdeuRJzTvZFXCi7msKCGWmOpSpWc9PrZIOVV5ad84mgsa3' \
32 | 'N0ccvCEtJ/7rdzZL/y9cQfZLmb0+VbuVTL76Siyw9zt7sMIjDSOtGBOXf1wcg' \
33 | 'O2m7NU37wRK3Ux4nvttTtrKeZ7f3DuDzsH61OrLcg98sz0XDW3Brl31dhrqdV' \
34 | 'ORIOv9SEpaRewDSX/wl74DVElH9VQ0fOHdsD6MNFnKl+NEAiYd4vZMjk1U8/e' \
35 | '4GAWZIF/EHaQxgHU3NBsWvRYsTQUPombgR81BXm9Quh9aj'
36 | assert credentials['Expiration'] == '2018-04-24T00:00:00Z'
37 |
38 |
39 | @patch('boto3.client')
40 | def test_aws_assume_role_no_credentials(patched_client, oa, shared_datadir):
41 | patched_client('sts').assume_role_with_saml.return_value = {}
42 |
43 | with pytest.raises(exceptions.AssumeRoleError,
44 | match="Credentials key not in returned json"):
45 | oa.aws_assume_role(
46 | 'arn:aws:iam::012345678901:saml-provider/OKTA',
47 | 'arn:aws:iam::012345678901:role/Okta_AdministratorAccess',
48 | 'fake_assertion',
49 | 3600)
50 |
--------------------------------------------------------------------------------
/tests/test_friendly_interval.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # pylint: disable=invalid-name,missing-docstring
3 |
4 |
5 | def test_hour(oa):
6 | assert oa.friendly_interval(3600) == '1 hour'
7 | assert oa.friendly_interval(7200) == '2 hours'
8 |
9 |
10 | def test_minute(oa):
11 | assert oa.friendly_interval(600) == '10 minutes'
12 | assert oa.friendly_interval(60) == '1 minute'
13 |
14 |
15 | def test_fractions(oa):
16 | assert oa.friendly_interval(5400) == '1.5 hours'
17 | assert oa.friendly_interval(30) == '0.5 minutes'
18 |
--------------------------------------------------------------------------------
/tests/test_get_arns.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # pylint: disable=invalid-name,missing-docstring
3 |
4 |
5 | def test_get_arns(oa, shared_datadir):
6 | with open("%s/saml_assertion.txt" % shared_datadir) as fh:
7 | assertion = fh.read()
8 | arns = oa.get_arns(assertion)
9 |
10 | # Principal
11 | assert arns[0] == 'arn:aws:iam::012345678901:saml-provider/OKTA'
12 | # Role
13 | assert arns[1] == 'arn:aws:iam::012345678901:role/Okta_AdministratorAccess'
14 |
--------------------------------------------------------------------------------
/tests/test_get_saml_assertion.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # pylint: disable=invalid-name,missing-docstring
3 | from unittest.mock import patch
4 |
5 |
6 | def test_get_saml_assertion(oa, shared_datadir):
7 | with patch("requests.post") as patched_post:
8 | patched_post.return_value.status_code = 200
9 | with open("%s/saml_assertion_page.html" % shared_datadir) as fh:
10 | patched_post.return_value.text = fh.read()
11 |
12 | sa = oa.get_saml_assertion("session_id", "app_url")
13 |
14 | assert sa == "VGVzdGluZyAxLi4uMi4uLjMuLi4K"
15 |
--------------------------------------------------------------------------------
/tests/test_log_in_to_okta.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=invalid-name,missing-docstring
2 | import json
3 | import pytest
4 |
5 | from okta_aws import exceptions
6 |
7 | from unittest.mock import patch
8 |
9 |
10 | def test_log_in_to_okta_success(oa, shared_datadir):
11 | oa.config = oa.load_config("%s/okta_aws.toml" % shared_datadir)
12 | with patch("requests.post") as patched_post:
13 | patched_post.return_value.status_code = 200
14 | with open("%s/okta_auth_success.json" % shared_datadir) as fh:
15 | text = fh.read()
16 | patched_post.return_value.text = text
17 | patched_post.return_value.json.return_value = json.loads(text)
18 |
19 | token = oa.log_in_to_okta('hunter2')
20 |
21 | assert token == "1234567890ABCDEFGHIJKLMNO"
22 |
23 |
24 | def test_log_in_to_okta_incorrect_password(oa, shared_datadir):
25 | oa.config = oa.load_config("%s/okta_aws.toml" % shared_datadir)
26 | with patch("requests.post") as patched_post:
27 | patched_post.return_value.status_code = 401
28 |
29 | with pytest.raises(exceptions.LoginError, match="Incorrect password"):
30 | oa.log_in_to_okta('hunter2')
31 |
32 |
33 | def test_log_in_to_okta_password_expired(oa, shared_datadir):
34 | oa.config = oa.load_config("%s/okta_aws.toml" % shared_datadir)
35 | with patch("requests.post") as patched_post:
36 | patched_post.return_value.status_code = 200
37 | with open("%s/okta_auth_password_expired.json" % shared_datadir) as fh:
38 | text = fh.read()
39 | patched_post.return_value.text = text
40 | patched_post.return_value.json.return_value = json.loads(text)
41 |
42 | with pytest.raises(exceptions.LoginError, match="Password Expired"):
43 | oa.log_in_to_okta('hunter2')
44 |
--------------------------------------------------------------------------------