From 95970c3aa1a92a4062189c5f88024592b6fc9948 Mon Sep 17 00:00:00 2001 From: thoward Date: Tue, 9 Apr 2019 17:02:12 -0700 Subject: [PATCH] Change Okta Cache location. Add more unit tests Generate pipfile.lock from Python 2.7.5 env. Generate pipfile.lock from Python 2.7.5 env. Bump version to 1.1.0 Update readme --- Pipfile | 1 + Pipfile.lock | 140 +++--- README.rst | 4 +- setup.py | 3 +- src/aws_okta_processor/__init__.py | 2 +- src/aws_okta_processor/core/okta.py | 31 +- tests/APPLICATIONS_RESPONSE | 38 ++ tests/AUTH_MFA_RESPONSE | 1 + tests/AUTH_TOKEN_RESPONSE | 1 + tests/MFA_WAITING_RESPONSE | 1 + tests/SESSION_RESPONSE | 1 + tests/core/test_okta.py | 651 ++++++++++++++++++++++++++-- tests/test_base.py | 10 + 13 files changed, 781 insertions(+), 103 deletions(-) create mode 100644 tests/APPLICATIONS_RESPONSE create mode 100644 tests/AUTH_MFA_RESPONSE create mode 100644 tests/AUTH_TOKEN_RESPONSE create mode 100644 tests/MFA_WAITING_RESPONSE create mode 100644 tests/SESSION_RESPONSE diff --git a/Pipfile b/Pipfile index 92521ea..d937d55 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ pytest-cov = "*" readme-renderer = "*" docutils = "*" "flake8" = "*" +responses = "*" [packages] aws-okta-processor = {path = "."} diff --git a/Pipfile.lock b/Pipfile.lock index 3890d88..65d02bf 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fa36ef76ff58a0b3d45edb6bd1555958162e92e6d653455c90b0bb6723384390" + "sha256": "7e30a9e8607342eb085f833c2789e4562ace5d9313ee89d54beb4e4f875828ff" }, "pipfile-spec": 6, "requires": {}, @@ -28,10 +28,10 @@ }, "attrs": { "hashes": [ - "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", - "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" ], - "version": "==18.2.0" + "version": "==19.1.0" }, "bleach": { "hashes": [ @@ -42,10 +42,10 @@ }, "certifi": { "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" ], - "version": "==2018.11.29" + "version": "==2019.3.9" }, "chardet": { "hashes": [ @@ -64,47 +64,55 @@ }, "configparser": { "hashes": [ - "sha256:27594cf4fc279f321974061ac69164aaebd2749af962ac8686b20503ac0bcf2d", - "sha256:9d51fe0a382f05b6b117c5e601fc219fede4a8c71703324af3f7d883aef476a3" + "sha256:8be81d89d6e7b4c0d4e44bcc525845f6da25821de80cb5e06e7e0238a2899e32", + "sha256:da60d0014fd8c55eb48c1c5354352e363e2d30bbf7057e5e171a468390184c75" ], "markers": "python_version < '3.2'", - "version": "==3.7.3" + "version": "==3.7.4" + }, + "cookies": { + "hashes": [ + "sha256:15bee753002dff684987b8df8c235288eb8d45f8191ae056254812dfd42c81d3", + "sha256:d6b698788cae4cfa4e62ef8643a9ca332b79bd96cb314294b864ae8d7eb3ee8e" + ], + "markers": "python_version < '3.4'", + "version": "==2.2.1" }, "coverage": { "hashes": [ - "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", - "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", - "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", - "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", - "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", - "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", - "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", - "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", - "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", - "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", - "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", - "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", - "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", - "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", - "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", - "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", - "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", - "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", - "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", - "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", - "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", - "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", - "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", - "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", - "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", - "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", - "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", - "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", - "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", - "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", - "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" - ], - "version": "==4.5.2" + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" + ], + "version": "==4.5.3" }, "docutils": { "hashes": [ @@ -145,7 +153,7 @@ "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" ], - "markers": "python_version < '3.0'", + "markers": "python_version < '3.3'", "version": "==1.0.2" }, "functools32": { @@ -196,10 +204,10 @@ }, "pbr": { "hashes": [ - "sha256:a7953f66e1f82e4b061f43096a4bcc058f7d3d41de9b94ac871770e8bdd831a2", - "sha256:d717573351cfe09f49df61906cd272abaa759b3e91744396b804965ff7bff38b" + "sha256:8257baf496c8522437e8a6cfe0f15e00aedc6c0e0e7c9d55eeeeab31e0853843", + "sha256:8c361cc353d988e4f5b998555c88098b9d5964c2e11acf7b0d21925a66bb5824" ], - "version": "==5.1.2" + "version": "==5.1.3" }, "pluggy": { "hashes": [ @@ -224,10 +232,10 @@ }, "pyflakes": { "hashes": [ - "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", - "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd" + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" ], - "version": "==2.1.0" + "version": "==2.1.1" }, "pygments": { "hashes": [ @@ -267,22 +275,30 @@ ], "version": "==2.21.0" }, + "responses": { + "hashes": [ + "sha256:502d9c0c8008439cfcdef7e251f507fcfdd503b56e8c0c87c3c3e3393953f790", + "sha256:97193c0183d63fba8cd3a041c75464e4b09ea0aff6328800d1546598567dde0b" + ], + "index": "pypi", + "version": "==0.10.6" + }, "scandir": { "hashes": [ - "sha256:04b8adb105f2ed313a7c2ef0f1cf7aff4871aa7a1883fa4d8c44b5551ab052d6", - "sha256:1444134990356c81d12f30e4b311379acfbbcd03e0bab591de2696a3b126d58e", - "sha256:1b5c314e39f596875e5a95dd81af03730b338c277c54a454226978d5ba95dbb6", - "sha256:346619f72eb0ddc4cf355ceffd225fa52506c92a2ff05318cfabd02a144e7c4e", - "sha256:44975e209c4827fc18a3486f257154d34ec6eaec0f90fef0cca1caa482db7064", - "sha256:61859fd7e40b8c71e609c202db5b0c1dbec0d5c7f1449dec2245575bdc866792", - "sha256:a5e232a0bf188362fa00123cc0bb842d363a292de7126126df5527b6a369586a", - "sha256:c14701409f311e7a9b7ec8e337f0815baf7ac95776cc78b419a1e6d49889a383", - "sha256:c7708f29d843fc2764310732e41f0ce27feadde453261859ec0fca7865dfc41b", - "sha256:c9009c527929f6e25604aec39b0a43c3f831d2947d89d6caaab22f057b7055c8", - "sha256:f5c71e29b4e2af7ccdc03a020c626ede51da471173b4a6ad1e904f2b2e04b4bd" + "sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e", + "sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022", + "sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f", + "sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f", + "sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae", + "sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173", + "sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4", + "sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32", + "sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188", + "sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d", + "sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac" ], "markers": "python_version < '3.5'", - "version": "==1.9.0" + "version": "==1.10.0" }, "six": { "hashes": [ diff --git a/README.rst b/README.rst index f38ae5b..f4e2c94 100644 --- a/README.rst +++ b/README.rst @@ -141,10 +141,10 @@ understand how this caching works to avoid confusion when attempting to switch b Okta ^^^^ -When aws-okta-processor attempts authentication it will check the system's temporary directory +When aws-okta-processor attempts authentication it will check ``~/.aws-okta-processor/cache/`` for a file named ``--session.json`` based on the ``user`` and ``organization`` option values passed. If the file is not found or the session contents are stale then -aws-okta-processor will create a new session and write it to the system's temporary directory. +aws-okta-processor will create a new session and write it to ``~/.aws-okta-processor/cache/``. If the file exists and the session is not stale then the existing session gets refreshed. ^^^ diff --git a/setup.py b/setup.py index 25e73d4..4b55b69 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,8 @@ TEST_REQUIREMENTS = [ 'pytest-cov', 'pytest-mock', - 'pytest>=2.8.0' + 'pytest>=2.8.0', + 'responses' ] diff --git a/src/aws_okta_processor/__init__.py b/src/aws_okta_processor/__init__.py index 1f356cc..1a72d32 100644 --- a/src/aws_okta_processor/__init__.py +++ b/src/aws_okta_processor/__init__.py @@ -1 +1 @@ -__version__ = '1.0.0' +__version__ = '1.1.0' diff --git a/src/aws_okta_processor/core/okta.py b/src/aws_okta_processor/core/okta.py index c3f3fd1..c8dd90e 100644 --- a/src/aws_okta_processor/core/okta.py +++ b/src/aws_okta_processor/core/okta.py @@ -4,7 +4,6 @@ import json import requests import dateutil -import tempfile import getpass import aws_okta_processor.core.prompt as prompt @@ -52,7 +51,7 @@ def __init__( self.factor = factor self.session = requests.Session() self.organization = organization - self.temp_file_path = self.get_temp_file_path() + self.cache_file_path = self.get_cache_file_path() self.okta_session_id = None okta_session = self.get_okta_session() @@ -73,29 +72,37 @@ def __init__( self.get_okta_session_id() - def get_temp_file_path(self): - temp_directory = tempfile.gettempdir() + def get_cache_file_path(self): + home_directory = os.path.expanduser('~') + cache_directory = os.path.join( + home_directory, + '.aws-okta-processor', + 'cache' + ) + + if not os.path.isdir(cache_directory): + os.makedirs(cache_directory) - temp_file_name = "{}-{}-session.json".format( + cache_file_name = "{}-{}-session.json".format( self.user_name, self.organization ) - temp_file_path = os.path.join(temp_directory, temp_file_name) + cache_file_path = os.path.join(cache_directory, cache_file_name) - return temp_file_path + return cache_file_path def set_okta_session(self, okta_session=None): - with open(self.temp_file_path, "w") as file: + with open(self.cache_file_path, "w") as file: json.dump(okta_session, file) - os.chmod(self.temp_file_path, 0o600) + os.chmod(self.cache_file_path, 0o600) def get_okta_session(self): session = {} - if os.path.isfile(self.temp_file_path): - with open(self.temp_file_path) as file: + if os.path.isfile(self.cache_file_path): + with open(self.cache_file_path) as file: session = json.load(file) return session @@ -177,7 +184,7 @@ def verify_factor(self, factor=None, state_token=None): if "sessionToken" in response_json: return response_json["sessionToken"] - if factor.factor == "push": + if "factorResult" in response_json and factor.factor == "push": if response_json["factorResult"] == "WAITING": factor.link = response_json["_links"]["next"]["href"] time.sleep(1) diff --git a/tests/APPLICATIONS_RESPONSE b/tests/APPLICATIONS_RESPONSE new file mode 100644 index 0000000..4ded643 --- /dev/null +++ b/tests/APPLICATIONS_RESPONSE @@ -0,0 +1,38 @@ +[ + { + "id": "00ub0oNGTSWTBKOLGLNR", + "label": "AWS", + "linkUrl": "https://organization.okta.com/home/amazon_aws/0oa3omz2i9XRNSRIHBZO/270", + "logoUrl": "https://organization.okta.com/img/logos/amazon-aws.png", + "appName": "amazon_aws", + "appInstanceId": "0oa3omz2i9XRNSRIHBZO", + "appAssignmentId": "0ua3omz7weMMMQJERBKY", + "credentialsSetup": false, + "hidden": false, + "sortOrder": 0 + }, + { + "id": "00ub0oNGTSWTBKOLGLNR", + "label": "AWS GOV", + "linkUrl": "https://organization.okta.com/home/amazon_aws/0oa3omz2i9XRNSRIHBZO/272", + "logoUrl": "https://organization.okta.com/img/logos/amazon-aws.png", + "appName": "amazon_aws", + "appInstanceId": "0oa3omz2i9XRNSRIHBZO", + "appAssignmentId": "0ua3omz7weMMMQJERBKY", + "credentialsSetup": false, + "hidden": false, + "sortOrder": 0 + }, + { + "id": "00ub0oNGTSWTBKOLGLNR", + "label": "Google Apps Calendar", + "linkUrl": "https://organization.okta.com/home/google/0oa3omz2i9XRNSRIHBZO/54", + "logoUrl": "https://organization.okta.com/img/logos/google-calendar.png", + "appName": "google", + "appInstanceId": "0oa3omz2i9XRNSRIHBZO", + "appAssignmentId": "0ua3omz7weMMMQJERBKY", + "credentialsSetup": false, + "hidden": false, + "sortOrder": 1 + } +] \ No newline at end of file diff --git a/tests/AUTH_MFA_RESPONSE b/tests/AUTH_MFA_RESPONSE new file mode 100644 index 0000000..01a528a --- /dev/null +++ b/tests/AUTH_MFA_RESPONSE @@ -0,0 +1 @@ +{"status":"MFA_REQUIRED", "stateToken": "state_token", "_embedded": {"factors": [{"factorType": "push", "_links": {"verify": {"href": "https://organization.okta.com/api/v1/authn/factors/id/verify"}}}]}} \ No newline at end of file diff --git a/tests/AUTH_TOKEN_RESPONSE b/tests/AUTH_TOKEN_RESPONSE new file mode 100644 index 0000000..d8ced14 --- /dev/null +++ b/tests/AUTH_TOKEN_RESPONSE @@ -0,0 +1 @@ +{"expiresAt":"2019-04-09T20:17:42.000Z","status":"SUCCESS","sessionToken":"single_use_token","_embedded":{"user":{"id":"foo","profile":{"login":"user@domain.tld","firstName":"foo","lastName":"bar","locale":"en","timeZone":"America/Los_Angeles"}}}} \ No newline at end of file diff --git a/tests/MFA_WAITING_RESPONSE b/tests/MFA_WAITING_RESPONSE new file mode 100644 index 0000000..a661671 --- /dev/null +++ b/tests/MFA_WAITING_RESPONSE @@ -0,0 +1 @@ +{"stateToken": "state_token", "factorResult": "WAITING", "_links": {"next": {"href": "https://organization.okta.com/api/v1/authn/factors/id/lifecycle/activate/poll"}}} \ No newline at end of file diff --git a/tests/SESSION_RESPONSE b/tests/SESSION_RESPONSE new file mode 100644 index 0000000..5f7cf22 --- /dev/null +++ b/tests/SESSION_RESPONSE @@ -0,0 +1 @@ +{"id": "session_token", "userId": "bar", "login": "user@domain.tld", "createdAt": "2019-04-08T18:37:43.000Z", "expiresAt": "2019-04-09T06:37:43.000Z", "status": "ACTIVE", "lastPasswordVerification": "2019-04-08T18:37:43.000Z", "lastFactorVerification": null, "amr": ["pwd"], "idp": {"id": "foo", "type": "bar"}, "mfaActive": false, "_links": {"self": {"href": "https://organization.okta.com/api/v1/sessions/me", "hints": {"allow": ["GET", "DELETE"]}}, "refresh": {"href": "https://organization.okta.com/api/v1/sessions/me/lifecycle/refresh", "hints": {"allow": ["POST"]}}, "user": {"name": "Foo", "href": "https://organization.okta.com/api/v1/users/me", "hints": {"allow": ["GET"]}}}} \ No newline at end of file diff --git a/tests/core/test_okta.py b/tests/core/test_okta.py index 1194114..04aabf8 100644 --- a/tests/core/test_okta.py +++ b/tests/core/test_okta.py @@ -1,45 +1,646 @@ -from unittest import TestCase +from test_base import TestBase +from test_base import SESSION_RESPONSE +from test_base import AUTH_TOKEN_RESPONSE +from test_base import AUTH_MFA_RESPONSE +from test_base import MFA_WAITING_RESPONSE +from test_base import APPLICATIONS_RESPONSE +from test_base import SAML_RESPONSE + from mock import patch +from mock import call from mock import MagicMock +from datetime import datetime +from collections import OrderedDict +from requests import ConnectionError +from requests import ConnectTimeout from aws_okta_processor.core.okta import Okta +import responses +import json + + +class StubDate(datetime): + pass -class TestOkta(TestCase): + +class TestOkta(TestBase): + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') @patch('aws_okta_processor.core.okta.print_tty') - @patch('aws_okta_processor.core.okta.Okta.set_okta_session') - @patch('aws_okta_processor.core.okta.Okta.get_okta_session') - @patch('aws_okta_processor.core.okta.Okta.get_okta_single_use_token') - @patch('aws_okta_processor.core.okta.requests') + @responses.activate def test_okta( self, - mock_requests, - mock_get_session_token, - mock_get_session, - mock_set_session, - mock_print_tty + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod ): - mock_session = MagicMock() - mock_requests.Session.return_value = mock_session - mock_get_session_token.return_value = "single_use_token" - mock_get_session.return_value = { - 'id': 'session_id', - 'expiresAt': '2019-01-22T19:24:24Z' - } + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_TOKEN_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions', + json=json.loads(SESSION_RESPONSE) + ) okta = Okta( user_name="user_name", user_pass="user_pass", - organization="organization_domain", - factor="factor_type" + organization="organization.okta.com" ) - self.assertIs(okta.session, mock_session) self.assertEqual(okta.okta_single_use_token, "single_use_token") - self.assertEqual(okta.organization, "organization_domain") - self.assertEqual(okta.factor, "factor_type") + self.assertEqual(okta.organization, "organization.okta.com") + self.assertEqual(okta.okta_session_id, "session_token") + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.getpass') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_no_pass( + self, + mock_print_tty, + mock_makedirs, + mock_getpass, + mock_open, + mock_chmod + ): + mock_getpass.getpass.return_value = "user_pass" + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_TOKEN_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions', + json=json.loads(SESSION_RESPONSE) + ) + + okta = Okta( + user_name="user_name", + organization="organization.okta.com" + ) + + mock_getpass.getpass.assert_called_once() + self.assertEqual(okta.okta_single_use_token, "single_use_token") + self.assertEqual(okta.organization, "organization.okta.com") + self.assertEqual(okta.okta_session_id, "session_token") + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.datetime', StubDate) + @patch('aws_okta_processor.core.okta.os.path.isfile') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_cached_session( + self, + mock_print_tty, + mock_makedirs, + mock_isfile, + mock_open, + mock_chmod + ): + StubDate.now = classmethod(lambda cls, tz: datetime(1, 1, 1, 0, 0, tzinfo=tz)) + + mock_isfile.return_value = True + mock_enter = MagicMock() + mock_enter.read.return_value = SESSION_RESPONSE + mock_open().__enter__.return_value = mock_enter + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions/me/lifecycle/refresh', + json=json.loads(SESSION_RESPONSE) + ) + + okta = Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + self.assertEqual(okta.okta_session_id, "session_token") + self.assertEqual(okta.organization, "organization.okta.com") + + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_auth_value_error( + self, + mock_print_tty, + mock_makedirs + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + body="NOT JSON", + status=500 + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Invalid JSON") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_auth_send_error( + self, + mock_print_tty, + mock_makedirs + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json={ + "status": "foo", + "errorSummary": "bar" + }, + status=500 + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Status: foo"), + call("Error: Summary: bar") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_mfa_challenge( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_MFA_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn/factors/id/verify', + json=json.loads(MFA_WAITING_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn/factors/id/lifecycle/activate/poll', + json=json.loads(AUTH_TOKEN_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions', + json=json.loads(SESSION_RESPONSE) + ) + + okta = Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + self.assertEqual(okta.okta_single_use_token, "single_use_token") + self.assertEqual(okta.organization, "organization.okta.com") + self.assertEqual(okta.okta_session_id, "session_token") + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_mfa_verify_value_error( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_MFA_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn/factors/id/verify', + body="NOT JSON", + status=500 + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Invalid JSON") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_mfa_verify_send_error( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_MFA_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn/factors/id/verify', + json={ + "status": "foo", + "errorSummary": "bar" + }, + status=500 + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Status: foo"), + call("Error: Summary: bar") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_session_id_key_error( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_TOKEN_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions', + json={ + "status": "foo", + "errorSummary": "bar" + }, + status=500 + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Status: foo"), + call("Error: Summary: bar") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_session_id_value_error( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_TOKEN_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions', + body="NOT JSON", + status=500 + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Invalid JSON") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.datetime', StubDate) + @patch('aws_okta_processor.core.okta.os.path.isfile') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_refresh_key_error( + self, + mock_print_tty, + mock_makedirs, + mock_isfile, + mock_open, + mock_chmod + ): + StubDate.now = classmethod(lambda cls, tz: datetime(1, 1, 1, 0, 0, tzinfo=tz)) - mock_get_session_token.assert_called_once_with( + mock_isfile.return_value = True + mock_enter = MagicMock() + mock_enter.read.return_value = SESSION_RESPONSE + mock_open().__enter__.return_value = mock_enter + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions/me/lifecycle/refresh', + json={ + "status": "foo", + "errorSummary": "bar" + }, + status=500 + ) + + Okta( user_name="user_name", - user_pass="user_pass" + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Status: foo"), + call("Error: Summary: bar") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.datetime', StubDate) + @patch('aws_okta_processor.core.okta.os.path.isfile') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_refresh_value_error( + self, + mock_print_tty, + mock_makedirs, + mock_isfile, + mock_open, + mock_chmod + ): + StubDate.now = classmethod(lambda cls, tz: datetime(1, 1, 1, 0, 0, tzinfo=tz)) + + mock_isfile.return_value = True + mock_enter = MagicMock() + mock_enter.read.return_value = SESSION_RESPONSE + mock_open().__enter__.return_value = mock_enter + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions/me/lifecycle/refresh', + body="bob", + status=500 ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Status Code: 500"), + call("Error: Invalid JSON") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_get_applications( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_TOKEN_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions', + json=json.loads(SESSION_RESPONSE) + ) + + responses.add( + responses.GET, + 'https://organization.okta.com/api/v1/users/me/appLinks', + json=json.loads(APPLICATIONS_RESPONSE) + ) + + okta = Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + applications = okta.get_applications() + expected_applications = OrderedDict( + [ + ('AWS', 'https://organization.okta.com/home/amazon_aws/0oa3omz2i9XRNSRIHBZO/270'), + ('AWS GOV', 'https://organization.okta.com/home/amazon_aws/0oa3omz2i9XRNSRIHBZO/272') + ] + ) + + self.assertEqual(applications, expected_applications) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_get_saml_response( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + json=json.loads(AUTH_TOKEN_RESPONSE) + ) + + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/sessions', + json=json.loads(SESSION_RESPONSE) + ) + + responses.add( + responses.GET, + 'https://organization.okta.com/home/amazon_aws/0oa3omz2i9XRNSRIHBZO/270', + body=SAML_RESPONSE + ) + + okta = Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + saml_response = okta.get_saml_response( + application_url='https://organization.okta.com/home/amazon_aws/0oa3omz2i9XRNSRIHBZO/270' + ) + + self.assertEqual(saml_response, SAML_RESPONSE) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_connection_timeout( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + body=ConnectTimeout() + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Timed Out") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) + + @patch('aws_okta_processor.core.okta.os.chmod') + @patch('aws_okta_processor.core.okta.open') + @patch('aws_okta_processor.core.okta.os.makedirs') + @patch('aws_okta_processor.core.okta.print_tty') + @responses.activate + def test_okta_connection_error( + self, + mock_print_tty, + mock_makedirs, + mock_open, + mock_chmod + ): + responses.add( + responses.POST, + 'https://organization.okta.com/api/v1/authn', + body=ConnectionError() + ) + + with self.assertRaises(SystemExit): + Okta( + user_name="user_name", + user_pass="user_pass", + organization="organization.okta.com" + ) + + print_tty_calls = [ + call("Error: Connection Error") + ] + + mock_print_tty.assert_has_calls(print_tty_calls) diff --git a/tests/test_base.py b/tests/test_base.py index a118a0c..e6ce2f1 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -8,6 +8,16 @@ SAML_RESPONSE = open(SAML_RESPONSE_PATH, 'r').read() SIGN_IN_RESPONSE_PATH = os.path.join(ABS_PATH, "SIGN_IN_RESPONSE") SIGN_IN_RESPONSE = open(SIGN_IN_RESPONSE_PATH, 'r').read() +SESSION_RESPONSE_PATH = os.path.join(ABS_PATH, "SESSION_RESPONSE") +SESSION_RESPONSE = open(SESSION_RESPONSE_PATH, 'r').read() +AUTH_TOKEN_RESPONSE_PATH = os.path.join(ABS_PATH, "AUTH_TOKEN_RESPONSE") +AUTH_TOKEN_RESPONSE = open(AUTH_TOKEN_RESPONSE_PATH, 'r').read() +AUTH_MFA_RESPONSE_PATH = os.path.join(ABS_PATH, "AUTH_MFA_RESPONSE") +AUTH_MFA_RESPONSE = open(AUTH_MFA_RESPONSE_PATH, 'r').read() +MFA_WAITING_RESPONSE_PATH = os.path.join(ABS_PATH, "MFA_WAITING_RESPONSE") +MFA_WAITING_RESPONSE = open(MFA_WAITING_RESPONSE_PATH, 'r').read() +APPLICATIONS_RESPONSE_PATH = os.path.join(ABS_PATH, "APPLICATIONS_RESPONSE") +APPLICATIONS_RESPONSE = open(APPLICATIONS_RESPONSE_PATH, 'r').read() class TestBase(TestCase):