-
Notifications
You must be signed in to change notification settings - Fork 25
Initial WorkOS package with SSO #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
0c55ced
Initial SSO code
89b2a3c
Cleanup
fba1a1f
SSOProfile class
612a268
Cleanup
10a6000
Raise exception on missing settings
9363720
Update .gitignore for vscode
cb0f4df
Basic exception handling for requests
1f2994c
Remove use of redirect uri for getting profile
7d2724d
Extract request data for exceptions
b860ede
Different way to do version
da046fd
Base headers for api requests
5f5a0b8
Update requirements for setup.py
c401626
Raise the correct Auth exceptions for requests
86b3f26
Docstrings for the code and also rename SSOProfile -> WorkOSProfile
129295c
Update user agent with language
f49145e
README
c4aa2cf
Fill in some missing SSO comments
1ed57b3
Test requirements
01da060
Fix passing in SSO and update semaphore settings
7a85936
Change field ordering in WorkOSProfile resource
030ebfb
Exclude tests in setup.py
547763d
Client and SSO tests
d21b01a
Refactor mocking response
0799ebe
Tests for request helper
faa305c
PR Feedback
a00f5ec
Update version to 0.0.1
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,3 +127,6 @@ dmypy.json | |
|
|
||
| # Pyre type checker | ||
| .pyre/ | ||
|
|
||
| # VSCode | ||
| .vscode/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,43 @@ | ||
| # workos-python | ||
| API Client for WorkOS | ||
|
|
||
| Pyhon SDK to conveniently access the [WorkOS API](https://workos.com). | ||
|
|
||
| ## Installation | ||
|
|
||
| To install from PyPi, run the following: | ||
| ``` | ||
| pip install workos | ||
| ``` | ||
|
|
||
| To install from source, clone the repo and run the following: | ||
| ``` | ||
| python setup.py install | ||
| ``` | ||
|
|
||
| ## Getting Started | ||
|
|
||
| The package will need to be configured with your [api key](https://dashboard.workos.com/api-keys) at a minimum and [project id](https://dashboard.workos.com/sso/configuration) if you plan on utilizing SSO: | ||
| ```python | ||
| import workos | ||
|
|
||
| workos.api_key = sk_abdsomecharactersm284 | ||
| workos.project_id = project_b27needthisforssotemxo | ||
| ``` | ||
|
|
||
| For your convenience, a client is available as an entry point for accessing the WorkOS feature set: | ||
| ```python | ||
| from workos import client | ||
|
|
||
| # URL to redirect a User to to initiate the WorkOS OAuth 2.0 workflow | ||
| client.sso.get_authorization_url( | ||
| 'customer-domain.com', | ||
| 'my-domain.com/auth/callback', | ||
| state={ | ||
| 'stuff': 'from_the_original_request', | ||
| 'more_things': 'ill_get_it_all_back_when_oauth_is_complete', | ||
| } | ||
| ) | ||
|
|
||
| # Get the WorkOSProfile for an authenticated User | ||
| client.get_profile(oauth_code) | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Install package deps | ||
| -e . | ||
|
|
||
| # Test deps | ||
| pytest==4.6.9 | ||
| pytest-cov==2.8.1 | ||
| six==1.13.0 |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import pytest | ||
| import requests | ||
|
|
||
| import workos | ||
|
|
||
| class MockResponse(object): | ||
| def __init__(self, response_dict, status_code, headers=None): | ||
| self.response_dict = response_dict | ||
| self.status_code = status_code | ||
| self.headers = {} if headers is None else headers | ||
|
|
||
| def json(self): | ||
| return self.response_dict | ||
|
|
||
| @pytest.fixture | ||
| def set_api_key(monkeypatch): | ||
| monkeypatch.setattr(workos, 'api_key', 'sk_abdsomecharactersm284') | ||
|
|
||
| @pytest.fixture | ||
| def set_project_id(monkeypatch): | ||
| monkeypatch.setattr(workos, 'project_id', 'project_b27needthisforssotemxo') | ||
|
|
||
| @pytest.fixture | ||
| def set_api_key_and_project_id(set_api_key, set_project_id): | ||
| pass | ||
|
|
||
| @pytest.fixture | ||
| def mock_request_method(monkeypatch): | ||
| def inner(method, response_dict, status_code, headers=None): | ||
| def mock(*args, **kwargs): | ||
| return MockResponse(response_dict, status_code, headers=headers) | ||
|
|
||
| monkeypatch.setattr(requests, method, mock) | ||
|
|
||
| return inner | ||
|
|
||
| @pytest.fixture | ||
| def capture_and_mock_requests(monkeypatch): | ||
| def inner(): | ||
| captured_requests = [] | ||
|
|
||
| def capture(*args, **kwargs): | ||
| captured_requests.append((args, kwargs)) | ||
| return MockResponse({}, 200) | ||
|
|
||
| monkeypatch.setattr(requests, 'get', capture) | ||
| monkeypatch.setattr(requests, 'post', capture) | ||
|
|
||
| return captured_requests | ||
|
|
||
| return inner |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import pytest | ||
|
|
||
| from workos import client | ||
| from workos.exceptions import ConfigurationException | ||
|
|
||
| class TestClient(object): | ||
| @pytest.fixture(autouse=True) | ||
| def setup(self): | ||
| client._sso = None | ||
|
|
||
| def test_initialize_sso(self, set_api_key_and_project_id): | ||
| assert bool(client.sso) | ||
|
|
||
| def test_initialize_sso_missing_api_key(self, set_project_id): | ||
| with pytest.raises(ConfigurationException) as ex: | ||
| client.sso | ||
|
|
||
| message = str(ex) | ||
|
|
||
| assert 'api_key' in message | ||
| assert 'project_id' not in message | ||
|
|
||
| def test_initialize_sso_missing_project_id(self, set_api_key): | ||
| with pytest.raises(ConfigurationException) as ex: | ||
| client.sso | ||
|
|
||
| message = str(ex) | ||
|
|
||
| assert 'project_id' in message | ||
| assert 'api_key' not in message | ||
|
|
||
| def test_initialize_sso_missing_api_key_and_project_id(self): | ||
| with pytest.raises(ConfigurationException) as ex: | ||
| client.sso | ||
|
|
||
| message = str(ex) | ||
|
|
||
| assert all(setting in message for setting in ('api_key', 'project_id',)) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import json | ||
| from six.moves.urllib.parse import parse_qsl, urlparse | ||
|
|
||
| import pytest | ||
|
|
||
| import workos | ||
| from workos.sso import SSO | ||
| from workos.utils.request import RESPONSE_TYPE_CODE | ||
|
|
||
| class TestSSO(object): | ||
| @pytest.fixture(autouse=True) | ||
| def setup(self, set_api_key_and_project_id): | ||
| self.customer_domain = 'workos.com' | ||
| self.redirect_uri = 'https://localhost/auth/callback' | ||
| self.state = { 'things': 'with_stuff', } | ||
|
|
||
| self.sso = SSO() | ||
|
|
||
| @pytest.fixture | ||
| def mock_profile(self): | ||
| return { | ||
| 'id': 'prof_01DWAS7ZQWM70PV93BFV1V78QV', | ||
| 'email': 'demo@workos-okta.com', | ||
| 'first_name': 'WorkOS', | ||
| 'last_name': 'Demo', | ||
| 'connection_type': 'OktaSAML', | ||
| 'idp_id': '00u1klkowm8EGah2H357' | ||
| } | ||
|
|
||
| def test_authorization_url_has_expected_query_params(self): | ||
| authorization_url = self.sso.get_authorization_url( | ||
| self.customer_domain, | ||
| self.redirect_uri, | ||
| state=self.state | ||
| ) | ||
|
|
||
| parsed_url = urlparse(authorization_url) | ||
|
|
||
| assert dict(parse_qsl(parsed_url.query)) == { | ||
| 'domain': self.customer_domain, | ||
| 'client_id': workos.project_id, | ||
| 'redirect_uri': self.redirect_uri, | ||
| 'response_type': RESPONSE_TYPE_CODE, | ||
| 'state': json.dumps(self.state), | ||
| } | ||
|
|
||
|
|
||
| def test_get_profile_returns_expected_workosprofile_object( | ||
| self, mock_profile, mock_request_method | ||
| ): | ||
| response_dict = { | ||
| 'profile': { | ||
| 'object': 'profile', | ||
| 'id': mock_profile['id'], | ||
| 'email': mock_profile['email'], | ||
| 'first_name': mock_profile['first_name'], | ||
| 'connection_type': mock_profile['connection_type'], | ||
| 'last_name': mock_profile['last_name'], | ||
| 'idp_id': mock_profile['idp_id'], | ||
| }, | ||
| 'access_token': '01DY34ACQTM3B1CSX1YSZ8Z00D', | ||
| } | ||
|
|
||
| mock_request_method('post', response_dict, 200) | ||
|
|
||
| profile = self.sso.get_profile(123) | ||
|
|
||
| assert profile.to_dict() == mock_profile |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import pytest | ||
|
|
||
| from workos.exceptions import ( | ||
| AuthenticationException, AuthorizationException, BadRequestException, | ||
| ServerException, | ||
| ) | ||
| from workos.utils.request import RequestHelper, BASE_HEADERS | ||
|
|
||
| STATUS_CODE_TO_EXCEPTION_MAPPING = { | ||
| 400: BadRequestException, | ||
| 401: AuthenticationException, | ||
| 403: AuthorizationException, | ||
| 500: ServerException, | ||
| } | ||
|
|
||
| class TestRequestHelper(object): | ||
| def test_set_base_api_url(self): | ||
| pass | ||
|
|
||
| def test_request_raises_expected_exception_for_status_code( | ||
| self, mock_request_method | ||
| ): | ||
| request_helper = RequestHelper() | ||
|
|
||
| for status_code, exception in STATUS_CODE_TO_EXCEPTION_MAPPING.items(): | ||
| mock_request_method('get', {}, status_code) | ||
|
|
||
| with pytest.raises(exception): | ||
| request_helper.request('bad_place') | ||
|
|
||
| def test_request_exceptions_include_expected_request_data( | ||
| self, mock_request_method | ||
| ): | ||
| request_helper = RequestHelper() | ||
|
|
||
| request_id = 'request-123' | ||
| response_message = 'stuff happened' | ||
|
|
||
| for status_code, exception in STATUS_CODE_TO_EXCEPTION_MAPPING.items(): | ||
| mock_request_method( | ||
| 'get', | ||
| {'message': response_message, }, | ||
| status_code, | ||
| headers={'X-Request-ID': request_id} | ||
| ) | ||
|
|
||
| try: | ||
| request_helper.request('bad_place') | ||
| except exception as ex: | ||
| assert ex.message == response_message | ||
| assert ex.request_id == request_id | ||
| except Exception as ex: | ||
| # This'll fail for sure here but... just using the nice error that'd come up | ||
| assert ex.__class__ == exception | ||
|
|
||
| def test_request_bad_body_raises_expected_exception_with_request_data( | ||
| self, mock_request_method | ||
| ): | ||
| request_id = 'request-123' | ||
|
|
||
| mock_request_method( | ||
| 'get', | ||
| 'this_isnt_json', | ||
| 200, | ||
| headers={'X-Request-ID': request_id} | ||
| ) | ||
|
|
||
| try: | ||
| RequestHelper().request('bad_place') | ||
| except ServerException as ex: | ||
| assert ex.message == None | ||
| assert ex.request_id == request_id | ||
| except Exception as ex: | ||
| # This'll fail for sure here but... just using the nice error that'd come up | ||
| assert ex.__class__ == ServerException | ||
|
|
||
| def test_request_includes_base_headers(self, capture_and_mock_requests): | ||
| requests = capture_and_mock_requests() | ||
|
|
||
| RequestHelper().request('ok_place') | ||
|
|
||
| assert len(requests) == 1 | ||
|
|
||
| base_headers = set(BASE_HEADERS.items()) | ||
| headers = set(requests[0][1]['headers'].items()) | ||
|
|
||
| assert base_headers.issubset(headers) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| __all__ = [ | ||
| '__package_name__', '__package_url__', '__version__', '__author__', | ||
| '__author_email__', '__description__', '__license__', | ||
| ] | ||
|
|
||
| __package_name__ = 'workos' | ||
|
|
||
| __package_url__ = 'https://github.com/workos-inc/workos-python' | ||
|
|
||
| __version__ = '0.0.1' | ||
|
|
||
| __author__ = 'WorkOS' | ||
|
|
||
| __author_email__ = 'team@workos.com' | ||
|
|
||
| __description__ = 'WorkOS Python Client' | ||
|
|
||
| __license__ = 'MIT' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| from workos.__about__ import __version__ | ||
| from workos.client import client | ||
|
|
||
| api_key = None | ||
| project_id = None | ||
| base_api_url = 'https://api.workos.com/' |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: don't have to, but can be more explicit if you want to case on the setting that's missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea