From 486f28cd0952007d7e97a02bdc543f2e8b3a0476 Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Mon, 9 Mar 2020 17:18:02 -0500 Subject: [PATCH 01/11] Trying to setup google groups --- oauthenticator/google.py | 55 +++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index e765f69c..44001825 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -22,6 +22,10 @@ class GoogleOAuthenticator(OAuthenticator, GoogleOAuth2Mixin): + google_api_base_url = "https://www.googleapis.com" + + # add the following to your jupyterhub_config.py to check groups + # c.GoogleOAuthenticator.scope = ['openid', 'email', 'https://www.googleapis.com/auth/admin.directory.group.readonly'] @default('scope') def _scope_default(self): return ['openid', 'email'] @@ -32,10 +36,14 @@ def _authorize_url_default(self): @default("token_url") def _token_url_default(self): - return "https://www.googleapis.com/oauth2/v4/token" + return "%s/oauth2/v4/token" % (self.google_api_base_url) + + google_group_whitelist = Set( + config=True, help="Automatically whitelist members of selected groups" + ) user_info_url = Unicode( - "https://www.googleapis.com/oauth2/v1/userinfo", config=True + "%s/oauth2/v1/userinfo" % (self.google_api_base_url), config=True ) hosted_domain = List( @@ -127,10 +135,45 @@ async def authenticate(self, handler, data=None): # unambiguous domain, use only base name username = user_email.split('@')[0] - return { - 'name': username, - 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, - } + # Check if user is a member of any whitelisted groups or projects. + # These checks are performed here, as it requires `access_token`. + user_in_group = False + is_group_specified = False + + if self.google_group_whitelist: + is_group_specified = True + user_in_group = await self._check_group_whitelist(user_id, access_token) + + no_config_specified = not is_group_specified + + if ( + (is_group_specified and user_in_group) + or no_config_specified + ): + return { + 'name': username, + 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, + } + else: + self.log.warning("%s not in group or project whitelist", username) + return None + + + async def _check_group_whitelist(self, user_email, user_email_domain, access_token): + http_client = AsyncHTTPClient() + headers = _api_headers(access_token) + # Check if user is a member of any group in the whitelist + for group in map(url_escape, self.google_group_whitelist): + url = "%s/admin/directory/v1/groups/%s/members/%s" % ( + self.google_api_base_url, + "%s@%s" % (user_email, user_email_domain), + user_email, + ) + req = HTTPRequest(url, method="GET", headers=headers) + resp = await http_client.fetch(req, raise_error=False) + if resp.code == 200: + return True # user _is_ in group + return False class LocalGoogleOAuthenticator(LocalAuthenticator, GoogleOAuthenticator): From 80ba566ccd449481602453a8582ffdd5c2806b11 Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Mon, 9 Mar 2020 17:28:37 -0500 Subject: [PATCH 02/11] Adding Set to google --- oauthenticator/google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index 44001825..48895e1e 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -13,7 +13,7 @@ from tornado.auth import GoogleOAuth2Mixin from tornado.web import HTTPError -from traitlets import Unicode, List, default, validate +from traitlets import Set, Unicode, List, default, validate from jupyterhub.auth import LocalAuthenticator from jupyterhub.utils import url_path_join From e626e052a7d4349ba5bdd28a1dd34e38b2b2141e Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Mon, 9 Mar 2020 17:54:38 -0500 Subject: [PATCH 03/11] not self for google url --- oauthenticator/google.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index 48895e1e..c82e0b78 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -22,7 +22,7 @@ class GoogleOAuthenticator(OAuthenticator, GoogleOAuth2Mixin): - google_api_base_url = "https://www.googleapis.com" + google_api_base_url = Unicode("https://www.googleapis.com", config=True) # add the following to your jupyterhub_config.py to check groups # c.GoogleOAuthenticator.scope = ['openid', 'email', 'https://www.googleapis.com/auth/admin.directory.group.readonly'] @@ -165,7 +165,7 @@ async def _check_group_whitelist(self, user_email, user_email_domain, access_tok # Check if user is a member of any group in the whitelist for group in map(url_escape, self.google_group_whitelist): url = "%s/admin/directory/v1/groups/%s/members/%s" % ( - self.google_api_base_url, + google_api_base_url, "%s@%s" % (user_email, user_email_domain), user_email, ) From caee8d9b0eee977e4bd03a8d15fdf44fb2a1bc63 Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Mon, 9 Mar 2020 18:01:31 -0500 Subject: [PATCH 04/11] no self for google_api_base_url --- oauthenticator/google.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index c82e0b78..b578ca07 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -36,14 +36,14 @@ def _authorize_url_default(self): @default("token_url") def _token_url_default(self): - return "%s/oauth2/v4/token" % (self.google_api_base_url) + return "%s/oauth2/v4/token" % (google_api_base_url) google_group_whitelist = Set( config=True, help="Automatically whitelist members of selected groups" ) user_info_url = Unicode( - "%s/oauth2/v1/userinfo" % (self.google_api_base_url), config=True + "%s/oauth2/v1/userinfo" % (google_api_base_url), config=True ) hosted_domain = List( From 275590dfcce51b319b2ac5da0e2d0ac89252228a Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Mon, 9 Mar 2020 18:15:09 -0500 Subject: [PATCH 05/11] Google API URL as self --- oauthenticator/google.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index b578ca07..2bb85eca 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -22,7 +22,18 @@ class GoogleOAuthenticator(OAuthenticator, GoogleOAuth2Mixin): - google_api_base_url = Unicode("https://www.googleapis.com", config=True) + google_api_url = Unicode("https://www.googleapis.com", config=True) + + @default('google_api_base_url') + def _google_api_base_url(self) + """get default google apis url from env""" + google_api_url = os.getenv('GOOGLE_API_URL') + + # default to gitlab.com + if not google_api_url: + google_api_url = 'https://www.googleapis.com' + + return google_api_url # add the following to your jupyterhub_config.py to check groups # c.GoogleOAuthenticator.scope = ['openid', 'email', 'https://www.googleapis.com/auth/admin.directory.group.readonly'] @@ -36,14 +47,14 @@ def _authorize_url_default(self): @default("token_url") def _token_url_default(self): - return "%s/oauth2/v4/token" % (google_api_base_url) + return "%s/oauth2/v4/token" % (self.google_api_url) google_group_whitelist = Set( config=True, help="Automatically whitelist members of selected groups" ) user_info_url = Unicode( - "%s/oauth2/v1/userinfo" % (google_api_base_url), config=True + "%s/oauth2/v1/userinfo" % (self.google_api_url), config=True ) hosted_domain = List( @@ -165,7 +176,7 @@ async def _check_group_whitelist(self, user_email, user_email_domain, access_tok # Check if user is a member of any group in the whitelist for group in map(url_escape, self.google_group_whitelist): url = "%s/admin/directory/v1/groups/%s/members/%s" % ( - google_api_base_url, + self.google_api_base_url, "%s@%s" % (user_email, user_email_domain), user_email, ) From 41301289594644a5c3651e4201c144e4c72e3e36 Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Mon, 9 Mar 2020 18:37:24 -0500 Subject: [PATCH 06/11] User info self --- oauthenticator/google.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index 2bb85eca..076b9671 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -25,7 +25,7 @@ class GoogleOAuthenticator(OAuthenticator, GoogleOAuth2Mixin): google_api_url = Unicode("https://www.googleapis.com", config=True) @default('google_api_base_url') - def _google_api_base_url(self) + def _default_google_api_base_url(self): """get default google apis url from env""" google_api_url = os.getenv('GOOGLE_API_URL') @@ -53,9 +53,11 @@ def _token_url_default(self): config=True, help="Automatically whitelist members of selected groups" ) - user_info_url = Unicode( - "%s/oauth2/v1/userinfo" % (self.google_api_url), config=True - ) + user_info_url = Unicode(config=True) + + @default('user_info_url') + def _user_info_url(self): + return "%s/oauth2/v1/userinfo" % (self.google_api_url) hosted_domain = List( Unicode(), From 60f02a757eea28b4b24407233a8d66f558f6e7f6 Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Mon, 9 Mar 2020 21:54:41 -0500 Subject: [PATCH 07/11] this works with GSuite groups --- oauthenticator/google.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index 076b9671..c9332311 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -9,7 +9,7 @@ import urllib.parse from tornado import gen -from tornado.httpclient import AsyncHTTPClient +from tornado.httpclient import HTTPRequest, AsyncHTTPClient from tornado.auth import GoogleOAuth2Mixin from tornado.web import HTTPError @@ -24,8 +24,8 @@ class GoogleOAuthenticator(OAuthenticator, GoogleOAuth2Mixin): google_api_url = Unicode("https://www.googleapis.com", config=True) - @default('google_api_base_url') - def _default_google_api_base_url(self): + @default('google_api_url') + def _google_api_url(self): """get default google apis url from env""" google_api_url = os.getenv('GOOGLE_API_URL') @@ -53,11 +53,9 @@ def _token_url_default(self): config=True, help="Automatically whitelist members of selected groups" ) - user_info_url = Unicode(config=True) - - @default('user_info_url') - def _user_info_url(self): - return "%s/oauth2/v1/userinfo" % (self.google_api_url) + user_info_url = Unicode( + "https://www.googleapis.com/oauth2/v1/userinfo", config=True + ) hosted_domain = List( Unicode(), @@ -155,7 +153,7 @@ async def authenticate(self, handler, data=None): if self.google_group_whitelist: is_group_specified = True - user_in_group = await self._check_group_whitelist(user_id, access_token) + user_in_group = await self._check_group_whitelist(user_email, user_email_domain, access_token) no_config_specified = not is_group_specified @@ -168,21 +166,21 @@ async def authenticate(self, handler, data=None): 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, } else: - self.log.warning("%s not in group or project whitelist", username) + self.log.warning("%s not in group whitelist", username) return None async def _check_group_whitelist(self, user_email, user_email_domain, access_token): http_client = AsyncHTTPClient() - headers = _api_headers(access_token) # Check if user is a member of any group in the whitelist - for group in map(url_escape, self.google_group_whitelist): - url = "%s/admin/directory/v1/groups/%s/members/%s" % ( - self.google_api_base_url, - "%s@%s" % (user_email, user_email_domain), + for group in self.google_group_whitelist: + url = "%s/admin/directory/v1/groups/%s/members/%s?access_token=%s" % ( + self.google_api_url, + "%s@%s" % (group, user_email_domain), user_email, + access_token, ) - req = HTTPRequest(url, method="GET", headers=headers) + req = HTTPRequest(url, method="GET") resp = await http_client.fetch(req, raise_error=False) if resp.code == 200: return True # user _is_ in group From 34fbb2275db9d034e9b0c834f5e9915c2d54e058 Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Tue, 10 Mar 2020 12:10:51 -0500 Subject: [PATCH 08/11] Now you can specify which google groups would give its members admin access to jupyterhub --- oauthenticator/google.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index c9332311..eaac36f1 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -53,6 +53,10 @@ def _token_url_default(self): config=True, help="Automatically whitelist members of selected groups" ) + admin_google_groups = Set( + config=True, help="Groups whose members should have Jupyterhub admin privileges" + ) + user_info_url = Unicode( "https://www.googleapis.com/oauth2/v1/userinfo", config=True ) @@ -146,6 +150,9 @@ async def authenticate(self, handler, data=None): # unambiguous domain, use only base name username = user_email.split('@')[0] + if self.admin_google_groups: + is_admin = await self._check_user_in_groups(self.admin_google_groups , user_email, user_email_domain, access_token) + # Check if user is a member of any whitelisted groups or projects. # These checks are performed here, as it requires `access_token`. user_in_group = False @@ -153,14 +160,22 @@ async def authenticate(self, handler, data=None): if self.google_group_whitelist: is_group_specified = True - user_in_group = await self._check_group_whitelist(user_email, user_email_domain, access_token) + user_in_group = await self._check_user_in_groups(self.google_group_whitelist , user_email, user_email_domain, access_token) no_config_specified = not is_group_specified - if ( + if is_admin: + self.log.info("%s is in the admin group", username) + return { + 'name': username, + 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, + 'admin': is_admin, + } + elif ( (is_group_specified and user_in_group) or no_config_specified ): + self.log.info("%s can login on this server", username) return { 'name': username, 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, @@ -170,10 +185,10 @@ async def authenticate(self, handler, data=None): return None - async def _check_group_whitelist(self, user_email, user_email_domain, access_token): + async def _check_user_in_groups(self, groups, user_email, user_email_domain, access_token): http_client = AsyncHTTPClient() # Check if user is a member of any group in the whitelist - for group in self.google_group_whitelist: + for group in groups: url = "%s/admin/directory/v1/groups/%s/members/%s?access_token=%s" % ( self.google_api_url, "%s@%s" % (group, user_email_domain), From 10c57df7f22e941ffdd4347e88b6353b236d6051 Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Tue, 10 Mar 2020 14:13:11 -0500 Subject: [PATCH 09/11] only send is_admin when admin_group is specified --- oauthenticator/google.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index eaac36f1..3878386a 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -150,10 +150,15 @@ async def authenticate(self, handler, data=None): # unambiguous domain, use only base name username = user_email.split('@')[0] + # Check if user is a member of any admin groups. + # These checks are performed here, as it requires `access_token`. + is_admin = False + is_admin_group_specified = False if self.admin_google_groups: + is_admin_group_specified = True is_admin = await self._check_user_in_groups(self.admin_google_groups , user_email, user_email_domain, access_token) - # Check if user is a member of any whitelisted groups or projects. + # Check if user is a member of any whitelisted groups. # These checks are performed here, as it requires `access_token`. user_in_group = False is_group_specified = False @@ -164,13 +169,20 @@ async def authenticate(self, handler, data=None): no_config_specified = not is_group_specified - if is_admin: + if is_admin_group_specified and is_admin: self.log.info("%s is in the admin group", username) return { 'name': username, 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, 'admin': is_admin, } + elif is_admin_group_specified and is_group_specified and user_in_group: + self.log.info("%s can login on this server", username) + return { + 'name': username, + 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, + 'admin': is_admin, + } elif ( (is_group_specified and user_in_group) or no_config_specified From b540afa5fcf1e9486c08e4656c6e40935c71238c Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Tue, 10 Mar 2020 15:36:35 -0500 Subject: [PATCH 10/11] Removing gitlab.com comment and changing log to debug --- oauthenticator/google.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index 3878386a..9a507cb8 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -29,7 +29,7 @@ def _google_api_url(self): """get default google apis url from env""" google_api_url = os.getenv('GOOGLE_API_URL') - # default to gitlab.com + # default to googleapis.com if not google_api_url: google_api_url = 'https://www.googleapis.com' @@ -170,14 +170,14 @@ async def authenticate(self, handler, data=None): no_config_specified = not is_group_specified if is_admin_group_specified and is_admin: - self.log.info("%s is in the admin group", username) + self.log.debug("%s is in the admin group", username) return { 'name': username, 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, 'admin': is_admin, } elif is_admin_group_specified and is_group_specified and user_in_group: - self.log.info("%s can login on this server", username) + self.log.debug"%s can login on this server", username) return { 'name': username, 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, @@ -187,7 +187,7 @@ async def authenticate(self, handler, data=None): (is_group_specified and user_in_group) or no_config_specified ): - self.log.info("%s can login on this server", username) + self.log.debug("%s can login on this server", username) return { 'name': username, 'auth_state': {'access_token': access_token, 'google_user': bodyjs}, From 7026e0b98b60235b7cafa5325f061e5a6d35dc5c Mon Sep 17 00:00:00 2001 From: Ricardo Rosales Date: Tue, 10 Mar 2020 16:12:15 -0500 Subject: [PATCH 11/11] Adding ( to debug log --- oauthenticator/google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauthenticator/google.py b/oauthenticator/google.py index 9a507cb8..e3ce157b 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -177,7 +177,7 @@ async def authenticate(self, handler, data=None): 'admin': is_admin, } elif is_admin_group_specified and is_group_specified and user_in_group: - self.log.debug"%s can login on this server", username) + self.log.debug("%s can login on this server", username) return { 'name': username, 'auth_state': {'access_token': access_token, 'google_user': bodyjs},