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 :""" - 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 :""" + 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 :""" + 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/', methods=['POST']) @app.route('/admin/users/bylogin/', 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')