From 99c366b78fdea42287cd14ff0dca34f91da91ef0 Mon Sep 17 00:00:00 2001
From: James Tanner <tanner.jc@gmail.com>
Date: Thu, 7 Sep 2023 11:01:42 -0400
Subject: [PATCH] Checkin.

No-Issue

Signed-off-by: James Tanner <tanner.jc@gmail.com>
---
 galaxy_ng/app/api/v1/viewsets/namespaces.py   |   7 +-
 galaxy_ng/app/dynaconf_hooks.py               |  21 +-
 galaxy_ng/social/__init__.py                  | 232 +--------------
 galaxy_ng/social/__init__.py.orig             | 184 ++++++++++++
 galaxy_ng/social/__init__.py.patched          | 267 ++++++++++++++++++
 galaxy_ng/social/pipeline/social_auth.py      | 147 ++++++++++
 galaxy_ng/social/pipeline/user.py             |  77 ++++-
 .../test_community_namespace_rbac.py          |  89 ++++++
 galaxy_ng/tests/integration/utils/github.py   |  12 +-
 profiles/community/github_mock/flaskapp.py    |  77 +++--
 10 files changed, 852 insertions(+), 261 deletions(-)
 mode change 100644 => 120000 galaxy_ng/social/__init__.py
 create mode 100644 galaxy_ng/social/__init__.py.orig
 create mode 100644 galaxy_ng/social/__init__.py.patched
 create mode 100644 galaxy_ng/social/pipeline/social_auth.py

diff --git a/galaxy_ng/app/api/v1/viewsets/namespaces.py b/galaxy_ng/app/api/v1/viewsets/namespaces.py
index c40b462c0c..5fed4489f4 100644
--- a/galaxy_ng/app/api/v1/viewsets/namespaces.py
+++ b/galaxy_ng/app/api/v1/viewsets/namespaces.py
@@ -14,6 +14,7 @@
 from rest_framework.pagination import PageNumberPagination
 
 from galaxy_ng.app.access_control.access_policy import LegacyAccessPolicy
+from galaxy_ng.app.utils.rbac import get_v3_namespace_owners
 
 from galaxy_ng.app.models.auth import User
 from galaxy_ng.app.api.v1.models import (
@@ -100,7 +101,11 @@ class LegacyNamespaceOwnersViewSet(viewsets.GenericViewSet, mixins.ListModelMixi
     authentication_classes = GALAXY_AUTHENTICATION_CLASSES
 
     def get_queryset(self):
-        return get_object_or_404(LegacyNamespace, pk=self.kwargs["pk"]).owners.all()
+        #return get_object_or_404(LegacyNamespace, pk=self.kwargs["pk"]).owners.all()
+        ns = get_object_or_404(LegacyNamespace, pk=self.kwargs["pk"])
+        if ns.namespace:
+            return get_v3_namespace_owners(ns.namespace)
+        return []
 
     @extend_schema(
         parameters=[LegacyNamespaceOwnerSerializer(many=True)],
diff --git a/galaxy_ng/app/dynaconf_hooks.py b/galaxy_ng/app/dynaconf_hooks.py
index d031ef550b..8e955524eb 100755
--- a/galaxy_ng/app/dynaconf_hooks.py
+++ b/galaxy_ng/app/dynaconf_hooks.py
@@ -226,17 +226,24 @@ def configure_socialauth(settings: Dynaconf) -> Dict[str, Any]:
         # are associated to existing users instead of creating
         # a whole new randomized username.
         data['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.social_auth.social_details',
+            'galaxy_ng.social.pipeline.social_auth.social_details',
+            #'social_core.pipeline.social_auth.social_uid',
+            'galaxy_ng.social.pipeline.social_auth.social_uid',
+            #'social_core.pipeline.social_auth.auth_allowed',
+            'galaxy_ng.social.pipeline.social_auth.auth_allowed',
+            #'social_core.pipeline.social_auth.social_user',
+            'galaxy_ng.social.pipeline.social_auth.social_user',
             # 'social_core.pipeline.user.get_username',
             'galaxy_ng.social.pipeline.user.get_username',
             # 'social_core.pipeline.user.create_user',
             'galaxy_ng.social.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'
+            #'social_core.pipeline.social_auth.associate_user',
+            'galaxy_ng.social.pipeline.social_auth.associate_user',
+            #'social_core.pipeline.social_auth.load_extra_data',
+            'galaxy_ng.social.pipeline.social_auth.load_extra_data',
+            #'social_core.pipeline.user.user_details'
+            'galaxy_ng.social.pipeline.user.user_details'
         ]
 
     return data
diff --git a/galaxy_ng/social/__init__.py b/galaxy_ng/social/__init__.py
deleted file mode 100644
index d045dd8e5d..0000000000
--- a/galaxy_ng/social/__init__.py
+++ /dev/null
@@ -1,231 +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_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)
-
-        # 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
-
-        # extract the login now to prevent mutation
-        login = data['login']
-
-        # we need the uid for "security"
-        uid = data['id']
-
-        # craft a unique email
-        email = str(uid) + '@github.com'
-        data['email'] = email
-
-        print('-' * 100)
-        from pprint import pprint
-        pprint(data)
-        pprint(args)
-        pprint(kwargs)
-        print('-' * 100)
-
-        # 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
-
-        # use upstream auth method
-        kwargs.update({'response': data, 'backend': self})
-        user = self.strategy.authenticate(*args, **kwargs)
-
-        '''
-        print(f'AUTH RESPONSE: {type(auth_response)} {auth_response}')
-        return auth_response
-
-        # Need user for group and rbac binding
-        user = User.objects.filter(email=email).first()
-        if user is None:
-            user = User.objects.filter(username=login).first()
-        if not user:
-            raise Exception(f'user for {login} was not created {type(self.strategy)} {args} {kwargs}')
-        '''
-
-        print(f'USER: login:{user.username} email:{user.email}')
-
-        if user and user.username != login:
-            user.username = login
-            user.save()
-
-        # only create other resources if the user authenticates successfully
-        if user:
-
-            # create a legacynamespace?
-            legacy_namespace, _ = self._ensure_legacynamespace(login, uid=uid)
-
-            # define namespace, validate and create ...
-            namespace_name = self.transform_namespace_name(login)
-
-            if self.validate_namespace_name(namespace_name):
-
-                '''
-                # Create a group to bind rbac perms.
-                group, _ = self._ensure_group(namespace_name, user, uid=uid)
-                '''
-
-                # create a v3 namespace?
-                namespace, _ = self._ensure_namespace(namespace_name, user, None)
-
-                # set the foreign key ...
-                self.bind_v1_v3_namespaces(legacy_namespace, namespace)
-
-        return user
-
-    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()
-        return ns_utils.map_v3_namespace(name)
-
-    def _ensure_group(self, namespace_name, user, uid=None):
-        """Create a group in the form of <namespace>:<namespace_name>"""
-        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, uid=None):
-        """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, uid=None):
-        """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 and user:
-                if not legacy_namespace.owners.filter(id=user.id).exists():
-                    legacy_namespace.owners.add(user)
-
-        return legacy_namespace, created
-
-    def bind_v1_v3_namespaces(self, legacy_namespace, namespace):
-        print(f'BIND {type(legacy_namespace)}:{legacy_namespace} {type(namespace)}:{namespace}')
-        if legacy_namespace.namespace != namespace:
-            legacy_namespace.namespace = namespace
-            legacy_namespace.save()
-            print(f'\tBOUND {legacy_namespace.namespace}')
-
-    @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 b/galaxy_ng/social/__init__.py
new file mode 120000
index 0000000000..641a46981f
--- /dev/null
+++ b/galaxy_ng/social/__init__.py
@@ -0,0 +1 @@
+__init__.py.orig
\ No newline at end of file
diff --git a/galaxy_ng/social/__init__.py.orig b/galaxy_ng/social/__init__.py.orig
new file mode 100644
index 0000000000..e8deda16e1
--- /dev/null
+++ b/galaxy_ng/social/__init__.py.orig
@@ -0,0 +1,184 @@
+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 <namespace>:<namespace_name>"""
+        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/__init__.py.patched b/galaxy_ng/social/__init__.py.patched
new file mode 100644
index 0000000000..2f9647d071
--- /dev/null
+++ b/galaxy_ng/social/__init__.py.patched
@@ -0,0 +1,267 @@
+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)
+
+        # 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
+
+        # extract the login now to prevent mutation
+        login = data['login']
+
+        # we need the uid for "security"
+        uid = data['id']
+
+        # craft a unique email
+        email = str(uid) + '@github.com'
+        data['email'] = email
+
+        print('-' * 100)
+        from pprint import pprint
+        pprint(data)
+        pprint(args)
+        pprint(kwargs)
+        print('-' * 100)
+
+        # 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
+
+        '''
+        # use upstream auth method
+        kwargs.update({'response': data, 'backend': self})
+        print(f'STRATEGY: {self.strategy} type:{type(self.strategy)}')
+        user = self.strategy.authenticate(*args, **kwargs)
+        '''
+
+        #print(f'AUTH RESPONSE: {type(auth_response)} {auth_response}')
+        #return auth_response
+
+        # Need user for group and rbac binding
+        #user = User.objects.filter(email=email).first()
+        #if user is None:
+        #    user = User.objects.filter(username=login).first()
+
+        user = None
+        if user is None:
+            kwargs.update({'response': data, 'backend': self})
+            user = self.strategy.authenticate(*args, **kwargs)
+
+        if not user:
+            raise Exception(f'user for {login} was not created {type(self.strategy)} {args} {kwargs}')
+
+        '''
+        def load_extra_data(backend, details, response, uid, user, *args, **kwargs):
+            social = kwargs.get("social") or backend.strategy.storage.user.get_social_auth(
+                backend.name, uid
+            )
+            if social:
+                extra_data = backend.extra_data(user, uid, response, details, *args, **kwargs)
+                social.set_extra_data(extra_data)
+                print(f'USER: login:{user.username} email:{user.email}')
+
+                # synchronize the found github login to the galaxy login
+                if user and user.username != login:
+
+                    # did some other user have this login before ...?
+
+                    user.username = login
+                    user.save()
+        '''
+
+        '''
+        # what is the backend? what is backend.name? can we get the social user with it?
+        for x in dir(self):
+            print(x)
+            if x == 'strategy':
+                for y in dir(self.strategy):
+                    print(f'\t{y}')
+
+        backend = self.strategy.get_backend()
+        print(f'backend:{backend}')
+        '''
+
+        #social_user = self.strategy.storage.user.get_social_auth(self.name, user.id)
+        social_user = kwargs.get('social')
+        print(f'XXXX -> SOCIAL_USER:{social_user}')
+
+        # only create other resources if the user authenticates successfully
+        if user:
+
+            # create a legacynamespace?
+            legacy_namespace, _ = self._ensure_legacynamespace(login, uid=uid)
+
+            # define namespace, validate and create ...
+            namespace_name = self.transform_namespace_name(login)
+
+            if self.validate_namespace_name(namespace_name):
+
+                '''
+                # Create a group to bind rbac perms.
+                group, _ = self._ensure_group(namespace_name, user, uid=uid)
+                '''
+
+                # create a v3 namespace?
+                namespace, _ = self._ensure_namespace(namespace_name, user, None)
+
+                # set the foreign key ...
+                self.bind_v1_v3_namespaces(legacy_namespace, namespace)
+
+        return user
+
+    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()
+        return ns_utils.map_v3_namespace(name)
+
+    def _ensure_group(self, namespace_name, user, uid=None):
+        """Create a group in the form of <namespace>:<namespace_name>"""
+        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, uid=None):
+        """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, uid=None):
+        """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 and user:
+                if not legacy_namespace.owners.filter(id=user.id).exists():
+                    legacy_namespace.owners.add(user)
+
+        return legacy_namespace, created
+
+    def bind_v1_v3_namespaces(self, legacy_namespace, namespace):
+        print(f'BIND {type(legacy_namespace)}:{legacy_namespace} {type(namespace)}:{namespace}')
+        if legacy_namespace.namespace != namespace:
+            legacy_namespace.namespace = namespace
+            legacy_namespace.save()
+            print(f'\tBOUND {legacy_namespace.namespace}')
+
+    @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/social_auth.py b/galaxy_ng/social/pipeline/social_auth.py
new file mode 100644
index 0000000000..17196c940f
--- /dev/null
+++ b/galaxy_ng/social/pipeline/social_auth.py
@@ -0,0 +1,147 @@
+#from ..exceptions import AuthAlreadyAssociated, AuthException, AuthForbidden
+from social_core.exceptions import AuthAlreadyAssociated, AuthException, AuthForbidden
+
+
+def social_details(backend, details, response, *args, **kwargs):
+    print('SOCIAL_DETAILS')
+    res = {"details": dict(backend.get_user_details(response), **details)}
+    print(f'\t{res}')
+    return {"details": dict(backend.get_user_details(response), **details)}
+
+
+def social_uid(backend, details, response, *args, **kwargs):
+    print('SOCIAL_UID')
+    res = {"uid": backend.get_user_id(details, response)}
+    print(f'\t{res}')
+    return {"uid": backend.get_user_id(details, response)}
+
+
+def auth_allowed(backend, details, response, *args, **kwargs):
+    print('AUTH_ALLOWED')
+    res = backend.auth_allowed(response, details)
+    print(f'\t{res}')
+    if not backend.auth_allowed(response, details):
+        raise AuthForbidden(backend)
+
+
+def social_user(backend, uid, user=None, *args, **kwargs):
+    """
+    This attempts to find the local user and a related social user.
+    Upon first login, the user and the social user should be None,
+    thereby resulting in this returning new_association=True
+
+    Afterwards, according to the config the next steps are ...
+        1) create_user
+        2) associate_user
+    """
+
+    print('SOCIAL_USER')
+    provider = backend.name
+    social = backend.strategy.storage.user.get_social_auth(provider, uid)
+    if social:
+        if user and social.user != user:
+            print('\traise AuthAlreadyAssociated')
+            raise AuthAlreadyAssociated(backend)
+        elif not user:
+            print(f'\tuser socia.user:{social.user}')
+            user = social.user
+
+    res = {
+        "social": social,
+        "user": user,
+        "is_new": user is None,
+        "new_association": social is None,
+    }
+    print(f'\tSOCIAL_USER RESULT:{res}')
+
+    return {
+        "social": social,
+        "user": user,
+        "is_new": user is None,
+        "new_association": social is None,
+    }
+
+
+def associate_user(backend, uid, user=None, social=None, *args, **kwargs):
+    print('ASSOCIATE_USER')
+    print(f'\tbackend:{backend}')
+    print(f'\tuser:{user}')
+    print(f'\tsocial:{user}')
+    print(f'\targs:{args}')
+    print(f'\tkwargs:{kwargs}')
+
+    if user and not social:
+        try:
+            social = backend.strategy.storage.user.create_social_auth(
+                user, uid, backend.name
+            )
+        except Exception as err:
+            if not backend.strategy.storage.is_integrity_error(err):
+                raise
+            # Protect for possible race condition, those bastard with FTL
+            # clicking capabilities, check issue #131:
+            #   https://github.com/omab/django-social-auth/issues/131
+            result = social_user(backend, uid, user, *args, **kwargs)
+            # Check if matching social auth really exists. In case it does
+            # not, the integrity error probably had different cause than
+            # existing entry and should not be hidden.
+            if not result["social"]:
+                raise
+            return result
+        else:
+            return {"social": social, "user": social.user, "new_association": True}
+
+
+def associate_by_email(backend, details, user=None, *args, **kwargs):
+    """
+    Associate current auth with a user with the same email address in the DB.
+
+    This pipeline entry is not 100% secure unless you know that the providers
+    enabled enforce email verification on their side, otherwise a user can
+    attempt to take over another user account by using the same (not validated)
+    email address on some provider.  This pipeline entry is disabled by
+    default.
+    """
+    print('ASSOCIATE_BY_EMAIL')
+    if user:
+        return None
+
+    email = details.get("email")
+    if email:
+        # Try to associate accounts registered with the same email address,
+        # only if it's a single object. AuthException is raised if multiple
+        # objects are returned.
+        users = list(backend.strategy.storage.user.get_users_by_email(email))
+        if len(users) == 0:
+            return None
+        elif len(users) > 1:
+            raise AuthException(
+                backend, "The given email address is associated with another account"
+            )
+        else:
+            return {"user": users[0], "is_new": False}
+
+
+def load_extra_data(backend, details, response, uid, user, *args, **kwargs):
+
+    print('LOAD EXTRA DATA!!!')
+    '''
+    print(f'\tbackend:{backend}')
+    print(f'\tdetails:{details}')
+    print(f'\tresponse:{response}')
+    print(f'\tuid:{uid}')
+    print(f'\tuser:{user}')
+    print(f'\targs:{kwargs}')
+    print(f'\tkwargs:{kwargs}')
+    '''
+
+    social = kwargs.get("social") or backend.strategy.storage.user.get_social_auth(
+        backend.name, uid
+    )
+
+    if social:
+
+        print(f'\tSOCIAL: {social} TYPE:{type(social)}')
+
+        extra_data = backend.extra_data(user, uid, response, details, *args, **kwargs)
+        social.set_extra_data(extra_data)
diff --git a/galaxy_ng/social/pipeline/user.py b/galaxy_ng/social/pipeline/user.py
index 0dddc27955..f3597f89d0 100644
--- a/galaxy_ng/social/pipeline/user.py
+++ b/galaxy_ng/social/pipeline/user.py
@@ -14,7 +14,14 @@ def get_username(strategy, details, backend, user=None, *args, **kwargs):
 def create_user(strategy, details, backend, user=None, *args, **kwargs):
 
     print('-' * 100)
-    print(f'CREATE USER user:{user} details:{details} args:{args} kwargs:{kwargs}')
+    print(f'CREATE USER')
+    print(f'\t strat:{strategy}')
+    print(f'\t backend:{backend}')
+    print(f'\t user:{user}')
+    print(f'\t user-type:{type(user)}')
+    print(f'\t details:{details}')
+    print(f'\t args:{args}')
+    print(f'\t kwargs:{kwargs}')
     print('-' * 100)
 
     if user:
@@ -25,6 +32,7 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs):
         (name, kwargs.get(name, details.get(name)))
         for name in backend.setting('USER_FIELDS', USER_FIELDS)
     )
+    print(f'FIELDS:{fields}')
 
     if not fields:
         print(f'SKIPPING USER CREATE: NO FIELDS')
@@ -32,6 +40,14 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs):
 
     # bypass the strange logic that can't find the user ... ?
     username = details.get('username')
+    github_id = details.get('id')
+    if not github_id:
+        github_id = kwargs['response']['id']
+
+    print('-----------------------------------------')
+    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:
@@ -41,8 +57,67 @@ def create_user(strategy, details, backend, user=None, *args, **kwargs):
                 'user': found_user
             }
 
+    print(f'\tCALLING {strategy}.create_user({fields})')
+    # return self.storage.user.create_user(*args, **kwargs)
+    print(f'\t\tSTORAGE:{strategy.storage} TYPE:{type(strategy.storage)}')
     new_user = strategy.create_user(**fields)
     return {
         'is_new': True,
         'user': new_user
     }
+
+
+def user_details(strategy, details, backend, user=None, *args, **kwargs):
+    """Update user details using data from provider."""
+
+    print('USER_DETAILS')
+    print(f'\tstrat:{strategy} details:{details} backend:{backend} user:{user} args:{args} kwargs:{kwargs}')
+
+    if not user:
+        print('\tNO USER RETURNING')
+        return
+
+    changed = False  # flag to track changes
+
+    # Default protected user fields (username, id, pk and email) can be ignored
+    # by setting the SOCIAL_AUTH_NO_DEFAULT_PROTECTED_USER_FIELDS to True
+    if strategy.setting("NO_DEFAULT_PROTECTED_USER_FIELDS") is True:
+        protected = ()
+    else:
+        protected = (
+            "username",
+            "id",
+            "pk",
+            "email",
+            "password",
+            "is_active",
+            "is_staff",
+            "is_superuser",
+        )
+
+    protected = protected + tuple(strategy.setting("PROTECTED_USER_FIELDS", []))
+
+    # Update user model attributes with the new data sent by the current
+    # provider. Update on some attributes is disabled by default, for
+    # example username and id fields. It's also possible to disable update
+    # on fields defined in SOCIAL_AUTH_PROTECTED_USER_FIELDS.
+    field_mapping = strategy.setting("USER_FIELD_MAPPING", {}, backend)
+    for name, value in details.items():
+        # Convert to existing user field if mapping exists
+        name = field_mapping.get(name, name)
+        if value is None or not hasattr(user, name) or name in protected:
+            continue
+
+        current_value = getattr(user, name, None)
+        if current_value == value:
+            continue
+
+        immutable_fields = tuple(strategy.setting("IMMUTABLE_USER_FIELDS", []))
+        if name in immutable_fields and current_value:
+            continue
+
+        changed = True
+        setattr(user, name, value)
+
+    if changed:
+        strategy.storage.user.changed(user)
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 4a21baec63..61df4c7d7e 100644
--- a/galaxy_ng/tests/integration/community/test_community_namespace_rbac.py
+++ b/galaxy_ng/tests/integration/community/test_community_namespace_rbac.py
@@ -148,3 +148,92 @@ def test_social_auth_v3_rbac_workflow(ansible_config):
     # TODO ... what happens with the other owners of a v3 namespace when the original owner changes their login?
 
     #import epdb; epdb.st()
+
+
+@pytest.mark.deployment_community
+def test_social_user_with_changed_login(ansible_config):
+
+    '''
+    [jtanner@jtw530 galaxy_user_analaysis]$ head -n1 data/user_errors.csv
+    galaxy_uid,galaxy_username,github_id,github_login,actual_github_id,actual_github_login
+    [jtanner@jtw530 galaxy_user_analaysis]$ grep sean-m-sullivan data/user_errors.csv
+    33901,Wilk42,30054029,sean-m-sullivan,30054029,sean-m-sullivan
+    '''
+
+    # a user logged into galaxy once with login "foo"
+    # the user then changed their name in github to "bar"
+    # the user then logged back into galaxy ...
+    #
+    #   1) social auth updates it's own data based on the github userid
+    #   2) galaxy's user still has the old login but is linked to the same social account
+
+    # What happens when someone claims the old username ..?
+    #
+    #   Once they login to galaxy they will get the "foo" login as their username?
+    #   the "foo" namespace will not belong to them
+    #   the "bar" namespace will not belong to them
+    #   do they own -any- namespaces?
+
+    ga = GithubAdminClient()
+    ga.delete_user(login='Wilk42')
+    ga.delete_user(login='sean-m-sullivan')
+    cleanup_social_user('Wilk42', ansible_config)
+    cleanup_social_user('sean-m-sullivan', ansible_config)
+
+    base_cfg = ansible_config('github_user_1')
+
+    old_login = 'Wilk42'
+    email = 'sean-m-sullivan@redhat.com'
+    user_a = ga.create_user(login=old_login, email=email)
+    user_a['username'] = old_login
+    user_a['token'] = None
+    user_a['url'] = base_cfg.get('url')
+    user_a['auth_url'] = base_cfg.get('auth_url')
+    user_a['github_url'] = base_cfg.get('github_url')
+    user_a['github_api_url'] = base_cfg.get('github_api_url')
+    l_ns_name = user_a['username']
+
+    # login once to make user
+    with SocialGithubClient(config=user_a) as client:
+
+        #import epdb; epdb.st()
+
+        a_resp = client.get('_ui/v1/me/')
+        ns_resp = client.get('_ui/v1/my-namespaces/')
+        ns_ds = ns_resp.json()
+        namespace_names_a = [x['name'] for x in ns_ds['data']]
+
+    # make a new login
+    new_login = 'sean-m-sullivan'
+
+    # change the user's login and try again
+    user_b = ga.modify_user(uid=user_a['id'], change=('login', new_login))
+    user_b['username'] = user_b['login']
+    user_b['token'] = None
+    user_b['url'] = base_cfg.get('url')
+    user_b['auth_url'] = base_cfg.get('auth_url')
+    user_b['github_url'] = base_cfg.get('github_url')
+    user_b['github_api_url'] = base_cfg.get('github_api_url')
+
+    # sean-m-sullivan never logs in again ...
+    '''
+    with SocialGithubClient(config=user_b) as client:
+        b_resp = client.get('_ui/v1/me/')
+    '''
+
+    # Let some new person claim the old login ...
+    user_c = ga.create_user(login=old_login, email='shadyperson@haxx.net')
+    user_c['username'] = old_login
+    user_c['token'] = None
+    user_c['url'] = base_cfg.get('url')
+    user_c['auth_url'] = base_cfg.get('auth_url')
+    user_c['github_url'] = base_cfg.get('github_url')
+    user_c['github_api_url'] = base_cfg.get('github_api_url')
+
+    with SocialGithubClient(config=user_c) as client:
+        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']]
+
+    import epdb; epdb.st()
diff --git a/galaxy_ng/tests/integration/utils/github.py b/galaxy_ng/tests/integration/utils/github.py
index b0153c2b7f..452c1af0bd 100644
--- a/galaxy_ng/tests/integration/utils/github.py
+++ b/galaxy_ng/tests/integration/utils/github.py
@@ -11,13 +11,19 @@ def list_users(self):
         rr = requests.get(url)
         return rr.json()
 
-    def create_user(self, uid=None, login=None, password=None):
+    def create_user(self, uid=None, login=None, email=None, password=None):
         url = self.baseurl + '/admin/users/add'
-        payload = {'id': uid, 'login': login, 'password': password}
+        payload = {'id': uid, 'login': login, 'email': email, 'password': password}
         rr  = requests.post(url, json=payload)
         return rr.json()
 
-    def modify_user(self, uid=None, login=None, password=None, change=None):
+    def delete_user(self, uid=None, login=None, email=None, password=None):
+        url = self.baseurl + '/admin/users/remove'
+        payload = {'id': uid, 'login': login}
+        rr  = requests.delete(url, json=payload)
+        return rr.json()
+
+    def modify_user(self, uid=None, login=None, email=None, password=None, change=None):
         if uid:
             url = self.baseurl + f'/admin/users/byid/{uid}'
         elif login:
diff --git a/profiles/community/github_mock/flaskapp.py b/profiles/community/github_mock/flaskapp.py
index bccc557a1c..4fa970f340 100644
--- a/profiles/community/github_mock/flaskapp.py
+++ b/profiles/community/github_mock/flaskapp.py
@@ -52,22 +52,26 @@
     'gh01': {
         'id': 1000,
         'login': 'gh01',
-        'password': 'redhat'
+        'password': 'redhat',
+        'email': 'gh01@gmail.com',
     },
     'gh02': {
         'id': 1001,
         'login': 'gh02',
-        'password': 'redhat'
+        'password': 'redhat',
+        'email': 'gh02@gmail.com',
     },
     'jctannerTEST': {
         'id': 1003,
         'login': 'jctannerTEST',
-        'password': 'redhat'
+        'password': 'redhat',
+        'email': 'jctannerTEST@gmail.com',
     },
     'geerlingguy': {
         'id': 1004,
         'login': 'geerlingguy',
-        'password': 'redhat'
+        'password': 'redhat',
+        'email': 'geerlingguy@gmail.com',
     }
 }
 
@@ -102,15 +106,16 @@ def create_tables():
         CREATE TABLE IF NOT EXISTS users (
             id INTEGER PRIMARY KEY AUTOINCREMENT,
             login TEXT NOT NULL UNIQUE,
+            email TEXT NOT NULL UNIQUE,
             password TEXT NOT NULL
         )
     ''')
     conn.commit()
 
     for uname, uinfo in USERS.items():
-        sql = "INSERT OR IGNORE INTO users (id, login, password) VALUES(?, ?, ?)"
+        sql = "INSERT OR IGNORE INTO users (id, login, email, password) VALUES(?, ?, ?, ?)"
         print(sql)
-        cursor.execute(sql, (uinfo['id'], uinfo['login'], uinfo['password'],))
+        cursor.execute(sql, (uinfo['id'], uinfo['login'], uinfo['email'], uinfo['password'],))
         conn.commit()
 
     cursor.execute('''
@@ -143,28 +148,30 @@ def create_tables():
 def get_user_by_id(userid):
     conn = sqlite3.connect(db_name)
     cursor = conn.cursor()
-    cursor.execute('SELECT id,login,password FROM users WHERE id = ?', (userid,))
+    cursor.execute('SELECT id,login,email,password FROM users WHERE id = ?', (userid,))
     row = cursor.fetchone()
     userid = row[0]
     login = row[1]
-    password = row[2]
+    email = row[2]
+    password = row[3]
     conn.close()
-    return {'id': userid, 'login': login, 'password': password}
+    return {'id': userid, 'login': login, 'email': email, 'password': password}
 
 
 def get_user_by_login(login):
     conn = sqlite3.connect(db_name)
     cursor = conn.cursor()
     print(f'FINDING USER BY LOGIN:{login}')
-    cursor.execute('SELECT id,login,password FROM users WHERE login = ?', (login,))
+    cursor.execute('SELECT id,login,email,password FROM users WHERE login = ?', (login,))
     row = cursor.fetchone()
     if row is None:
         return None
     userid = row[0]
     login = row[1]
-    password = row[2]
+    email = row[2]
+    password = row[3]
     conn.close()
-    return {'id': userid, 'login': login, 'password': password}
+    return {'id': userid, 'login': login, 'email': email, 'password': password}
 
 
 def get_session_by_id(sid):
@@ -380,7 +387,7 @@ def admin_user_list():
     conn = sqlite3.connect(db_name)
     cursor = conn.cursor()
 
-    sql = "SELECT id, login, password FROM users"
+    sql = "SELECT id, login, email, password FROM users"
     cursor.execute(sql)
     rows = cursor.fetchall()
 
@@ -388,8 +395,9 @@ def admin_user_list():
     for row in rows:
         userid = row[0]
         login = row[1]
-        password = row[2]
-        u = {'id': userid, 'login': login, 'password': password}
+        email = row[2]
+        password = row[3]
+        u = {'id': userid, 'login': login, 'email': email, 'password': password}
         users.append(u)
 
     conn.close()
@@ -410,23 +418,56 @@ def admin_add_user():
     password = ds.get('password', get_new_password())
     if password is None:
         password = get_new_password()
+    email = ds.get('email', login + '@github.com')
+    if email is None or not email:
+        email = login + '@github.com'
 
     conn = sqlite3.connect(db_name)
     cursor = conn.cursor()
 
-    sql = "INSERT OR IGNORE INTO users (id, login, password) VALUES(?, ?, ?)"
+    print(f'CREATING USER {login} with {password}')
+    sql = "INSERT OR IGNORE INTO users (id, login, email, password) VALUES(?, ?, ?, ?)"
     print(sql)
-    cursor.execute(sql, (userid, login, password,))
+    cursor.execute(sql, (userid, login, email, password,))
     conn.commit()
 
     conn.close()
-    return jsonify({'id': userid, 'login': login, 'password': password})
+    return jsonify({'id': userid, 'login': login, 'email': email, 'password': password})
+
+
+@app.route('/admin/users/remove', methods=['DELETE'])
+def admin_remove_user():
+    ds = request.json
+
+    userid = ds.get('id', get_new_uid())
+    if userid is None:
+        userid = get_new_uid()
+    login = ds.get('login', get_new_login())
+    if login is None:
+        login = get_new_login()
+
+    conn = sqlite3.connect(db_name)
+    cursor = conn.cursor()
+
+    if userid:
+        sql = 'DELETE FROM users WHERE id=?'
+        cursor.execute(sql, (userid,))
+        conn.commit()
+    if login:
+        sql = 'DELETE FROM users WHERE login=?'
+        cursor.execute(sql, (login,))
+        conn.commit()
+
+    conn.close()
+    return jsonify({'status': 'complete'})
 
 
 @app.route('/admin/users/byid/<int:userid>', methods=['POST'])
 @app.route('/admin/users/bylogin/<string:login>', methods=['POST'])
 def admin_modify_user(userid=None, login=None):
+
     print(request.data)
+
     ds = request.json
     new_userid = ds.get('id')
     new_login = ds.get('login')