From 09357f290b4654d5b16009eec1d609e8a562f738 Mon Sep 17 00:00:00 2001 From: Clemens Wolff Date: Mon, 20 May 2019 12:06:32 -0400 Subject: [PATCH 1/4] Ensure .env file gets read --- app/app/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/settings.py b/app/app/settings.py index 7963b5b6..3c642eab 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -22,7 +22,7 @@ from furl import furl BASE_DIR = path.dirname(path.dirname(path.abspath(__file__))) env = Env() -env.read_env(BASE_DIR, recurse=False) +env.read_env(path.join(BASE_DIR, '.env'), recurse=False) # Quick-start development settings - unsuitable for production From f7f6f099a2d1055ca7a330d97463627705dfebfa Mon Sep 17 00:00:00 2001 From: Clemens Wolff Date: Mon, 20 May 2019 12:06:18 -0400 Subject: [PATCH 2/4] Make social auth pipeline explicit --- app/app/settings.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/app/settings.py b/app/app/settings.py index 3c642eab..ae77a203 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -125,6 +125,18 @@ SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = env('OAUTH_AAD_KEY', None) SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = env('OAUTH_AAD_SECRET', None) SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = env('OAUTH_AAD_TENANT', None) +SOCIAL_AUTH_PIPELINE = [ + 'social_core.pipeline.social_auth.social_details', + 'social_core.pipeline.social_auth.social_uid', + 'social_core.pipeline.social_auth.auth_allowed', + 'social_core.pipeline.social_auth.social_user', + 'social_core.pipeline.user.get_username', + 'social_core.pipeline.user.create_user', + 'social_core.pipeline.social_auth.associate_user', + 'social_core.pipeline.social_auth.load_extra_data', + 'social_core.pipeline.user.user_details', +] + # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases From 74630f078a918d66341a804fedaa9b14777fd195 Mon Sep 17 00:00:00 2001 From: Clemens Wolff Date: Mon, 20 May 2019 13:16:58 -0400 Subject: [PATCH 3/4] Add option to read user admin status from Github --- app/app/settings.py | 6 ++ app/server/social_auth.py | 44 +++++++++++ ...lAuth.test_fetch_permissions_is_admin.yaml | 76 ++++++++++++++++++ ...Auth.test_fetch_permissions_not_admin.yaml | 77 +++++++++++++++++++ app/server/tests/test_social_auth.py | 57 ++++++++++++++ requirements.txt | 3 + 6 files changed, 263 insertions(+) create mode 100644 app/server/social_auth.py create mode 100644 app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_is_admin.yaml create mode 100644 app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_not_admin.yaml create mode 100644 app/server/tests/test_social_auth.py diff --git a/app/app/settings.py b/app/app/settings.py index ae77a203..3291a366 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -120,6 +120,11 @@ AUTHENTICATION_BACKENDS = [ SOCIAL_AUTH_GITHUB_KEY = env('OAUTH_GITHUB_KEY', None) SOCIAL_AUTH_GITHUB_SECRET = env('OAUTH_GITHUB_SECRET', None) +GITHUB_ADMIN_ORG_NAME = env('GITHUB_ADMIN_ORG_NAME', None) +GITHUB_ADMIN_TEAM_NAME = env('GITHUB_ADMIN_TEAM_NAME', None) + +if GITHUB_ADMIN_ORG_NAME and GITHUB_ADMIN_TEAM_NAME: + SOCIAL_AUTH_GITHUB_SCOPE = ['read:org'] SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = env('OAUTH_AAD_KEY', None) SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = env('OAUTH_AAD_SECRET', None) @@ -135,6 +140,7 @@ SOCIAL_AUTH_PIPELINE = [ 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', + 'server.social_auth.fetch_github_permissions', ] # Database diff --git a/app/server/social_auth.py b/app/server/social_auth.py new file mode 100644 index 00000000..943c63a6 --- /dev/null +++ b/app/server/social_auth.py @@ -0,0 +1,44 @@ +import requests +from django.conf import settings +from social_core.backends.github import GithubOAuth2 + + +# noinspection PyUnusedLocal +def fetch_github_permissions(strategy, details, user=None, is_new=False, *args, **kwargs): + org_name = getattr(settings, 'GITHUB_ADMIN_ORG_NAME', '') + team_name = getattr(settings, 'GITHUB_ADMIN_TEAM_NAME', '') + if not user or not isinstance(kwargs['backend'], GithubOAuth2) or not org_name or not team_name: + return + + response = requests.post( + url='https://api.github.com/graphql', + headers={ + 'Authorization': 'Bearer {}'.format(kwargs['response']['access_token']), + }, + json={ + 'query': ''' + query($userName: String!, $orgName: String!, $teamName: String!) { + organization(login: $orgName) { + teams(query: $teamName, userLogins: [$userName], first: 1) { + nodes { + name + } + } + } + } + ''', + 'variables': { + 'userName': details['username'], + 'orgName': org_name, + 'teamName': team_name, + } + } + ) + response.raise_for_status() + response = response.json() + + is_superuser = {'name': team_name} in response['data']['organization']['teams']['nodes'] + + if user.is_superuser != is_superuser: + user.is_superuser = is_superuser + user.save() diff --git a/app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_is_admin.yaml b/app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_is_admin.yaml new file mode 100644 index 00000000..1b3c3694 --- /dev/null +++ b/app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_is_admin.yaml @@ -0,0 +1,76 @@ +interactions: +- request: + body: '{"query": "\n query($userName: String!, $orgName: String!, + $teamName: String!) {\n organization(login: $orgName) {\n teams(query: + $teamName, userLogins: [$userName], first: 1) {\n nodes + {\n name\n }\n }\n }\n }\n ", + "variables": {"userName": "c-w", "orgName": "CatalystCode", "teamName": "doccano-dev"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '513' + Content-Type: + - application/json + User-Agent: + - python-requests/2.21.0 + method: POST + uri: https://api.github.com/graphql + response: + body: + string: '{"data":{"organization":{"teams":{"nodes":[{"name":"doccano-dev"}]}}}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - no-cache + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 May 2019 17:38:20 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v4; format=json + X-GitHub-Request-Id: + - E979:03BD:225D930:49FB694:5CE2E60C + X-OAuth-Scopes: + - read:org + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4955' + X-RateLimit-Reset: + - '1558377500' + X-XSS-Protection: + - 1; mode=block + content-length: + - '70' + status: + code: 200 + message: OK +version: 1 diff --git a/app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_not_admin.yaml b/app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_not_admin.yaml new file mode 100644 index 00000000..5f9c7565 --- /dev/null +++ b/app/server/tests/cassettes/TestGithubSocialAuth.test_fetch_permissions_not_admin.yaml @@ -0,0 +1,77 @@ +interactions: +- request: + body: '{"query": "\n query($userName: String!, $orgName: String!, + $teamName: String!) {\n organization(login: $orgName) {\n teams(query: + $teamName, userLogins: [$userName], first: 1) {\n nodes + {\n name\n }\n }\n }\n }\n ", + "variables": {"userName": "hirosan", "orgName": "CatalystCode", "teamName": + "doccano-dev"}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '517' + Content-Type: + - application/json + User-Agent: + - python-requests/2.21.0 + method: POST + uri: https://api.github.com/graphql + response: + body: + string: '{"data":{"organization":{"teams":{"nodes":[]}}}}' + headers: + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, + X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, + X-GitHub-Media-Type + Cache-Control: + - no-cache + Content-Security-Policy: + - default-src 'none' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 20 May 2019 17:38:20 GMT + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Server: + - GitHub.com + Status: + - 200 OK + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + Transfer-Encoding: + - chunked + X-Accepted-OAuth-Scopes: + - repo + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - deny + X-GitHub-Media-Type: + - github.v4; format=json + X-GitHub-Request-Id: + - E97B:0EFE:220AB47:4963FE2:5CE2E60C + X-OAuth-Scopes: + - read:org + X-RateLimit-Limit: + - '5000' + X-RateLimit-Remaining: + - '4954' + X-RateLimit-Reset: + - '1558377500' + X-XSS-Protection: + - 1; mode=block + content-length: + - '48' + status: + code: 200 + message: OK +version: 1 diff --git a/app/server/tests/test_social_auth.py b/app/server/tests/test_social_auth.py new file mode 100644 index 00000000..0c5d81e3 --- /dev/null +++ b/app/server/tests/test_social_auth.py @@ -0,0 +1,57 @@ +from django.contrib.auth import get_user_model +from django.test import TestCase, override_settings +from social_core.backends.github import GithubOAuth2 +from vcr_unittest import VCRMixin + +from .. import social_auth + +User = get_user_model() + + +class VCRTestCase(VCRMixin, TestCase): + @property + def access_token(self): + raise NotImplementedError() + + def _get_vcr(self, **kwargs): + kwargs['decode_compressed_response'] = True + kwargs['record_mode'] = 'none' if self.access_token == 'censored' else 'all' + return super()._get_vcr(**kwargs) + + def _get_vcr_kwargs(self, **kwargs): + kwargs['filter_headers'] = ['Authorization'] + return super()._get_vcr_kwargs(**kwargs) + + +@override_settings(GITHUB_ADMIN_ORG_NAME='CatalystCode') +@override_settings(GITHUB_ADMIN_TEAM_NAME='doccano-dev') +class TestGithubSocialAuth(VCRTestCase): + strategy = None + backend = GithubOAuth2(strategy=strategy) + access_token = 'censored' + + def test_fetch_permissions_is_admin(self): + user = User() + + social_auth.fetch_github_permissions( + strategy=self.strategy, + details={'username': 'c-w'}, + user=user, + backend=self.backend, + response={'access_token': self.access_token}, + ) + + self.assertTrue(user.is_superuser) + + def test_fetch_permissions_not_admin(self): + user = User() + + social_auth.fetch_github_permissions( + strategy=self.strategy, + details={'username': 'hirosan'}, + user=user, + backend=self.backend, + response={'access_token': self.access_token}, + ) + + self.assertFalse(user.is_superuser) diff --git a/requirements.txt b/requirements.txt index 177033a4..006722b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ model-mommy==1.6.0 psycopg2-binary==2.7.7 python-dateutil==2.7.3 pytz==2018.4 +requests==2.21.0 six==1.11.0 seqeval==0.0.6 social-auth-app-django==3.1.0 @@ -28,4 +29,6 @@ social-auth-core[azuread]==3.0.0 text-unidecode==1.2 tornado==5.0.2 unittest-xml-reporting==2.5.1 +vcrpy==2.0.1 +vcrpy-unittest==0.1.7 whitenoise[brotli]==4.1.2 From 973809199faad4078bfbf6f4599abaf3cf01cb8f Mon Sep 17 00:00:00 2001 From: Clemens Wolff Date: Mon, 20 May 2019 15:42:24 -0400 Subject: [PATCH 4/4] Add option to read user admin status from AAD --- app/app/settings.py | 6 +++ app/server/social_auth.py | 26 ++++++++++ ...lAuth.test_fetch_permissions_is_admin.yaml | 52 +++++++++++++++++++ ...Auth.test_fetch_permissions_not_admin.yaml | 52 +++++++++++++++++++ app/server/tests/test_social_auth.py | 38 ++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_is_admin.yaml create mode 100644 app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_not_admin.yaml diff --git a/app/app/settings.py b/app/app/settings.py index 3291a366..618f55e6 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -129,6 +129,11 @@ if GITHUB_ADMIN_ORG_NAME and GITHUB_ADMIN_TEAM_NAME: SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY = env('OAUTH_AAD_KEY', None) SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET = env('OAUTH_AAD_SECRET', None) SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT_ID = env('OAUTH_AAD_TENANT', None) +AZUREAD_ADMIN_GROUP_ID = env('AZUREAD_ADMIN_GROUP_ID', None) + +if AZUREAD_ADMIN_GROUP_ID: + SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_RESOURCE = 'https://graph.microsoft.com/' + SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SCOPE = ['Directory.Read.All'] SOCIAL_AUTH_PIPELINE = [ 'social_core.pipeline.social_auth.social_details', @@ -141,6 +146,7 @@ SOCIAL_AUTH_PIPELINE = [ 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', 'server.social_auth.fetch_github_permissions', + 'server.social_auth.fetch_azuread_permissions', ] # Database diff --git a/app/server/social_auth.py b/app/server/social_auth.py index 943c63a6..35adf722 100644 --- a/app/server/social_auth.py +++ b/app/server/social_auth.py @@ -1,5 +1,6 @@ import requests from django.conf import settings +from social_core.backends.azuread_tenant import AzureADTenantOAuth2 from social_core.backends.github import GithubOAuth2 @@ -42,3 +43,28 @@ def fetch_github_permissions(strategy, details, user=None, is_new=False, *args, if user.is_superuser != is_superuser: user.is_superuser = is_superuser user.save() + + +# noinspection PyUnusedLocal +def fetch_azuread_permissions(strategy, details, user=None, is_new=False, *args, **kwargs): + group_id = getattr(settings, 'AZUREAD_ADMIN_GROUP_ID', '') + if not user or not isinstance(kwargs['backend'], AzureADTenantOAuth2) or not group_id: + return + + response = requests.post( + url='https://graph.microsoft.com/v1.0/me/checkMemberGroups', + headers={ + 'Authorization': 'Bearer {}'.format(kwargs['response']['access_token']), + }, + json={ + 'groupIds': [group_id] + } + ) + response.raise_for_status() + response = response.json() + + is_superuser = group_id in response['value'] + + if user.is_superuser != is_superuser: + user.is_superuser = is_superuser + user.save() diff --git a/app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_is_admin.yaml b/app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_is_admin.yaml new file mode 100644 index 00000000..a9c7989c --- /dev/null +++ b/app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_is_admin.yaml @@ -0,0 +1,52 @@ +interactions: +- request: + body: '{"groupIds": ["dddddddd-dddd-dddd-dddd-dddddddddddd"]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '54' + Content-Type: + - application/json + User-Agent: + - python-requests/2.21.0 + method: POST + uri: https://graph.microsoft.com/v1.0/me/checkMemberGroups + response: + body: + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(Edm.String)","value":["dddddddd-dddd-dddd-dddd-dddddddddddd"]}' + headers: + Cache-Control: + - private + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Date: + - Mon, 20 May 2019 19:34:27 GMT + Duration: + - '89.2611' + Location: + - https://graph.microsoft.com + OData-Version: + - '4.0' + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + client-request-id: + - 78c55087-ba07-4e80-8498-8b23b1901356 + content-length: + - '135' + request-id: + - 78c55087-ba07-4e80-8498-8b23b1901356 + x-ms-ags-diagnostic: + - '{"ServerInfo":{"DataCenter":"East US","Slice":"SliceC","Ring":"2","ScaleUnit":"000","RoleInstance":"AGSFE_IN_51","ADSiteName":"EUS"}}' + status: + code: 200 + message: OK +version: 1 diff --git a/app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_not_admin.yaml b/app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_not_admin.yaml new file mode 100644 index 00000000..7d2cba35 --- /dev/null +++ b/app/server/tests/cassettes/TestAzureADTenantSocialAuth.test_fetch_permissions_not_admin.yaml @@ -0,0 +1,52 @@ +interactions: +- request: + body: '{"groupIds": ["eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '54' + Content-Type: + - application/json + User-Agent: + - python-requests/2.21.0 + method: POST + uri: https://graph.microsoft.com/v1.0/me/checkMemberGroups + response: + body: + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(Edm.String)","value":[]}' + headers: + Cache-Control: + - private + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Date: + - Mon, 20 May 2019 19:34:52 GMT + Duration: + - '84.3986' + Location: + - https://graph.microsoft.com + OData-Version: + - '4.0' + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + client-request-id: + - 69bf4728-ae4e-47ef-afd0-7d2c31129b83 + content-length: + - '97' + request-id: + - 69bf4728-ae4e-47ef-afd0-7d2c31129b83 + x-ms-ags-diagnostic: + - '{"ServerInfo":{"DataCenter":"East US","Slice":"SliceC","Ring":"2","ScaleUnit":"000","RoleInstance":"AGSFE_IN_5","ADSiteName":"EUS"}}' + status: + code: 200 + message: OK +version: 1 diff --git a/app/server/tests/test_social_auth.py b/app/server/tests/test_social_auth.py index 0c5d81e3..e8eca0cd 100644 --- a/app/server/tests/test_social_auth.py +++ b/app/server/tests/test_social_auth.py @@ -1,5 +1,6 @@ from django.contrib.auth import get_user_model from django.test import TestCase, override_settings +from social_core.backends.azuread_tenant import AzureADTenantOAuth2 from social_core.backends.github import GithubOAuth2 from vcr_unittest import VCRMixin @@ -55,3 +56,40 @@ class TestGithubSocialAuth(VCRTestCase): ) self.assertFalse(user.is_superuser) + + +@override_settings(SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_KEY='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') +@override_settings(SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_SECRET='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb=') +@override_settings(SOCIAL_AUTH_AZUREAD_TENANT_OAUTH2_TENANT='cccccccc-cccc-cccc-cccc-cccccccccccc') +class TestAzureADTenantSocialAuth(VCRTestCase): + strategy = None + backend = AzureADTenantOAuth2(strategy=strategy) + access_token = 'censored' + + @override_settings(AZUREAD_ADMIN_GROUP_ID='dddddddd-dddd-dddd-dddd-dddddddddddd') + def test_fetch_permissions_is_admin(self): + user = User() + + social_auth.fetch_azuread_permissions( + strategy=self.strategy, + details={}, + user=user, + backend=self.backend, + response={'access_token': self.access_token}, + ) + + self.assertTrue(user.is_superuser) + + @override_settings(AZUREAD_ADMIN_GROUP_ID='eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee') + def test_fetch_permissions_not_admin(self): + user = User() + + social_auth.fetch_azuread_permissions( + strategy=self.strategy, + details={}, + user=user, + backend=self.backend, + response={'access_token': self.access_token}, + ) + + self.assertFalse(user.is_superuser)