From 435ec2eb1c82ebeede0fb596e4340e239d7f08c2 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Thu, 7 Sep 2023 14:43:01 -0400 Subject: [PATCH] Checkin. No-Issue Signed-off-by: James Tanner --- galaxy_ng/app/utils/rbac.py | 18 +- galaxy_ng/social/__init__.py | 252 +++++++++++++++++- galaxy_ng/social/__init__.py.orig | 184 ------------- galaxy_ng/social/pipeline/user.py | 37 ++- .../test_community_namespace_rbac.py | 31 ++- 5 files changed, 326 insertions(+), 196 deletions(-) mode change 120000 => 100644 galaxy_ng/social/__init__.py delete mode 100644 galaxy_ng/social/__init__.py.orig diff --git a/galaxy_ng/app/utils/rbac.py b/galaxy_ng/app/utils/rbac.py index fe8ec92032..796f004e7c 100644 --- a/galaxy_ng/app/utils/rbac.py +++ b/galaxy_ng/app/utils/rbac.py @@ -78,5 +78,19 @@ def get_v3_namespace_owners(namespace: Namespace) -> list: include_model_permissions=False ) owners.extend(list(current_users)) - owners = sorted(set(owners)) - return owners + unique_owners = [] + for owner in owners: + if owner not in unique_owners: + unique_owners.append(owner) + return unique_owners + + +def get_owned_v3_namespaces(user: User): + + owned = [] + for namespace in Namespace.objects.all(): + owners = get_v3_namespace_owners(namespace) + if user in owners: + owned.append(namespace) + + return owned diff --git a/galaxy_ng/social/__init__.py b/galaxy_ng/social/__init__.py deleted file mode 120000 index 641a46981f..0000000000 --- a/galaxy_ng/social/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__init__.py.orig \ No newline at end of file diff --git a/galaxy_ng/social/__init__.py b/galaxy_ng/social/__init__.py new file mode 100644 index 0000000000..e8173cdb0d --- /dev/null +++ b/galaxy_ng/social/__init__.py @@ -0,0 +1,251 @@ +import logging +import requests + +from django.conf import settings +from django.db import transaction +from social_core.backends.github import GithubOAuth2 + +from galaxy_ng.app.models.auth import Group, User +from galaxy_ng.app.models import Namespace +from galaxy_ng.app.api.v1.models import LegacyNamespace +from galaxy_ng.app.utils import rbac +from galaxy_ng.app.utils import namespaces as ns_utils + +from galaxy_importer.constants import NAME_REGEXP + + +GITHUB_ACCOUNT_SCOPE = 'github' + +logger = logging.getLogger(__name__) + + +def logged(func): + def wrapper(*args, **kwargs): + logger.debug(f'LOGGED: {func}') + res = func(*args, **kwargs) + logger.debug(f'LOGGED: {func} {res}') + return res + return wrapper + + +# https://github.com/python-social-auth +# https://github.com/python-social-auth/social-core +class GalaxyNGOAuth2(GithubOAuth2): + + @logged + def get_session_state(self): + param = self.name + '_state' + sstate = self.strategy.session_get(param) + return sstate + + @logged + def do_auth(self, access_token, *args, **kwargs): + """Finish the auth process once the access_token was retrieved""" + + # userdata = id, login, access_token + data = self.get_github_user(access_token) + # print('-' * 100) + # from pprint import pprint + # pprint(data) + # print('-' * 100) + + # extract the login now to prevent mutation + gid = data['id'] + login = data['login'] + email = data['email'] + + # ensure access_token is passed in as a kwarg + if data is not None and 'access_token' not in data: + data['access_token'] = access_token + + kwargs.update({'response': data, 'backend': self}) + + # use upstream auth method to get or create the new user ... + auth_response = self.strategy.authenticate(*args, **kwargs) + + v3_namespace = self.handle_v3_namespace(auth_response, email, login, gid) + + ''' + # create a legacynamespace? + legacy_namespace, _ = self._ensure_legacynamespace(login) + + # define namespace, validate and create ... + namespace_name = self.transform_namespace_name(login) + if self.validate_namespace_name(namespace_name): + + # Need user for group and rbac binding + user = User.objects.filter(username=login).first() + + # Create a group to bind rbac perms. + group, _ = self._ensure_group(namespace_name, user) + + # create a v3 namespace? + namespace, _ = self._ensure_namespace(namespace_name, user, group) + ''' + + return auth_response + + def handle_v3_namespace(self, session_user, session_email, session_login, github_id): + + print(f'HANDLING V3 NAMESPACE session_user:{session_user} session_email:{session_email} session_login:{session_login}') + + # first make the namespace name ... + namespace_name = self.transform_namespace_name(session_login) + + print(f'TRANSFORMED NAME: {namespace_name}') + + if not self.validate_namespace_name(namespace_name): + print(f'DID NOT VALIDATE NAMESPACE NAME: {namespace_name}') + return + + # does the namespace already exist? + found_namespace = Namespace.objects.filter(name=namespace_name).first() + + print('\n' * 10) + print(f'FOUND NAMESPACE: {found_namespace}') + print('\n' * 10) + + # is it owned by this userid? + if found_namespace: + print(f'FOUND EXISTING NAMESPACE: {found_namespace}') + owners = rbac.get_v3_namespace_owners(found_namespace) + print(f'FOUND EXISTING OWNERS: {owners}') + + if session_user in owners: + return found_namespace + + # should always have a namespace ... + if found_namespace: + print(f'GENERATING A NEW NAMESPACE NAME SINCE USER DOES NOT OWN {found_namespace}') + namespace_name = self.generate_available_namespace_name(session_login, github_id) + print(f'FINAL NAMESPACE NAME {namespace_name}') + + # Create a group to bind rbac perms. + group, _ = self._ensure_group(namespace_name, session_user) + + # create a v3 namespace? + namespace, _ = self._ensure_namespace(namespace_name, session_user, group) + + owned = rbac.get_owned_v3_namespaces(session_user) + print(f'NS OWNED BY {session_user}: {owned}') + + return namespace + + def generate_available_namespace_name(self, session_login, github_id): + # we're only here because session_login is already taken as a namespace name and we need a new one for the user + + # this makes the weird gh_{BLAH} name ... + #namespace_name = ns_utils.map_v3_namespace(session_login) + + # we should iterate and append 0,1,2,3,4,5,etc on the login name until we find one that is free + counter = -1 + while True: + counter += 1 + namespace_name = self.transform_namespace_name(session_login) + str(counter) + if Namespace.objects.filter(name=namespace_name).count() == 0: + return namespace_name + + def validate_namespace_name(self, name): + """Similar validation to the v3 namespace serializer.""" + + # galaxy has "extra" requirements for a namespace ... + # https://github.com/ansible/galaxy-importer/blob/master/galaxy_importer/constants.py#L45 + # NAME_REGEXP = re.compile(r"^(?!.*__)[a-z]+[0-9a-z_]*$") + + if not NAME_REGEXP.match(name): + return False + if len(name) < 2: + return False + if name.startswith('_'): + return False + return True + + def transform_namespace_name(self, name): + """Convert namespace name to valid v3 name.""" + return name.replace('-', '_').lower() + + def _ensure_group(self, namespace_name, user): + """Create a group in the form of :""" + with transaction.atomic(): + group, created = \ + Group.objects.get_or_create_identity('namespace', namespace_name) + if created: + rbac.add_user_to_group(user, group) + return group, created + + def _ensure_namespace(self, namespace_name, user, group): + """Create an auto v3 namespace for the account""" + + with transaction.atomic(): + namespace, created = Namespace.objects.get_or_create(name=namespace_name) + owners = rbac.get_v3_namespace_owners(namespace) + if created or not owners: + # Binding by user breaks the UI workflow ... + # rbac.add_user_to_v3_namespace(user, namespace) + rbac.add_group_to_v3_namespace(group, namespace) + + return namespace, created + + def _ensure_legacynamespace(self, login): + """Create an auto legacynamespace for the account""" + + # userdata = id, login, access_token + user = User.objects.filter(username=login).first() + + # make the namespace + with transaction.atomic(): + legacy_namespace, created = \ + LegacyNamespace.objects.get_or_create( + name=login + ) + + # add the user to the owners + if created: + legacy_namespace.owners.add(user) + + return legacy_namespace, created + + @logged + def get_github_access_token(self, code): + baseurl = settings.SOCIAL_AUTH_GITHUB_BASE_URL + url = baseurl + '/login/oauth/access_token' + rr = requests.post( + f'{url}', + headers={'Accept': 'application/json'}, + json={ + 'code': code, + 'client_id': settings.SOCIAL_AUTH_GITHUB_KEY, + 'client_secret': settings.SOCIAL_AUTH_GITHUB_SECRET + } + ) + + ds = rr.json() + access_token = ds['access_token'] + return access_token + + @logged + def get_github_user(self, access_token): + api_url = settings.SOCIAL_AUTH_GITHUB_API_URL + url = api_url + '/user' + rr = requests.get( + f'{url}', + headers={ + 'Accept': 'application/json', + 'Authorization': f'token {access_token}' + }, + ) + return rr.json() + + @logged + def auth_complete(self, *args, **kwargs): + self.process_error(self.data) + + request = kwargs['request'] + code = request.GET.get('code', None) + access_token = self.get_github_access_token(code) + + return self.do_auth( + access_token, + *args, + **kwargs + ) diff --git a/galaxy_ng/social/__init__.py.orig b/galaxy_ng/social/__init__.py.orig deleted file mode 100644 index e8deda16e1..0000000000 --- a/galaxy_ng/social/__init__.py.orig +++ /dev/null @@ -1,184 +0,0 @@ -import logging -import requests - -from django.conf import settings -from django.db import transaction -from social_core.backends.github import GithubOAuth2 - -from galaxy_ng.app.models.auth import Group, User -from galaxy_ng.app.models import Namespace -from galaxy_ng.app.api.v1.models import LegacyNamespace -from galaxy_ng.app.utils import rbac - -from galaxy_importer.constants import NAME_REGEXP - - -GITHUB_ACCOUNT_SCOPE = 'github' - -logger = logging.getLogger(__name__) - - -def logged(func): - def wrapper(*args, **kwargs): - logger.debug(f'LOGGED: {func}') - res = func(*args, **kwargs) - logger.debug(f'LOGGED: {func} {res}') - return res - return wrapper - - -# https://github.com/python-social-auth -# https://github.com/python-social-auth/social-core -class GalaxyNGOAuth2(GithubOAuth2): - - @logged - def get_session_state(self): - param = self.name + '_state' - sstate = self.strategy.session_get(param) - return sstate - - @logged - def do_auth(self, access_token, *args, **kwargs): - """Finish the auth process once the access_token was retrieved""" - - # userdata = id, login, access_token - data = self.get_github_user(access_token) - # print('-' * 100) - # from pprint import pprint - # pprint(data) - # print('-' * 100) - - # extract the login now to prevent mutation - login = data['login'] - - # ensure access_token is passed in as a kwarg - if data is not None and 'access_token' not in data: - data['access_token'] = access_token - - kwargs.update({'response': data, 'backend': self}) - - # use upstream auth method - auth_response = self.strategy.authenticate(*args, **kwargs) - - # create a legacynamespace? - legacy_namespace, _ = self._ensure_legacynamespace(login) - - # define namespace, validate and create ... - namespace_name = self.transform_namespace_name(login) - if self.validate_namespace_name(namespace_name): - - # Need user for group and rbac binding - user = User.objects.filter(username=login).first() - - # Create a group to bind rbac perms. - group, _ = self._ensure_group(namespace_name, user) - - # create a v3 namespace? - namespace, _ = self._ensure_namespace(namespace_name, user, group) - - return auth_response - - def validate_namespace_name(self, name): - """Similar validation to the v3 namespace serializer.""" - - # galaxy has "extra" requirements for a namespace ... - # https://github.com/ansible/galaxy-importer/blob/master/galaxy_importer/constants.py#L45 - # NAME_REGEXP = re.compile(r"^(?!.*__)[a-z]+[0-9a-z_]*$") - - if not NAME_REGEXP.match(name): - return False - if len(name) < 2: - return False - if name.startswith('_'): - return False - return True - - def transform_namespace_name(self, name): - """Convert namespace name to valid v3 name.""" - return name.replace('-', '_').lower() - - def _ensure_group(self, namespace_name, user): - """Create a group in the form of :""" - with transaction.atomic(): - group, created = \ - Group.objects.get_or_create_identity('namespace', namespace_name) - if created: - rbac.add_user_to_group(user, group) - return group, created - - def _ensure_namespace(self, name, user, group): - """Create an auto v3 namespace for the account""" - - with transaction.atomic(): - namespace, created = Namespace.objects.get_or_create(name=name) - owners = rbac.get_v3_namespace_owners(namespace) - if created or not owners: - # Binding by user breaks the UI workflow ... - # rbac.add_user_to_v3_namespace(user, namespace) - rbac.add_group_to_v3_namespace(group, namespace) - - return namespace, created - - def _ensure_legacynamespace(self, login): - """Create an auto legacynamespace for the account""" - - # userdata = id, login, access_token - user = User.objects.filter(username=login).first() - - # make the namespace - with transaction.atomic(): - legacy_namespace, created = \ - LegacyNamespace.objects.get_or_create( - name=login - ) - - # add the user to the owners - if created: - legacy_namespace.owners.add(user) - - return legacy_namespace, created - - @logged - def get_github_access_token(self, code): - baseurl = settings.SOCIAL_AUTH_GITHUB_BASE_URL - url = baseurl + '/login/oauth/access_token' - rr = requests.post( - f'{url}', - headers={'Accept': 'application/json'}, - json={ - 'code': code, - 'client_id': settings.SOCIAL_AUTH_GITHUB_KEY, - 'client_secret': settings.SOCIAL_AUTH_GITHUB_SECRET - } - ) - - ds = rr.json() - access_token = ds['access_token'] - return access_token - - @logged - def get_github_user(self, access_token): - api_url = settings.SOCIAL_AUTH_GITHUB_API_URL - url = api_url + '/user' - rr = requests.get( - f'{url}', - headers={ - 'Accept': 'application/json', - 'Authorization': f'token {access_token}' - }, - ) - return rr.json() - - @logged - def auth_complete(self, *args, **kwargs): - self.process_error(self.data) - - request = kwargs['request'] - code = request.GET.get('code', None) - access_token = self.get_github_access_token(code) - - return self.do_auth( - access_token, - *args, - **kwargs - ) diff --git a/galaxy_ng/social/pipeline/user.py b/galaxy_ng/social/pipeline/user.py index f3597f89d0..92ea916345 100644 --- a/galaxy_ng/social/pipeline/user.py +++ b/galaxy_ng/social/pipeline/user.py @@ -2,6 +2,7 @@ from galaxy_ng.app.models.auth import User +from galaxy_ng.app.utils import namespaces as ns_utils USER_FIELDS = ['username', 'email'] @@ -25,6 +26,10 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): print('-' * 100) if user: + if user.username != details.get('username'): + print(f'FIXING EXISTING USERs USERNAME') + user.username = details.get('username') + user.save() print(f'SKIPPING USER CREATE: ALREADY EXISTS') return {'is_new': False} @@ -40,6 +45,7 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): # bypass the strange logic that can't find the user ... ? username = details.get('username') + email = details.get('email') github_id = details.get('id') if not github_id: github_id = kwargs['response']['id'] @@ -48,14 +54,29 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs): print(f'USERNAME:{username} GITHUB_ID:{github_id}') print('-----------------------------------------') - if username: - found_user = User.objects.filter(username=username).first() - if found_user is not None: - print(f'SKIPPING USER CREATE: FOUND USER') - return { - 'is_new': False, - 'user': found_user - } + #found_user = User.objects.filter(username=username).first() + #found_user = User.objects.filter(username=username, email=email).first() + found_email = User.objects.filter(email=email).first() + if found_email is not None: + + # fix the username if they've changed their login since last time + if found_email.username != username: + print(f'FIXING FOUND USERs USERNAME') + found_email.username = username + found_email.save() + + print(f'SKIPPING USER CREATE: FOUND USER {found_email} username:{found_email.username} email:{found_email.email}') + return { + 'is_new': False, + 'user': found_email + } + + found_username = User.objects.filter(username=username).first() + if found_username is not None: + # we have an old user who's got the username but it's not the same person logging in ... + # so change that username? The email should be unique right? + found_username.username = found_username.email + found_username.save() print(f'\tCALLING {strategy}.create_user({fields})') # return self.storage.user.create_user(*args, **kwargs) diff --git a/galaxy_ng/tests/integration/community/test_community_namespace_rbac.py b/galaxy_ng/tests/integration/community/test_community_namespace_rbac.py index 61df4c7d7e..615b0667c9 100644 --- a/galaxy_ng/tests/integration/community/test_community_namespace_rbac.py +++ b/galaxy_ng/tests/integration/community/test_community_namespace_rbac.py @@ -179,6 +179,7 @@ def test_social_user_with_changed_login(ansible_config): ga.delete_user(login='sean-m-sullivan') cleanup_social_user('Wilk42', ansible_config) cleanup_social_user('sean-m-sullivan', ansible_config) + cleanup_social_user('sean-m-sullivan@redhat.com', ansible_config) base_cfg = ansible_config('github_user_1') @@ -203,6 +204,8 @@ def test_social_user_with_changed_login(ansible_config): ns_ds = ns_resp.json() namespace_names_a = [x['name'] for x in ns_ds['data']] + # make a sean_m_sullivan namespace owned by the old login ... ? + # make a new login new_login = 'sean-m-sullivan' @@ -234,6 +237,32 @@ def test_social_user_with_changed_login(ansible_config): c_resp = client.get('_ui/v1/me/') ns_resp_c = client.get('_ui/v1/my-namespaces/') ns_ds_c = ns_resp_c.json() - namespace_names_c = [x['name'] for x in ns_ds['data']] + namespace_names_c = [x['name'] for x in ns_ds_c['data']] + + # user_a and user_c should have different galaxy uids + assert a_resp.json()['id'] != c_resp.json()['id'] + + # user_c should have the old login + assert c_resp.json()['username'] == old_login + + # user_c should not own the namespaces of user_a + for ns in namespace_names_a: + assert ns not in namespace_names_c + + # now sean logs in with his new login ... + with SocialGithubClient(config=user_b) as client: + b_resp = client.get('_ui/v1/me/') + ns_resp_b = client.get('_ui/v1/my-namespaces/') + ns_ds_b = ns_resp_b.json() + namespace_names_b = [x['name'] for x in ns_ds_b['data']] + + # his ID should be the same + assert b_resp.json()['id'] == a_resp.json()['id'] + + # his username should be the new login + assert b_resp.json()['username'] == new_login + + # he should own his old namespace AND a transformed one from his new login + assert namespace_names_b == ['wilk42', 'sean_m_sullivan'] import epdb; epdb.st()