From 61076547771ef2c739b9898a148dacf447f0afdc Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 12:29:17 -0400 Subject: [PATCH 01/11] Reverted code partially, known-working state --- gremlinapi/oauth.py | 109 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/gremlinapi/oauth.py b/gremlinapi/oauth.py index 2a4ae7e..b609c7b 100644 --- a/gremlinapi/oauth.py +++ b/gremlinapi/oauth.py @@ -295,6 +295,115 @@ def authenticate( bearer_token : str """ + + COMPANY_NAME = "Hooli" + LOGIN_ENDPOINT = "https://api.gremlin.com/v1/oauth/login" + SSO_ENDPOINT = "https://api.gremlin.com/v1/users/auth/sso" + + GREMLIN_COMPANY = "Hooli" + USERNAME = "kyle.bouchard+demo@gremlin.com" + GREMLIN_USER_MOCK = "fakeemail@googlecom" + PASSWORD = "***REMOVED***" + GREMLIN_PASSWORD_MOCK = "qwertyuiopoiuytrewq" + + # USERNAME = "test@gremlin.com" + # PASSWORD = "*********" + + # Initiates OAuth login with Gremlin + payload: dict = cls._payload(**{"headers": https_client.header()}) + (login_response, body) = https_client.api_call( + "GET", f"{LOGIN_ENDPOINT}?companyName={COMPANY_NAME}", **payload + ) + print(str(login_response)) + assert login_response.status_code == 307 + + # Response contains redirect to customers OAuth provider and OAuth state in cookie + # Extract cookie to be used later and then follow redirect and finish authentication. + state_cookie = login_response.cookies['oauth_state'] + oauth_provider_login_url = login_response.headers['Location'] + + assert state_cookie != None + assert oauth_provider_login_url != None + + + # This part is implementation specific depending on customer. + # Different OAuth providers may require different things when authenticating the user. + # Also for the first time they auth, a browser may be required so that they can grant + # gremlin access based on the scope we request. + + # In this example I'm using a hosted mock oauth provider `doshmajhan.mocklab.io`, + # so I just make a post request to the login endpoint + # with my credentials and it gives me a redirect back to Gremlin with the oauth code + # oauth_provider_login_page = requests.get(oauth_provider_login_url) + # payload: dict = {} + # (oauth_provider_login_page, body) = https_client.api_call( + # "GET", oauth_provider_login_url, **payload + # ) + + body = { + "email": USERNAME, + "password": PASSWORD, + "state": state_cookie, + "redirectUri": "https://api.gremlin.com/v1/oauth/callback", + "clientId": "clientId" + } + + # Don't follow redirect as we need to add the state cookie to the request + # oauth_provider_login_response = requests.post("http://doshmajhan.mocklab.io/login", data=body, allow_redirects=False) + payload: dict = cls._payload(**{"headers": https_client.header(), "data": body}) + (oauth_provider_login_response, body) = https_client.api_call( + "POST", "http://doshmajhan.mocklab.io/login", **payload + ) + assert oauth_provider_login_response.status_code == 302 + + + # We've successfully authenticted with our OAuth provider, now we continue the flow by following + # the redirect our OAuth provider created back to Gremlins /oauth/callback endpoint + gremlin_callback_url = oauth_provider_login_response.headers['Location'] + assert gremlin_callback_url != None + log.info(gremlin_callback_url) + # Add state cookie and then follow redirecet to gremlins /oauth/callback endpoint + # If the state cookie is not added the request will fail. Theres a state parameter in the + # rediect URL we're following and it needs to match the cookie. This helps prevent CSRF attacks. + cookie = { + 'oauth_state': state_cookie + } + # gremlin_callback_response = requests.get(gremlin_callback_url, cookies=cookie) + (gremlin_callback_response, body) = https_client.api_call( + "GET", gremlin_callback_url, **{"cookies":cookie} + ) + + # The response from the callback endpoint will contain the access_token in JSON + # This is the end of the OAuth specific flow. This access_token can now be exchanged + # for a gremlin session. + assert gremlin_callback_response.status_code == 200 + + access_token = gremlin_callback_response.json()['access_token'] + assert access_token != None + + # Make request to /users/auth/sso to exchange access_token for a Gremlin session + body = { + "companyName": COMPANY_NAME, + "accessToken": access_token, + "provider": "oauth", + } + # sso_response = requests.post(f"{SSO_ENDPOINT}?getCompanySession=true", data=body) + payload: dict = cls._payload(**{"data": body}) + (sso_response, body) = https_client.api_call( + "POST", f"{SSO_ENDPOINT}?getCompanySession=true", **payload + ) + print(str(sso_response)) + assert sso_response.status_code == 200 + + # Get bearer token from session + bearer_token = sso_response.json()['header'] + assert bearer_token != None + + print(bearer_token) + return bearer_token + + + company_name = cls._error_if_not_param("companyName", **kwargs) (state_cookie, oauth_provider_login_url) = cls.initiate_oauth( company_name, https_client From 3c47f097a943b2226a032451785a3a4a7521c285 Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 12:29:28 -0400 Subject: [PATCH 02/11] =?UTF-8?q?Bump=20version:=200.13.1=20=E2=86=92=200.?= =?UTF-8?q?14.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- gremlinapi/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ab757e8..de12a5a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.13.1 +current_version = 0.14.4 commit = True tag = True diff --git a/gremlinapi/util.py b/gremlinapi/util.py index a9ed625..8f081ca 100644 --- a/gremlinapi/util.py +++ b/gremlinapi/util.py @@ -7,7 +7,7 @@ log = logging.getLogger("GremlinAPI.client") -_version = "0.13.1" +_version = "0.14.4" def get_version(): From a77736e211b4bc4db86be555fe16a389ac4b3a90 Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 12:55:02 -0400 Subject: [PATCH 03/11] split and refactor, known working state --- gremlinapi/oauth.py | 142 +++++++++++--------------------------------- 1 file changed, 34 insertions(+), 108 deletions(-) diff --git a/gremlinapi/oauth.py b/gremlinapi/oauth.py index b609c7b..82b68b3 100644 --- a/gremlinapi/oauth.py +++ b/gremlinapi/oauth.py @@ -118,7 +118,6 @@ def initiate_oauth( ) # Initiates OAUTH login with Gremlin - # `allow_redirects` = False enables capture of the response to extract the state cookie # `status_code` 307 is a redirect to the OAUTH provider payload: dict = cls._payload(**{"headers": https_client.header()}) (resp, body) = https_client.api_call("GET", endpoint, **payload) @@ -164,7 +163,29 @@ def get_callback_url( gremlin_callback_url : str """ - payload: dict = cls._payload(**{"headers": https_client.header(), "body": data}) +# body = { + # "email": USERNAME, + # "password": PASSWORD, + # "state": state_cookie, + # "redirectUri": "https://api.gremlin.com/v1/oauth/callback", + # "clientId": "clientId" + # } + + # # Don't follow redirect as we need to add the state cookie to the request + # # oauth_provider_login_response = requests.post("http://doshmajhan.mocklab.io/login", data=body, allow_redirects=False) + # payload: dict = cls._payload(**{"headers": https_client.header(), "data": body}) + # (oauth_provider_login_response, body) = https_client.api_call( + # "POST", "http://doshmajhan.mocklab.io/login", **payload + # ) + # assert oauth_provider_login_response.status_code == 302 + + + # # We've successfully authenticted with our OAuth provider, now we continue the flow by following + # # the redirect our OAuth provider created back to Gremlins /oauth/callback endpoint + # gremlin_callback_url = oauth_provider_login_response.headers['Location'] + # assert gremlin_callback_url != None + + payload: dict = cls._payload(**{"headers": https_client.header(), "data": data}) (resp, body) = https_client.api_call( "POST", oauth_provider_login_url, **payload ) @@ -208,12 +229,13 @@ def get_access_token( # added the request will fail. There is a state parameter in the # redirect URL you are following and it needs to match the # value in the cookie. This helps prevent CSRF attacks. - cookie = {"oauth_state": state_cookie} - payload: dict = cls._payload( - **{"headers": https_client.header(), "cookies": cookie} + cookie = { + 'oauth_state': state_cookie + } + # gremlin_callback_response = requests.get(gremlin_callback_url, cookies=cookie) + (resp, body) = https_client.api_call( + "GET", gremlin_callback_url, **{"cookies":cookie} ) - (resp, body) = https_client.api_call("GET", gremlin_callback_url, **payload) - # gremlin_callback_response = requests.get(gremlin_callback_url, cookies=cookie) # The response from the callback endpoint will contain the `access_token` in JSON # This is the end of the OAuth specific flow. This `access_token` can @@ -252,7 +274,7 @@ def get_bearer_token( "accessToken": access_token, "provider": "oauth", } - payload: dict = cls._payload(**{"headers": https_client.header(), "body": body}) + payload: dict = cls._payload(**{"data": body}) endpoint = f"https://api.gremlin.com/v1/users/auth/sso?getCompanySession=true" (resp, body) = https_client.api_call("POST", endpoint, **payload) assert resp.status_code == 200 @@ -298,6 +320,7 @@ def authenticate( COMPANY_NAME = "Hooli" LOGIN_ENDPOINT = "https://api.gremlin.com/v1/oauth/login" + OAUTH_LOGIN = "http://doshmajhan.mocklab.io/login" SSO_ENDPOINT = "https://api.gremlin.com/v1/users/auth/sso" GREMLIN_COMPANY = "Hooli" @@ -306,104 +329,6 @@ def authenticate( PASSWORD = "***REMOVED***" GREMLIN_PASSWORD_MOCK = "qwertyuiopoiuytrewq" - # USERNAME = "test@gremlin.com" - # PASSWORD = "*********" - - # Initiates OAuth login with Gremlin - payload: dict = cls._payload(**{"headers": https_client.header()}) - (login_response, body) = https_client.api_call( - "GET", f"{LOGIN_ENDPOINT}?companyName={COMPANY_NAME}", **payload - ) - print(str(login_response)) - assert login_response.status_code == 307 - - # Response contains redirect to customers OAuth provider and OAuth state in cookie - # Extract cookie to be used later and then follow redirect and finish authentication. - state_cookie = login_response.cookies['oauth_state'] - oauth_provider_login_url = login_response.headers['Location'] - - assert state_cookie != None - assert oauth_provider_login_url != None - - - # This part is implementation specific depending on customer. - # Different OAuth providers may require different things when authenticating the user. - # Also for the first time they auth, a browser may be required so that they can grant - # gremlin access based on the scope we request. - - # In this example I'm using a hosted mock oauth provider `doshmajhan.mocklab.io`, - # so I just make a post request to the login endpoint - # with my credentials and it gives me a redirect back to Gremlin with the oauth code - # oauth_provider_login_page = requests.get(oauth_provider_login_url) - # payload: dict = {} - # (oauth_provider_login_page, body) = https_client.api_call( - # "GET", oauth_provider_login_url, **payload - # ) - - body = { - "email": USERNAME, - "password": PASSWORD, - "state": state_cookie, - "redirectUri": "https://api.gremlin.com/v1/oauth/callback", - "clientId": "clientId" - } - - # Don't follow redirect as we need to add the state cookie to the request - # oauth_provider_login_response = requests.post("http://doshmajhan.mocklab.io/login", data=body, allow_redirects=False) - payload: dict = cls._payload(**{"headers": https_client.header(), "data": body}) - (oauth_provider_login_response, body) = https_client.api_call( - "POST", "http://doshmajhan.mocklab.io/login", **payload - ) - assert oauth_provider_login_response.status_code == 302 - - - # We've successfully authenticted with our OAuth provider, now we continue the flow by following - # the redirect our OAuth provider created back to Gremlins /oauth/callback endpoint - gremlin_callback_url = oauth_provider_login_response.headers['Location'] - assert gremlin_callback_url != None - log.info(gremlin_callback_url) - # Add state cookie and then follow redirecet to gremlins /oauth/callback endpoint - # If the state cookie is not added the request will fail. Theres a state parameter in the - # rediect URL we're following and it needs to match the cookie. This helps prevent CSRF attacks. - cookie = { - 'oauth_state': state_cookie - } - # gremlin_callback_response = requests.get(gremlin_callback_url, cookies=cookie) - (gremlin_callback_response, body) = https_client.api_call( - "GET", gremlin_callback_url, **{"cookies":cookie} - ) - - # The response from the callback endpoint will contain the access_token in JSON - # This is the end of the OAuth specific flow. This access_token can now be exchanged - # for a gremlin session. - assert gremlin_callback_response.status_code == 200 - - access_token = gremlin_callback_response.json()['access_token'] - assert access_token != None - - # Make request to /users/auth/sso to exchange access_token for a Gremlin session - body = { - "companyName": COMPANY_NAME, - "accessToken": access_token, - "provider": "oauth", - } - # sso_response = requests.post(f"{SSO_ENDPOINT}?getCompanySession=true", data=body) - payload: dict = cls._payload(**{"data": body}) - (sso_response, body) = https_client.api_call( - "POST", f"{SSO_ENDPOINT}?getCompanySession=true", **payload - ) - print(str(sso_response)) - assert sso_response.status_code == 200 - - # Get bearer token from session - bearer_token = sso_response.json()['header'] - assert bearer_token != None - - print(bearer_token) - return bearer_token - - - company_name = cls._error_if_not_param("companyName", **kwargs) (state_cookie, oauth_provider_login_url) = cls.initiate_oauth( company_name, https_client @@ -415,7 +340,8 @@ def authenticate( "redirectUri": "https://api.gremlin.com/v1/oauth/callback", "clientId": cls._error_if_not_param("clientId", **kwargs), } - gremlin_callback_url = cls.get_callback_url(oauth_provider_login_url, auth_body) + gremlin_callback_url = cls.get_callback_url(OAUTH_LOGIN, auth_body) access_token = cls.get_access_token(state_cookie, gremlin_callback_url) - bearer_token = cls.get_bearer_token(company_name, access_token) + bearer_token = cls.get_bearer_token(COMPANY_NAME, access_token) + print(bearer_token) return bearer_token From a6b6dc03fd64ee54fda2769045dafd5af1c3c34b Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 12:55:09 -0400 Subject: [PATCH 04/11] =?UTF-8?q?Bump=20version:=200.14.4=20=E2=86=92=200.?= =?UTF-8?q?14.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- gremlinapi/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index de12a5a..bfabe7f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.4 +current_version = 0.14.5 commit = True tag = True diff --git a/gremlinapi/util.py b/gremlinapi/util.py index 8f081ca..8ef0dc9 100644 --- a/gremlinapi/util.py +++ b/gremlinapi/util.py @@ -7,7 +7,7 @@ log = logging.getLogger("GremlinAPI.client") -_version = "0.14.4" +_version = "0.14.5" def get_version(): From f4f0eca5b3bcc8196e7b654328cd3589acd39e2a Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 13:15:12 -0400 Subject: [PATCH 05/11] OAUTH now works as intended, removed hardcoded Gremlin URIs to util --- gremlinapi/oauth.py | 85 +++++++++++---------------------------------- gremlinapi/util.py | 5 +++ 2 files changed, 26 insertions(+), 64 deletions(-) diff --git a/gremlinapi/oauth.py b/gremlinapi/oauth.py index 82b68b3..391b9f7 100644 --- a/gremlinapi/oauth.py +++ b/gremlinapi/oauth.py @@ -26,13 +26,18 @@ from typing import Union, Type, Any, Tuple +from gremlinapi.util import ( + GREMLIN_OAUTH_LOGIN, + GREMLIN_OAUTH_COMPANIES_URI, + GREMLIN_SSO_USER_AUTH, + GREMLIN_OAUTH_CALLBACK, +) log = logging.getLogger("GremlinAPI.client") class GremlinAPIOAUTH(GremlinAPI): @classmethod - @experimental("`configure` method not fully implemented") def configure( cls, company_id: str = "", @@ -78,9 +83,7 @@ def configure( log.error(error_msg) raise GremlinParameterError(error_msg) - endpoint: str = ( - f"https://api.gremlin.com/v1/companies/{company_id}/oauth/settings" - ) + endpoint: str = f"{GREMLIN_OAUTH_COMPANIES_URI}/{company_id}/oauth/settings" data: dict = { "authorizationUri": cls._error_if_not_param("authorizationUri", **kwargs), "tokenUri": cls._error_if_not_param("tokenUri", **kwargs), @@ -94,7 +97,6 @@ def configure( return resp.status_code @classmethod - @experimental def initiate_oauth( cls, company_name: str, @@ -113,9 +115,7 @@ def initiate_oauth( state_cookie, oauth_provider_login_url : Tuple[str, str] """ - endpoint: str = ( - f"https://api.gremlin.com/v1/oauth/login?companyName={company_name}" - ) + endpoint: str = f"{GREMLIN_OAUTH_LOGIN}?companyName={company_name}" # Initiates OAUTH login with Gremlin # `status_code` 307 is a redirect to the OAUTH provider @@ -131,7 +131,6 @@ def initiate_oauth( return state_cookie, oauth_provider_login_url @classmethod - @experimental def get_callback_url( cls, oauth_provider_login_url: str, @@ -163,28 +162,6 @@ def get_callback_url( gremlin_callback_url : str """ -# body = { - # "email": USERNAME, - # "password": PASSWORD, - # "state": state_cookie, - # "redirectUri": "https://api.gremlin.com/v1/oauth/callback", - # "clientId": "clientId" - # } - - # # Don't follow redirect as we need to add the state cookie to the request - # # oauth_provider_login_response = requests.post("http://doshmajhan.mocklab.io/login", data=body, allow_redirects=False) - # payload: dict = cls._payload(**{"headers": https_client.header(), "data": body}) - # (oauth_provider_login_response, body) = https_client.api_call( - # "POST", "http://doshmajhan.mocklab.io/login", **payload - # ) - # assert oauth_provider_login_response.status_code == 302 - - - # # We've successfully authenticted with our OAuth provider, now we continue the flow by following - # # the redirect our OAuth provider created back to Gremlins /oauth/callback endpoint - # gremlin_callback_url = oauth_provider_login_response.headers['Location'] - # assert gremlin_callback_url != None - payload: dict = cls._payload(**{"headers": https_client.header(), "data": data}) (resp, body) = https_client.api_call( "POST", oauth_provider_login_url, **payload @@ -193,16 +170,11 @@ def get_callback_url( # You have now successfully authenticted with your OAuth provider, # now continue the flow by following the redirect your OAuth provider # created back to Gremlins /oauth/callback endpoint - # log.info(str(resp.headers)) gremlin_callback_url = resp.headers["Location"] - # gremlin_callback_url = auth_body["redirectUri"] assert gremlin_callback_url != None - # NOTE TO SELF: the `location` key is nonexistant on the resp, and the - # redirectUri is `null` in the original state cookie as returned from the mock endpoint return gremlin_callback_url @classmethod - @experimental def get_access_token( cls, state_cookie: str, @@ -229,14 +201,10 @@ def get_access_token( # added the request will fail. There is a state parameter in the # redirect URL you are following and it needs to match the # value in the cookie. This helps prevent CSRF attacks. - cookie = { - 'oauth_state': state_cookie - } - # gremlin_callback_response = requests.get(gremlin_callback_url, cookies=cookie) + cookie = {"oauth_state": state_cookie} (resp, body) = https_client.api_call( - "GET", gremlin_callback_url, **{"cookies":cookie} + "GET", gremlin_callback_url, **{"cookies": cookie} ) - # gremlin_callback_response = requests.get(gremlin_callback_url, cookies=cookie) # The response from the callback endpoint will contain the `access_token` in JSON # This is the end of the OAuth specific flow. This `access_token` can # now be exchanged for a Gremlin session. @@ -247,7 +215,6 @@ def get_access_token( return access_token @classmethod - @experimental def get_bearer_token( cls, company_name: str, @@ -275,7 +242,7 @@ def get_bearer_token( "provider": "oauth", } payload: dict = cls._payload(**{"data": body}) - endpoint = f"https://api.gremlin.com/v1/users/auth/sso?getCompanySession=true" + endpoint = f"{GREMLIN_SSO_USER_AUTH}?getCompanySession=true" (resp, body) = https_client.api_call("POST", endpoint, **payload) assert resp.status_code == 200 @@ -289,7 +256,6 @@ def get_bearer_token( return bearer_token @classmethod - @experimental def authenticate( cls, https_client: Type[GremlinAPIHttpClient] = get_gremlin_httpclient(), @@ -306,10 +272,11 @@ def authenticate( `kwargs` is required in the following format: { - companyName : The company for which the oauth authentication should commence, + company_name : The company for which the oauth authentication should commence, email: Login email for your user, password: Login password for your user, - clientId: Client Id obtained when registering Gremlin with your OAuth provider, + client_id: Client Id obtained when registering Gremlin with your OAuth provider, + oauth_login_uri: The login Uri from your OAUTH provider, } Returns @@ -318,18 +285,7 @@ def authenticate( """ - COMPANY_NAME = "Hooli" - LOGIN_ENDPOINT = "https://api.gremlin.com/v1/oauth/login" - OAUTH_LOGIN = "http://doshmajhan.mocklab.io/login" - SSO_ENDPOINT = "https://api.gremlin.com/v1/users/auth/sso" - - GREMLIN_COMPANY = "Hooli" - USERNAME = "kyle.bouchard+demo@gremlin.com" - GREMLIN_USER_MOCK = "fakeemail@googlecom" - PASSWORD = "***REMOVED***" - GREMLIN_PASSWORD_MOCK = "qwertyuiopoiuytrewq" - - company_name = cls._error_if_not_param("companyName", **kwargs) + company_name = cls._error_if_not_param("company_name", **kwargs) (state_cookie, oauth_provider_login_url) = cls.initiate_oauth( company_name, https_client ) @@ -337,11 +293,12 @@ def authenticate( "email": cls._error_if_not_param("email", **kwargs), "password": cls._error_if_not_param("password", **kwargs), "state": state_cookie, # obtained in earlier step - "redirectUri": "https://api.gremlin.com/v1/oauth/callback", - "clientId": cls._error_if_not_param("clientId", **kwargs), + "redirectUri": GREMLIN_OAUTH_CALLBACK, + "clientId": cls._error_if_not_param("client_id", **kwargs), } - gremlin_callback_url = cls.get_callback_url(OAUTH_LOGIN, auth_body) + gremlin_callback_url = cls.get_callback_url( + cls._error_if_not_param("oauth_login_uri", **kwargs), auth_body + ) access_token = cls.get_access_token(state_cookie, gremlin_callback_url) - bearer_token = cls.get_bearer_token(COMPANY_NAME, access_token) - print(bearer_token) + bearer_token = cls.get_bearer_token(company_name, access_token) return bearer_token diff --git a/gremlinapi/util.py b/gremlinapi/util.py index 8ef0dc9..70f1c22 100644 --- a/gremlinapi/util.py +++ b/gremlinapi/util.py @@ -16,6 +16,11 @@ def get_version(): string_types = (type(b""), type(""), type(f"")) +GREMLIN_OAUTH_LOGIN = "https://api.gremlin.com/v1/oauth/login" +GREMLIN_OAUTH_COMPANIES_URI = "https://api.gremlin.com/v1/companies" +GREMLIN_SSO_USER_AUTH = "https://api.gremlin.com/v1/users/auth/sso" +GREMLIN_OAUTH_CALLBACK = "https://api.gremlin.com/v1/oauth/callback" + def experimental(func): """ From 1463d7f201a57df68a90473f1d4e957e9d08e342 Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 13:15:33 -0400 Subject: [PATCH 06/11] =?UTF-8?q?Bump=20version:=200.14.5=20=E2=86=92=200.?= =?UTF-8?q?14.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- gremlinapi/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index bfabe7f..25146ce 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.5 +current_version = 0.14.6 commit = True tag = True diff --git a/gremlinapi/util.py b/gremlinapi/util.py index 70f1c22..ca51300 100644 --- a/gremlinapi/util.py +++ b/gremlinapi/util.py @@ -7,7 +7,7 @@ log = logging.getLogger("GremlinAPI.client") -_version = "0.14.5" +_version = "0.14.6" def get_version(): From 591d391fd295cd08791094b4e3b6e7294e11d50c Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 13:42:18 -0400 Subject: [PATCH 07/11] updated OAUTH documentation --- README.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 1939968..2c9c9a2 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,6 @@ config.team_id = team_id #### Authentication with OAUTH -***Experimental - not fully implemented*** - To authentication through a desired OAUTH workflow, the required information is similar to `gremlinapi.login()`. When successfully authenticated through OAUTH, the bearer token, used later in the API workflow, is returned. @@ -125,15 +123,17 @@ When successfully authenticated through OAUTH, the bearer token, used later in t ```python from gremlinapi.oauth import GremlinAPIOAUTH -GREMLIN_COMPANY = "Hooli" -GREMLIN_USER = "your.login.email@domain.com" -GREMLIN_PASSWORD = "y0urPa$$w0rd" +GREMLIN_COMPANY = "Your Company Name" +USERNAME = "your.login.email@domain.com" +PASSWORD = "y0urPa$$w0rd" +OAUTH_LOGIN = "http://your.oauth.provider/login" auth_args = { - "email":GREMLIN_USER, - "password": GREMLIN_PASSWORD, - "clientId": "mocklab_oauth2", - "companyName": GREMLIN_COMPANY, + "email":USERNAME, + "password": PASSWORD, + "client_id": "mocklab_oauth2", + "company_name": GREMLIN_COMPANY, + "oauth_login_uri": OAUTH_LOGIN, } bearer_token = GremlinAPIOAUTH.authenticate(**auth_args) @@ -141,8 +141,6 @@ bearer_token = GremlinAPIOAUTH.authenticate(**auth_args) #### OAUTH Configuration -***Experimental - not fully implemented*** - OAUTH can be configured through an API endpoint per the following configuration dictionary and code example. You must previous be logged in or otherwise authenticated for the below code to succeed. @@ -154,15 +152,15 @@ GREMLIN_TEAM_ID = "your-team-id" config_body = { # Used to authenticate against the OAuth provider. We will redirect the user to this URL when they initate a OAuth login. - "authorizationUri": "your-authorization-uri", + "authorizationUri": "http://your.oauth.provider/authorize", # Used to exchange an OAuth code, obtained after logging into the OAuth provider, for an access token. - "tokenUri": "your-token-uri", + "tokenUri": "http://your.oauth.provider/oauth/token", # Used to query for the email of the user.. - "userInfoUri": "your-userinfo-uri", + "userInfoUri": "http://your.oauth.provider/userinfo", # The public identifier obtained when registering Gremlin with your OAuth provider. - "clientId": "mocklab_oauth2", + "clientId": "your_client_id", # The secret obtained when registering Gremlin with your OAuth provider. - "clientSecret": "foo", + "clientSecret": "your_client_secret", # Define what level of access the access token will have that Gremlin obtains during the OAuth login. The default is `email`. If you change it from the default, the scope provided must be able to read the email of the user. "scope":"email", } From 4d2efa4345a93c48d999be94205c82001616a2b2 Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 13:42:35 -0400 Subject: [PATCH 08/11] =?UTF-8?q?Bump=20version:=200.14.6=20=E2=86=92=200.?= =?UTF-8?q?14.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- gremlinapi/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 25146ce..ab66bff 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.6 +current_version = 0.14.7 commit = True tag = True diff --git a/gremlinapi/util.py b/gremlinapi/util.py index ca51300..73d1a64 100644 --- a/gremlinapi/util.py +++ b/gremlinapi/util.py @@ -7,7 +7,7 @@ log = logging.getLogger("GremlinAPI.client") -_version = "0.14.6" +_version = "0.14.7" def get_version(): From c264e824959d575d81366270f95f32c50bdb25e9 Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 14:54:01 -0400 Subject: [PATCH 09/11] Completed unit testing and final documentation for OAUTH --- gremlinapi/oauth.py | 36 ++++++++-------- tests/test_oauth.py | 102 ++++++++++++++++++++++++++++++++------------ tests/util.py | 11 +++++ 3 files changed, 103 insertions(+), 46 deletions(-) diff --git a/gremlinapi/oauth.py b/gremlinapi/oauth.py index 391b9f7..c22bd1c 100644 --- a/gremlinapi/oauth.py +++ b/gremlinapi/oauth.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020 Kyle Bouchard , Gremlin Inc +# Copyright (C) 2021 Kyle Bouchard , Gremlin Inc import logging import json @@ -58,16 +58,16 @@ def configure( `kwargs` is required in the following format: { - companyId : Defines the Company ID for OAUTH - authorizationUri : Used to authenticate against the OAuth provider. + 'companyId' : Defines the Company ID for OAUTH + 'authorizationUri' : Used to authenticate against the OAuth provider. We will redirect the user to this URL when they initate a OAuth login. - tokenUri : Used to exchange an OAuth code. + 'tokenUri' : Used to exchange an OAuth code. This is obtained after logging into the OAuth provider, for an access token. - userInfoUri : Used to query for the email of the user. - clientId : The public identifier obtained when registering Gremlin with your OAuth + 'userInfoUri' : Used to query for the email of the user. + 'clientId' : The public identifier obtained when registering Gremlin with your OAuth provider. - clientSecret : The secret obtained when registering Gremlin with your OAuth provider. - scope : (OPTIONAL) Define what level of access the access token will have that Gremlin + 'clientSecret' : The secret obtained when registering Gremlin with your OAuth provider. + 'scope' : (OPTIONAL) Define what level of access the access token will have that Gremlin obtains during the OAuth login. The default is `email`. If you change it from the default, the scope provided must be able to read the email of the user. } @@ -149,12 +149,12 @@ def get_callback_url( `data` is in the following format: { - email: Login email for your user, - password: Login password for your user, - state: Value of the state cookie obtained in the previous step, - redirectUri: URL where your provider should redirect you to after authenticating. + 'email': Login email for your user, + 'password': Login password for your user, + 'state': Value of the state cookie obtained in the previous step, + 'redirectUri': URL where your provider should redirect you to after authenticating. It should be https://api.gremlin.com/v1/oauth/callback, - clientId: Client Id obtained when registering Gremlin with your OAuth provider, + 'clientId': Client Id obtained when registering Gremlin with your OAuth provider, } Returns @@ -272,11 +272,11 @@ def authenticate( `kwargs` is required in the following format: { - company_name : The company for which the oauth authentication should commence, - email: Login email for your user, - password: Login password for your user, - client_id: Client Id obtained when registering Gremlin with your OAuth provider, - oauth_login_uri: The login Uri from your OAUTH provider, + 'company_name' : The company for which the oauth authentication should commence, + 'email': Login email for your user, + 'password': Login password for your user, + 'client_id': Client Id obtained when registering Gremlin with your OAuth provider, + 'oauth_login_uri': The login Uri from your OAUTH provider, } Returns diff --git a/tests/test_oauth.py b/tests/test_oauth.py index a8d20cd..36ab786 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -4,7 +4,15 @@ import requests from gremlinapi.oauth import GremlinAPIOAUTH -from .util import mock_json, mock_data, hooli_id +from .util import ( + mock_json, + mock_data, + hooli_id, + access_token_json, + mock_access_token, + mock_bearer_token, + bearer_token_json, +) class TestOAUTH(unittest.TestCase): @@ -27,30 +35,68 @@ def test_configure_with_decorator(self, mock_get) -> None: ) @patch("requests.get") - def test_authenticate_with_with(self, mock_get) -> None: - with patch("requests.post") as mock_post: - GREMLIN_COMPANY = "Hooli" - GREMLIN_USER_MOCK = "fakeemail@googlecom" - GREMLIN_PASSWORD_MOCK = "qwertyuiopoiuytrewq" - auth_args = { - "email": GREMLIN_USER_MOCK, - "password": GREMLIN_PASSWORD_MOCK, - "clientId": "mocklab_oauth2", - "companyName": GREMLIN_COMPANY, - } - mock_post.return_value = requests.Response() - mock_post.return_value.status_code = 200 - mock_post.return_value.json = mock_json - mock_get.return_value = requests.Response() - mock_get.return_value.status_code = 307 - mock_get.return_value.json = mock_json - mock_get.return_value.cookies = { - "oauth_state": "ewogICJub25jZSIgOiAiZGM2NjA5ODQtNGY2NS00NGYyLWE2MDktODQ0ZjY1ODRmMjM2IiwKICAiY29tcGFueUlkIiA6ICI5Njc2ODY4Yi02MGQyLTVlYmUtYWE2Ni1jMWRlODE2MmZmOWQiLAogICJyZWRpcmVjdFVyaSIgOiBudWxsCn0=" - } - mock_get.return_value.headers = { - "Location": "https://api.gremlin.com/v1/oauth/callback", - } - # self.assertEqual( - # GremlinAPIOAUTH.authenticate(hooli_id, **auth_args), - # mock_post.return_value, - # ) + def test_initiate_oauth_with_decorator(self, mock_get) -> None: + company_name = "Mock Company" + mock_get.return_value = requests.Response() + mock_get.return_value.status_code = 307 + mock_get.return_value.json = mock_json + mock_get.return_value.cookies = {"oauth_state": "mock_oauth_state="} + mock_get.return_value.headers = { + "Location": "mock_uri", + } + state_cookie, oauth_provider_login_url = GremlinAPIOAUTH.initiate_oauth( + company_name + ) + self.assertEqual( + state_cookie, + mock_get.return_value.cookies["oauth_state"], + ) + self.assertEqual( + oauth_provider_login_url, + mock_get.return_value.headers["Location"], + ) + + @patch("requests.post") + def test_get_callback_url_with_decorator(self, mock_post) -> None: + mock_login_uri = "http://example.com/login" + mock_callback_uri = "http:example.com/callback" + auth_body = { + "email": "mock@email.com", + "password": "m0ckp44sw0rd", + "state": "mockstatecookie1234", # obtained in earlier step + "redirectUri": "mockredirect.uri.com", + "clientId": "mock_client_id", + } + mock_post.return_value = requests.Response() + mock_post.return_value.status_code = 200 + mock_post.return_value.json = mock_json + mock_post.return_value.headers = { + "Location": mock_callback_uri, + } + self.assertEqual( + GremlinAPIOAUTH.get_callback_url(mock_login_uri, auth_body), + mock_callback_uri, + ) + + @patch("requests.get") + def test_get_access_token_with_decorator(self, mock_get) -> None: + mock_callback_uri = "http:example.com/callback" + mock_state_cookie = "abd3bd14bvd1beb1eabc1bead1badffcb6af1c6bfd6bddcdca6ddc=" + mock_get.return_value = requests.Response() + mock_get.return_value.status_code = 200 + mock_get.return_value.json = access_token_json + self.assertEqual( + GremlinAPIOAUTH.get_access_token(mock_state_cookie, mock_callback_uri), + mock_access_token, + ) + + @patch("requests.post") + def test_get_bearer_token_with_decorator(self, mock_get) -> None: + mock_company_name = "Mock Company, Inc." + mock_get.return_value = requests.Response() + mock_get.return_value.status_code = 200 + mock_get.return_value.json = bearer_token_json + self.assertEqual( + GremlinAPIOAUTH.get_bearer_token(mock_company_name, mock_access_token), + mock_bearer_token, + ) diff --git a/tests/util.py b/tests/util.py index 695f20c..5ee80b6 100644 --- a/tests/util.py +++ b/tests/util.py @@ -7,6 +7,17 @@ GremlinAttackCommandHelper, ) +mock_access_token = "asdf976asdf9786" +mock_bearer_token = "kjhg2345kjhg234" + + +def access_token_json(): + return {"access_token": mock_access_token} + + +def bearer_token_json(): + return {"header": mock_bearer_token} + def mock_json(): return mock_data From a46c36b64322d987d3468c1e525ab5a9aa47b65b Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 14:54:15 -0400 Subject: [PATCH 10/11] =?UTF-8?q?Bump=20version:=200.14.7=20=E2=86=92=200.?= =?UTF-8?q?15.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- gremlinapi/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ab66bff..ed44277 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.14.7 +current_version = 0.15.0 commit = True tag = True diff --git a/gremlinapi/util.py b/gremlinapi/util.py index 73d1a64..6f280e5 100644 --- a/gremlinapi/util.py +++ b/gremlinapi/util.py @@ -7,7 +7,7 @@ log = logging.getLogger("GremlinAPI.client") -_version = "0.14.7" +_version = "0.15.0" def get_version(): From 96101e32a21c5b6654825199e1b7c60ba8e88752 Mon Sep 17 00:00:00 2001 From: Kyle Bouchard Date: Fri, 23 Apr 2021 15:01:45 -0400 Subject: [PATCH 11/11] =?UTF-8?q?Bump=20version:=200.15.0=20=E2=86=92=200.?= =?UTF-8?q?15.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- gremlinapi/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ed44277..21db3f0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.15.0 +current_version = 0.15.1 commit = True tag = True diff --git a/gremlinapi/util.py b/gremlinapi/util.py index 6f280e5..ac92f5e 100644 --- a/gremlinapi/util.py +++ b/gremlinapi/util.py @@ -7,7 +7,7 @@ log = logging.getLogger("GremlinAPI.client") -_version = "0.15.0" +_version = "0.15.1" def get_version():