We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Allow basic auth using email
**Description of the bug**
Authentication attempt using hsrest client with bad (typo etc) credentials.
```
---------------------------------------------------------------------------
HydroShareHTTPException                   Traceback (most recent call last)
<ipython-input-4-9e65448ed82e> in <module>
----> 1 hs.getResourceFile('bc14db4a50bd4d89bb750126a0a1ba79', file_name, destination=PROJECT_DIR)

/cvmfs/cybergis.illinois.edu/software/conda/cjw/pyrhessys-2024-02/lib/python3.7/site-packages/hs_restclient/__init__.py in getResourceFile(self, pid, filename, destination)
    901                 raise HydroShareNotFound((pid, filename))
    902             else:
--> 903                 raise HydroShareHTTPException(r)
    904 
    905         if destination is None:

HydroShareHTTPException: Received status 500 <!doctype html>
<html lang="en">
```
```
[06/Apr/2024 09:29:39] ERROR [django.request:224] Internal Server Error: /hsapi/resource/bc14db4a50bd4d89bb750126a0a1ba79/files/CoweetaSub18_10m_TEST.zip/
Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 497, in dispatch
    self.initial(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 414, in initial
    self.perform_authentication(request)
  File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 324, in perform_authentication
    request.user
  File "/usr/local/lib/python3.9/site-packages/rest_framework/request.py", line 227, in user
    self._authenticate()
  File "/usr/local/lib/python3.9/site-packages/rest_framework/request.py", line 380, in _authenticate
    user_auth_tuple = authenticator.authenticate(self)
  File "/hydroshare/hs_core/authentication.py", line 96, in authenticate
    user = User.objects.get(username=decoded_username)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 435, in get
    raise self.model.DoesNotExist(
django.contrib.auth.models.User.DoesNotExist: User matching query does not exist.
```

Add exception handling to [query for user](https://github.com/hydroshare/hydroshare/blob/2.13.6/hs_core/authentication.py#L96) to return 403 or other appropriate responses instead of 500.

**Additional information**
hs V 2.13.6


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `theme/utils.py`
Content:
```
1 import os
2 from uuid import uuid4
3 from datetime import date, timedelta
4 
5 
6 def _get_upload_path(folder_name, name, filename):
7     """
8     get upload path for pictures uploaded for UserProfile, GroupAccess, and Community models
9     to make sure the file name is unique
10     :param folder_name: folder name to put file in
11     :param name: instance name to uniquely identify a user profile, community, or group
12     :param filename: uploaded file name
13     :return: the upload path to be used for the ImageField in UserProfile, GroupAccess and Community models
14     """
15     file_base, file_ext = os.path.splitext(filename)
16     unique_id = uuid4().hex
17     return f'{folder_name}/{file_base}_{name}_{unique_id}{file_ext}'
18 
19 
20 def get_upload_path_community(instance, filename):
21     return _get_upload_path('community', instance.name, filename)
22 
23 
24 def get_upload_path_group(instance, filename):
25     return _get_upload_path('group', instance.group.name, filename)
26 
27 
28 def get_upload_path_userprofile(instance, filename):
29     return _get_upload_path('profile', instance.user.username, filename)
30 
31 
32 def get_quota_message(user):
33     """
34     get quota warning, grace period, or enforcement message to email users and display
35     when the user logins in and display on user profile page
36     :param user: The User instance
37     :return: quota message string
38     """
39     from theme.models import QuotaMessage
40     if not QuotaMessage.objects.exists():
41         QuotaMessage.objects.create()
42     qmsg = QuotaMessage.objects.first()
43     soft_limit = qmsg.soft_limit_percent
44     hard_limit = qmsg.hard_limit_percent
45     return_msg = ''
46     for uq in user.quotas.all():
47         percent = uq.used_value * 100.0 / uq.allocated_value
48         rounded_percent = round(percent, 2)
49         rounded_used_val = round(uq.used_value, 4)
50 
51         if percent >= hard_limit or (percent >= 100 and uq.remaining_grace_period == 0):
52             # return quota enforcement message
53             msg_template_str = '{}{}\n'.format(qmsg.enforce_content_prepend, qmsg.content)
54             return_msg += msg_template_str.format(used=rounded_used_val,
55                                                   unit=uq.unit,
56                                                   allocated=uq.allocated_value,
57                                                   zone=uq.zone,
58                                                   percent=rounded_percent)
59         elif percent >= 100 and uq.remaining_grace_period > 0:
60             # return quota grace period message
61             cut_off_date = date.today() + timedelta(days=uq.remaining_grace_period)
62             msg_template_str = '{}{}\n'.format(qmsg.grace_period_content_prepend, qmsg.content)
63             return_msg += msg_template_str.format(used=rounded_used_val,
64                                                   unit=uq.unit,
65                                                   allocated=uq.allocated_value,
66                                                   zone=uq.zone,
67                                                   percent=rounded_percent,
68                                                   cut_off_date=cut_off_date)
69         elif percent >= soft_limit:
70             # return quota warning message
71             msg_template_str = '{}{}\n'.format(qmsg.warning_content_prepend, qmsg.content)
72             return_msg += msg_template_str.format(used=rounded_used_val,
73                                                   unit=uq.unit,
74                                                   allocated=uq.allocated_value,
75                                                   zone=uq.zone,
76                                                   percent=rounded_percent)
77         else:
78             # return quota informational message
79             return_msg += qmsg.warning_content_prepend.format(allocated=uq.allocated_value,
80                                                               unit=uq.unit,
81                                                               used=rounded_used_val,
82                                                               zone=uq.zone,
83                                                               percent=rounded_percent)
84         return return_msg
85
```

Path: `hs_core/authentication.py`
Content:
```
1 import requests
2 import base64
3 from mozilla_django_oidc.auth import OIDCAuthenticationBackend
4 from hs_core.hydroshare import create_account
5 from django.urls import reverse, resolve
6 from django.conf import settings
7 from django.utils.http import urlencode
8 from django.contrib.auth.models import User
9 from rest_framework.authentication import BaseAuthentication
10 from keycloak.keycloak_openid import KeycloakOpenID
11 
12 
13 class HydroShareOIDCAuthenticationBackend(OIDCAuthenticationBackend):
14     def create_user(self, claims):
15         subject_areas = claims.get('subject_areas', '').split(";")
16         return create_account(
17             email=claims.get('email', ''),
18             username=claims.get('preferred_username', self.get_username(claims)),
19             first_name=claims.get('given_name', ''),
20             last_name=claims.get('family_name', ''),
21             superuser=False,
22             active=claims.get('email_verified', True),
23             organization=claims.get('organization', ''),
24             user_type=claims.get('user_type', ''),
25             country=claims.get('country', ''),
26             state=claims.get('state', ''),
27             subject_areas=subject_areas)
28 
29 
30 def build_oidc_url(request):
31     """Builds a link to OIDC service
32     To be called from within a view function
33 
34     Args:
35         request: current request being processed by the view
36 
37     Returns:
38         string: redirect URL for oidc service
39     """
40     view, args, kwargs = resolve(reverse('oidc_authentication_init'))
41     kwargs["request"] = request
42     redirect = view(*args, **kwargs)
43     return redirect.url
44 
45 
46 def provider_logout(request):
47     """ Create the user's OIDC logout URL."""
48     # User must confirm logout request with the default logout URL
49     # and is not redirected.
50     logout_url = settings.OIDC_OP_LOGOUT_ENDPOINT
51     redirect_url = settings.LOGOUT_REDIRECT_URL
52 
53     # If we have the oidc_id_token, we can automatically redirect
54     # the user back to the application.
55     oidc_id_token = request.session.get('oidc_id_token', None)
56     if oidc_id_token:
57         data = {
58             "id_token_hint": oidc_id_token,
59             "post_logout_redirect_uri": request.build_absolute_uri(
60                 location=redirect_url
61             )
62         }
63         res = requests.post(logout_url, data)
64         if not res.ok:
65             logout_url = logout_url + "?" + urlencode(data)
66         else:
67             logout_url = redirect_url
68     return logout_url
69 
70 
71 KEYCLOAK = KeycloakOpenID(
72     server_url=settings.OIDC_KEYCLOAK_URL,
73     client_id=settings.OIDC_RP_CLIENT_ID,
74     realm_name=settings.OIDC_KEYCLOAK_REALM,
75     client_secret_key=settings.OIDC_RP_CLIENT_SECRET,
76 )
77 
78 
79 class BasicOIDCAuthentication(BaseAuthentication):
80 
81     def authenticate(self, request):
82         auth = request.headers.get('Authorization')
83         if not auth or 'Basic' not in auth:
84             return None
85         _, value, *_ = request.headers.get('Authorization').split()
86 
87         decoded_username, decoded_password = (
88             base64.b64decode(value).decode("utf-8").split(":")
89         )
90         # authenticate against keycloak
91         try:
92             KEYCLOAK.token(decoded_username, decoded_password)
93         except Exception:
94             return None
95 
96         user = User.objects.get(username=decoded_username)
97         return (user, None)
98
```

Path: `theme/backends.py`
Content:
```
1 
2 
3 from django.contrib.auth import get_user_model
4 from django.contrib.auth.backends import ModelBackend
5 from django.contrib.auth.tokens import default_token_generator, PasswordResetTokenGenerator
6 from django.db.models import Q
7 from django.utils.http import base36_to_int, urlsafe_base64_decode
8 from six import text_type
9 
10 
11 class WithoutLoggedInDateTokenGenerator(PasswordResetTokenGenerator):
12     """
13     Removes last_login from the hash of the PasswordResetTokenGenerator.  Allows use of generated tokens in scenarios
14     where a user logging into their account after the token is generated should not expire the token
15     """
16 
17     def _make_hash_value(self, user, timestamp):
18         # override this method to remove last_login from hash
19         return (
20             text_type(user.pk) + user.password
21             + text_type(timestamp)
22         )
23 
24 
25 without_login_date_token_generator = WithoutLoggedInDateTokenGenerator()
26 User = get_user_model()
27 
28 
29 class CaseInsensitiveMezzanineBackend(ModelBackend):
30     """
31     Extends Django's ``ModelBackend`` to allow login via username,
32     email, or verification token.  Username and email use a case insensitive query.
33     Args are either ``username`` and ``password``, or ``uidb36``
34     and ``token``. In either case, ``is_active`` can also be given.
35     For login, is_active is not given, so that the login form can
36     raise a specific error for inactive users.
37     For password reset, True is given for is_active.
38     For signup verficiation, False is given for is_active.
39     """
40 
41     def authenticate(self, *args, **kwargs):
42         if kwargs:
43             username = kwargs.pop("username", None)
44             if username:
45                 username_or_email = Q(username__iexact=username) | Q(email__iexact=username)
46                 password = kwargs.pop("password", None)
47                 try:
48                     user = User.objects.get(username_or_email, **kwargs)
49                 except User.DoesNotExist:
50                     pass
51                 else:
52                     if user.check_password(password):
53                         return user
54             else:
55                 if 'uidb36' in kwargs:
56                     kwargs["id"] = base36_to_int(kwargs.pop("uidb36"))
57                 elif 'uidb64' in kwargs:
58                     # https://docs.djangoproject.com/en/3.2/releases/1.6/#django-contrib-auth-password-reset-uses-base-64-encoding-of-user-pk
59                     uidb64 = kwargs.pop("uidb64")
60                     kwargs["id"] = urlsafe_base64_decode(uidb64).decode()
61                 else:
62                     return
63                 token = kwargs.pop("token")
64                 try:
65                     user = User.objects.get(**kwargs)
66                 except User.DoesNotExist:
67                     pass
68                 else:
69                     if default_token_generator.check_token(user, token):
70                         return user
71                     elif without_login_date_token_generator.check_token(user, token):
72                         return user
73
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/hs_core/authentication.py b/hs_core/authentication.py
--- a/hs_core/authentication.py
+++ b/hs_core/authentication.py
@@ -5,9 +5,9 @@
 from django.urls import reverse, resolve
 from django.conf import settings
 from django.utils.http import urlencode
-from django.contrib.auth.models import User
 from rest_framework.authentication import BaseAuthentication
 from keycloak.keycloak_openid import KeycloakOpenID
+from theme.utils import get_user_from_username_or_email
 
 
 class HydroShareOIDCAuthenticationBackend(OIDCAuthenticationBackend):
@@ -92,6 +92,4 @@
             KEYCLOAK.token(decoded_username, decoded_password)
         except Exception:
             return None
-
-        user = User.objects.get(username=decoded_username)
-        return (user, None)
+        return get_user_from_username_or_email(decoded_username)
diff --git a/theme/backends.py b/theme/backends.py
--- a/theme/backends.py
+++ b/theme/backends.py
@@ -3,9 +3,9 @@
 from django.contrib.auth import get_user_model
 from django.contrib.auth.backends import ModelBackend
 from django.contrib.auth.tokens import default_token_generator, PasswordResetTokenGenerator
-from django.db.models import Q
 from django.utils.http import base36_to_int, urlsafe_base64_decode
 from six import text_type
+from theme.utils import get_user_from_username_or_email
 
 
 class WithoutLoggedInDateTokenGenerator(PasswordResetTokenGenerator):
@@ -40,17 +40,12 @@
 
     def authenticate(self, *args, **kwargs):
         if kwargs:
-            username = kwargs.pop("username", None)
-            if username:
-                username_or_email = Q(username__iexact=username) | Q(email__iexact=username)
+            username_or_email = kwargs.pop("username", None)
+            if username_or_email:
                 password = kwargs.pop("password", None)
-                try:
-                    user = User.objects.get(username_or_email, **kwargs)
-                except User.DoesNotExist:
-                    pass
-                else:
-                    if user.check_password(password):
-                        return user
+                user = get_user_from_username_or_email(username_or_email, **kwargs)
+                if user and user.check_password(password):
+                    return user
             else:
                 if 'uidb36' in kwargs:
                     kwargs["id"] = base36_to_int(kwargs.pop("uidb36"))
diff --git a/theme/utils.py b/theme/utils.py
--- a/theme/utils.py
+++ b/theme/utils.py
@@ -1,6 +1,7 @@
 import os
 from uuid import uuid4
 from datetime import date, timedelta
+from django.db.models import Q
 
 
 def _get_upload_path(folder_name, name, filename):
@@ -82,3 +83,19 @@
                                                               zone=uq.zone,
                                                               percent=rounded_percent)
         return return_msg
+
+
+def get_user_from_username_or_email(username_or_email, **kwargs):
+    """
+    Get a user object from a username or email address
+    :param username_or_email: username or email address
+    :return: a user object, or None if no user is found
+    """
+    from django.contrib.auth import get_user_model
+    User = get_user_model()
+    try:
+        return User.objects.get(
+            Q(username__iexact=username_or_email)
+            | Q(email__iexact=username_or_email), **kwargs)
+    except User.DoesNotExist:
+        return None

{"golden_diff": "diff --git a/hs_core/authentication.py b/hs_core/authentication.py\n--- a/hs_core/authentication.py\n+++ b/hs_core/authentication.py\n@@ -5,9 +5,9 @@\n from django.urls import reverse, resolve\n from django.conf import settings\n from django.utils.http import urlencode\n-from django.contrib.auth.models import User\n from rest_framework.authentication import BaseAuthentication\n from keycloak.keycloak_openid import KeycloakOpenID\n+from theme.utils import get_user_from_username_or_email\n \n \n class HydroShareOIDCAuthenticationBackend(OIDCAuthenticationBackend):\n@@ -92,6 +92,4 @@\n             KEYCLOAK.token(decoded_username, decoded_password)\n         except Exception:\n             return None\n-\n-        user = User.objects.get(username=decoded_username)\n-        return (user, None)\n+        return get_user_from_username_or_email(decoded_username)\ndiff --git a/theme/backends.py b/theme/backends.py\n--- a/theme/backends.py\n+++ b/theme/backends.py\n@@ -3,9 +3,9 @@\n from django.contrib.auth import get_user_model\n from django.contrib.auth.backends import ModelBackend\n from django.contrib.auth.tokens import default_token_generator, PasswordResetTokenGenerator\n-from django.db.models import Q\n from django.utils.http import base36_to_int, urlsafe_base64_decode\n from six import text_type\n+from theme.utils import get_user_from_username_or_email\n \n \n class WithoutLoggedInDateTokenGenerator(PasswordResetTokenGenerator):\n@@ -40,17 +40,12 @@\n \n     def authenticate(self, *args, **kwargs):\n         if kwargs:\n-            username = kwargs.pop(\"username\", None)\n-            if username:\n-                username_or_email = Q(username__iexact=username) | Q(email__iexact=username)\n+            username_or_email = kwargs.pop(\"username\", None)\n+            if username_or_email:\n                 password = kwargs.pop(\"password\", None)\n-                try:\n-                    user = User.objects.get(username_or_email, **kwargs)\n-                except User.DoesNotExist:\n-                    pass\n-                else:\n-                    if user.check_password(password):\n-                        return user\n+                user = get_user_from_username_or_email(username_or_email, **kwargs)\n+                if user and user.check_password(password):\n+                    return user\n             else:\n                 if 'uidb36' in kwargs:\n                     kwargs[\"id\"] = base36_to_int(kwargs.pop(\"uidb36\"))\ndiff --git a/theme/utils.py b/theme/utils.py\n--- a/theme/utils.py\n+++ b/theme/utils.py\n@@ -1,6 +1,7 @@\n import os\n from uuid import uuid4\n from datetime import date, timedelta\n+from django.db.models import Q\n \n \n def _get_upload_path(folder_name, name, filename):\n@@ -82,3 +83,19 @@\n                                                               zone=uq.zone,\n                                                               percent=rounded_percent)\n         return return_msg\n+\n+\n+def get_user_from_username_or_email(username_or_email, **kwargs):\n+    \"\"\"\n+    Get a user object from a username or email address\n+    :param username_or_email: username or email address\n+    :return: a user object, or None if no user is found\n+    \"\"\"\n+    from django.contrib.auth import get_user_model\n+    User = get_user_model()\n+    try:\n+        return User.objects.get(\n+            Q(username__iexact=username_or_email)\n+            | Q(email__iexact=username_or_email), **kwargs)\n+    except User.DoesNotExist:\n+        return None\n", "issue": "Allow basic auth using email\n**Description of the bug**\r\nAuthentication attempt using hsrest client with bad (typo etc) credentials.\r\n```\r\n---------------------------------------------------------------------------\r\nHydroShareHTTPException                   Traceback (most recent call last)\r\n<ipython-input-4-9e65448ed82e> in <module>\r\n----> 1 hs.getResourceFile('bc14db4a50bd4d89bb750126a0a1ba79', file_name, destination=PROJECT_DIR)\r\n\r\n/cvmfs/cybergis.illinois.edu/software/conda/cjw/pyrhessys-2024-02/lib/python3.7/site-packages/hs_restclient/__init__.py in getResourceFile(self, pid, filename, destination)\r\n    901                 raise HydroShareNotFound((pid, filename))\r\n    902             else:\r\n--> 903                 raise HydroShareHTTPException(r)\r\n    904 \r\n    905         if destination is None:\r\n\r\nHydroShareHTTPException: Received status 500 <!doctype html>\r\n<html lang=\"en\">\r\n```\r\n```\r\n[06/Apr/2024 09:29:39] ERROR [django.request:224] Internal Server Error: /hsapi/resource/bc14db4a50bd4d89bb750126a0a1ba79/files/CoweetaSub18_10m_TEST.zip/\r\nTraceback (most recent call last):\r\n  File \"/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py\", line 47, in inner\r\n    response = get_response(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py\", line 181, in _get_response\r\n    response = wrapped_callback(request, *callback_args, **callback_kwargs)\r\n  File \"/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py\", line 54, in wrapped_view\r\n    return view_func(*args, **kwargs)\r\n  File \"/usr/local/lib/python3.9/site-packages/django/views/generic/base.py\", line 70, in view\r\n    return self.dispatch(request, *args, **kwargs)\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 509, in dispatch\r\n    response = self.handle_exception(exc)\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 469, in handle_exception\r\n    self.raise_uncaught_exception(exc)\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 480, in raise_uncaught_exception\r\n    raise exc\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 497, in dispatch\r\n    self.initial(request, *args, **kwargs)\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 414, in initial\r\n    self.perform_authentication(request)\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/views.py\", line 324, in perform_authentication\r\n    request.user\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/request.py\", line 227, in user\r\n    self._authenticate()\r\n  File \"/usr/local/lib/python3.9/site-packages/rest_framework/request.py\", line 380, in _authenticate\r\n    user_auth_tuple = authenticator.authenticate(self)\r\n  File \"/hydroshare/hs_core/authentication.py\", line 96, in authenticate\r\n    user = User.objects.get(username=decoded_username)\r\n  File \"/usr/local/lib/python3.9/site-packages/django/db/models/manager.py\", line 85, in manager_method\r\n    return getattr(self.get_queryset(), name)(*args, **kwargs)\r\n  File \"/usr/local/lib/python3.9/site-packages/django/db/models/query.py\", line 435, in get\r\n    raise self.model.DoesNotExist(\r\ndjango.contrib.auth.models.User.DoesNotExist: User matching query does not exist.\r\n```\r\n\r\nAdd exception handling to [query for user](https://github.com/hydroshare/hydroshare/blob/2.13.6/hs_core/authentication.py#L96) to return 403 or other appropriate responses instead of 500.\r\n\r\n**Additional information**\r\nhs V 2.13.6\r\n\n", "before_files": [{"content": "import os\nfrom uuid import uuid4\nfrom datetime import date, timedelta\n\n\ndef _get_upload_path(folder_name, name, filename):\n    \"\"\"\n    get upload path for pictures uploaded for UserProfile, GroupAccess, and Community models\n    to make sure the file name is unique\n    :param folder_name: folder name to put file in\n    :param name: instance name to uniquely identify a user profile, community, or group\n    :param filename: uploaded file name\n    :return: the upload path to be used for the ImageField in UserProfile, GroupAccess and Community models\n    \"\"\"\n    file_base, file_ext = os.path.splitext(filename)\n    unique_id = uuid4().hex\n    return f'{folder_name}/{file_base}_{name}_{unique_id}{file_ext}'\n\n\ndef get_upload_path_community(instance, filename):\n    return _get_upload_path('community', instance.name, filename)\n\n\ndef get_upload_path_group(instance, filename):\n    return _get_upload_path('group', instance.group.name, filename)\n\n\ndef get_upload_path_userprofile(instance, filename):\n    return _get_upload_path('profile', instance.user.username, filename)\n\n\ndef get_quota_message(user):\n    \"\"\"\n    get quota warning, grace period, or enforcement message to email users and display\n    when the user logins in and display on user profile page\n    :param user: The User instance\n    :return: quota message string\n    \"\"\"\n    from theme.models import QuotaMessage\n    if not QuotaMessage.objects.exists():\n        QuotaMessage.objects.create()\n    qmsg = QuotaMessage.objects.first()\n    soft_limit = qmsg.soft_limit_percent\n    hard_limit = qmsg.hard_limit_percent\n    return_msg = ''\n    for uq in user.quotas.all():\n        percent = uq.used_value * 100.0 / uq.allocated_value\n        rounded_percent = round(percent, 2)\n        rounded_used_val = round(uq.used_value, 4)\n\n        if percent >= hard_limit or (percent >= 100 and uq.remaining_grace_period == 0):\n            # return quota enforcement message\n            msg_template_str = '{}{}\\n'.format(qmsg.enforce_content_prepend, qmsg.content)\n            return_msg += msg_template_str.format(used=rounded_used_val,\n                                                  unit=uq.unit,\n                                                  allocated=uq.allocated_value,\n                                                  zone=uq.zone,\n                                                  percent=rounded_percent)\n        elif percent >= 100 and uq.remaining_grace_period > 0:\n            # return quota grace period message\n            cut_off_date = date.today() + timedelta(days=uq.remaining_grace_period)\n            msg_template_str = '{}{}\\n'.format(qmsg.grace_period_content_prepend, qmsg.content)\n            return_msg += msg_template_str.format(used=rounded_used_val,\n                                                  unit=uq.unit,\n                                                  allocated=uq.allocated_value,\n                                                  zone=uq.zone,\n                                                  percent=rounded_percent,\n                                                  cut_off_date=cut_off_date)\n        elif percent >= soft_limit:\n            # return quota warning message\n            msg_template_str = '{}{}\\n'.format(qmsg.warning_content_prepend, qmsg.content)\n            return_msg += msg_template_str.format(used=rounded_used_val,\n                                                  unit=uq.unit,\n                                                  allocated=uq.allocated_value,\n                                                  zone=uq.zone,\n                                                  percent=rounded_percent)\n        else:\n            # return quota informational message\n            return_msg += qmsg.warning_content_prepend.format(allocated=uq.allocated_value,\n                                                              unit=uq.unit,\n                                                              used=rounded_used_val,\n                                                              zone=uq.zone,\n                                                              percent=rounded_percent)\n        return return_msg\n", "path": "theme/utils.py"}, {"content": "import requests\nimport base64\nfrom mozilla_django_oidc.auth import OIDCAuthenticationBackend\nfrom hs_core.hydroshare import create_account\nfrom django.urls import reverse, resolve\nfrom django.conf import settings\nfrom django.utils.http import urlencode\nfrom django.contrib.auth.models import User\nfrom rest_framework.authentication import BaseAuthentication\nfrom keycloak.keycloak_openid import KeycloakOpenID\n\n\nclass HydroShareOIDCAuthenticationBackend(OIDCAuthenticationBackend):\n    def create_user(self, claims):\n        subject_areas = claims.get('subject_areas', '').split(\";\")\n        return create_account(\n            email=claims.get('email', ''),\n            username=claims.get('preferred_username', self.get_username(claims)),\n            first_name=claims.get('given_name', ''),\n            last_name=claims.get('family_name', ''),\n            superuser=False,\n            active=claims.get('email_verified', True),\n            organization=claims.get('organization', ''),\n            user_type=claims.get('user_type', ''),\n            country=claims.get('country', ''),\n            state=claims.get('state', ''),\n            subject_areas=subject_areas)\n\n\ndef build_oidc_url(request):\n    \"\"\"Builds a link to OIDC service\n    To be called from within a view function\n\n    Args:\n        request: current request being processed by the view\n\n    Returns:\n        string: redirect URL for oidc service\n    \"\"\"\n    view, args, kwargs = resolve(reverse('oidc_authentication_init'))\n    kwargs[\"request\"] = request\n    redirect = view(*args, **kwargs)\n    return redirect.url\n\n\ndef provider_logout(request):\n    \"\"\" Create the user's OIDC logout URL.\"\"\"\n    # User must confirm logout request with the default logout URL\n    # and is not redirected.\n    logout_url = settings.OIDC_OP_LOGOUT_ENDPOINT\n    redirect_url = settings.LOGOUT_REDIRECT_URL\n\n    # If we have the oidc_id_token, we can automatically redirect\n    # the user back to the application.\n    oidc_id_token = request.session.get('oidc_id_token', None)\n    if oidc_id_token:\n        data = {\n            \"id_token_hint\": oidc_id_token,\n            \"post_logout_redirect_uri\": request.build_absolute_uri(\n                location=redirect_url\n            )\n        }\n        res = requests.post(logout_url, data)\n        if not res.ok:\n            logout_url = logout_url + \"?\" + urlencode(data)\n        else:\n            logout_url = redirect_url\n    return logout_url\n\n\nKEYCLOAK = KeycloakOpenID(\n    server_url=settings.OIDC_KEYCLOAK_URL,\n    client_id=settings.OIDC_RP_CLIENT_ID,\n    realm_name=settings.OIDC_KEYCLOAK_REALM,\n    client_secret_key=settings.OIDC_RP_CLIENT_SECRET,\n)\n\n\nclass BasicOIDCAuthentication(BaseAuthentication):\n\n    def authenticate(self, request):\n        auth = request.headers.get('Authorization')\n        if not auth or 'Basic' not in auth:\n            return None\n        _, value, *_ = request.headers.get('Authorization').split()\n\n        decoded_username, decoded_password = (\n            base64.b64decode(value).decode(\"utf-8\").split(\":\")\n        )\n        # authenticate against keycloak\n        try:\n            KEYCLOAK.token(decoded_username, decoded_password)\n        except Exception:\n            return None\n\n        user = User.objects.get(username=decoded_username)\n        return (user, None)\n", "path": "hs_core/authentication.py"}, {"content": "\n\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.backends import ModelBackend\nfrom django.contrib.auth.tokens import default_token_generator, PasswordResetTokenGenerator\nfrom django.db.models import Q\nfrom django.utils.http import base36_to_int, urlsafe_base64_decode\nfrom six import text_type\n\n\nclass WithoutLoggedInDateTokenGenerator(PasswordResetTokenGenerator):\n    \"\"\"\n    Removes last_login from the hash of the PasswordResetTokenGenerator.  Allows use of generated tokens in scenarios\n    where a user logging into their account after the token is generated should not expire the token\n    \"\"\"\n\n    def _make_hash_value(self, user, timestamp):\n        # override this method to remove last_login from hash\n        return (\n            text_type(user.pk) + user.password\n            + text_type(timestamp)\n        )\n\n\nwithout_login_date_token_generator = WithoutLoggedInDateTokenGenerator()\nUser = get_user_model()\n\n\nclass CaseInsensitiveMezzanineBackend(ModelBackend):\n    \"\"\"\n    Extends Django's ``ModelBackend`` to allow login via username,\n    email, or verification token.  Username and email use a case insensitive query.\n    Args are either ``username`` and ``password``, or ``uidb36``\n    and ``token``. In either case, ``is_active`` can also be given.\n    For login, is_active is not given, so that the login form can\n    raise a specific error for inactive users.\n    For password reset, True is given for is_active.\n    For signup verficiation, False is given for is_active.\n    \"\"\"\n\n    def authenticate(self, *args, **kwargs):\n        if kwargs:\n            username = kwargs.pop(\"username\", None)\n            if username:\n                username_or_email = Q(username__iexact=username) | Q(email__iexact=username)\n                password = kwargs.pop(\"password\", None)\n                try:\n                    user = User.objects.get(username_or_email, **kwargs)\n                except User.DoesNotExist:\n                    pass\n                else:\n                    if user.check_password(password):\n                        return user\n            else:\n                if 'uidb36' in kwargs:\n                    kwargs[\"id\"] = base36_to_int(kwargs.pop(\"uidb36\"))\n                elif 'uidb64' in kwargs:\n                    # https://docs.djangoproject.com/en/3.2/releases/1.6/#django-contrib-auth-password-reset-uses-base-64-encoding-of-user-pk\n                    uidb64 = kwargs.pop(\"uidb64\")\n                    kwargs[\"id\"] = urlsafe_base64_decode(uidb64).decode()\n                else:\n                    return\n                token = kwargs.pop(\"token\")\n                try:\n                    user = User.objects.get(**kwargs)\n                except User.DoesNotExist:\n                    pass\n                else:\n                    if default_token_generator.check_token(user, token):\n                        return user\n                    elif without_login_date_token_generator.check_token(user, token):\n                        return user\n", "path": "theme/backends.py"}], "after_files": [{"content": "import os\nfrom uuid import uuid4\nfrom datetime import date, timedelta\nfrom django.db.models import Q\n\n\ndef _get_upload_path(folder_name, name, filename):\n    \"\"\"\n    get upload path for pictures uploaded for UserProfile, GroupAccess, and Community models\n    to make sure the file name is unique\n    :param folder_name: folder name to put file in\n    :param name: instance name to uniquely identify a user profile, community, or group\n    :param filename: uploaded file name\n    :return: the upload path to be used for the ImageField in UserProfile, GroupAccess and Community models\n    \"\"\"\n    file_base, file_ext = os.path.splitext(filename)\n    unique_id = uuid4().hex\n    return f'{folder_name}/{file_base}_{name}_{unique_id}{file_ext}'\n\n\ndef get_upload_path_community(instance, filename):\n    return _get_upload_path('community', instance.name, filename)\n\n\ndef get_upload_path_group(instance, filename):\n    return _get_upload_path('group', instance.group.name, filename)\n\n\ndef get_upload_path_userprofile(instance, filename):\n    return _get_upload_path('profile', instance.user.username, filename)\n\n\ndef get_quota_message(user):\n    \"\"\"\n    get quota warning, grace period, or enforcement message to email users and display\n    when the user logins in and display on user profile page\n    :param user: The User instance\n    :return: quota message string\n    \"\"\"\n    from theme.models import QuotaMessage\n    if not QuotaMessage.objects.exists():\n        QuotaMessage.objects.create()\n    qmsg = QuotaMessage.objects.first()\n    soft_limit = qmsg.soft_limit_percent\n    hard_limit = qmsg.hard_limit_percent\n    return_msg = ''\n    for uq in user.quotas.all():\n        percent = uq.used_value * 100.0 / uq.allocated_value\n        rounded_percent = round(percent, 2)\n        rounded_used_val = round(uq.used_value, 4)\n\n        if percent >= hard_limit or (percent >= 100 and uq.remaining_grace_period == 0):\n            # return quota enforcement message\n            msg_template_str = '{}{}\\n'.format(qmsg.enforce_content_prepend, qmsg.content)\n            return_msg += msg_template_str.format(used=rounded_used_val,\n                                                  unit=uq.unit,\n                                                  allocated=uq.allocated_value,\n                                                  zone=uq.zone,\n                                                  percent=rounded_percent)\n        elif percent >= 100 and uq.remaining_grace_period > 0:\n            # return quota grace period message\n            cut_off_date = date.today() + timedelta(days=uq.remaining_grace_period)\n            msg_template_str = '{}{}\\n'.format(qmsg.grace_period_content_prepend, qmsg.content)\n            return_msg += msg_template_str.format(used=rounded_used_val,\n                                                  unit=uq.unit,\n                                                  allocated=uq.allocated_value,\n                                                  zone=uq.zone,\n                                                  percent=rounded_percent,\n                                                  cut_off_date=cut_off_date)\n        elif percent >= soft_limit:\n            # return quota warning message\n            msg_template_str = '{}{}\\n'.format(qmsg.warning_content_prepend, qmsg.content)\n            return_msg += msg_template_str.format(used=rounded_used_val,\n                                                  unit=uq.unit,\n                                                  allocated=uq.allocated_value,\n                                                  zone=uq.zone,\n                                                  percent=rounded_percent)\n        else:\n            # return quota informational message\n            return_msg += qmsg.warning_content_prepend.format(allocated=uq.allocated_value,\n                                                              unit=uq.unit,\n                                                              used=rounded_used_val,\n                                                              zone=uq.zone,\n                                                              percent=rounded_percent)\n        return return_msg\n\n\ndef get_user_from_username_or_email(username_or_email, **kwargs):\n    \"\"\"\n    Get a user object from a username or email address\n    :param username_or_email: username or email address\n    :return: a user object, or None if no user is found\n    \"\"\"\n    from django.contrib.auth import get_user_model\n    User = get_user_model()\n    try:\n        return User.objects.get(\n            Q(username__iexact=username_or_email)\n            | Q(email__iexact=username_or_email), **kwargs)\n    except User.DoesNotExist:\n        return None\n", "path": "theme/utils.py"}, {"content": "import requests\nimport base64\nfrom mozilla_django_oidc.auth import OIDCAuthenticationBackend\nfrom hs_core.hydroshare import create_account\nfrom django.urls import reverse, resolve\nfrom django.conf import settings\nfrom django.utils.http import urlencode\nfrom rest_framework.authentication import BaseAuthentication\nfrom keycloak.keycloak_openid import KeycloakOpenID\nfrom theme.utils import get_user_from_username_or_email\n\n\nclass HydroShareOIDCAuthenticationBackend(OIDCAuthenticationBackend):\n    def create_user(self, claims):\n        subject_areas = claims.get('subject_areas', '').split(\";\")\n        return create_account(\n            email=claims.get('email', ''),\n            username=claims.get('preferred_username', self.get_username(claims)),\n            first_name=claims.get('given_name', ''),\n            last_name=claims.get('family_name', ''),\n            superuser=False,\n            active=claims.get('email_verified', True),\n            organization=claims.get('organization', ''),\n            user_type=claims.get('user_type', ''),\n            country=claims.get('country', ''),\n            state=claims.get('state', ''),\n            subject_areas=subject_areas)\n\n\ndef build_oidc_url(request):\n    \"\"\"Builds a link to OIDC service\n    To be called from within a view function\n\n    Args:\n        request: current request being processed by the view\n\n    Returns:\n        string: redirect URL for oidc service\n    \"\"\"\n    view, args, kwargs = resolve(reverse('oidc_authentication_init'))\n    kwargs[\"request\"] = request\n    redirect = view(*args, **kwargs)\n    return redirect.url\n\n\ndef provider_logout(request):\n    \"\"\" Create the user's OIDC logout URL.\"\"\"\n    # User must confirm logout request with the default logout URL\n    # and is not redirected.\n    logout_url = settings.OIDC_OP_LOGOUT_ENDPOINT\n    redirect_url = settings.LOGOUT_REDIRECT_URL\n\n    # If we have the oidc_id_token, we can automatically redirect\n    # the user back to the application.\n    oidc_id_token = request.session.get('oidc_id_token', None)\n    if oidc_id_token:\n        data = {\n            \"id_token_hint\": oidc_id_token,\n            \"post_logout_redirect_uri\": request.build_absolute_uri(\n                location=redirect_url\n            )\n        }\n        res = requests.post(logout_url, data)\n        if not res.ok:\n            logout_url = logout_url + \"?\" + urlencode(data)\n        else:\n            logout_url = redirect_url\n    return logout_url\n\n\nKEYCLOAK = KeycloakOpenID(\n    server_url=settings.OIDC_KEYCLOAK_URL,\n    client_id=settings.OIDC_RP_CLIENT_ID,\n    realm_name=settings.OIDC_KEYCLOAK_REALM,\n    client_secret_key=settings.OIDC_RP_CLIENT_SECRET,\n)\n\n\nclass BasicOIDCAuthentication(BaseAuthentication):\n\n    def authenticate(self, request):\n        auth = request.headers.get('Authorization')\n        if not auth or 'Basic' not in auth:\n            return None\n        _, value, *_ = request.headers.get('Authorization').split()\n\n        decoded_username, decoded_password = (\n            base64.b64decode(value).decode(\"utf-8\").split(\":\")\n        )\n        # authenticate against keycloak\n        try:\n            KEYCLOAK.token(decoded_username, decoded_password)\n        except Exception:\n            return None\n        return get_user_from_username_or_email(decoded_username)\n", "path": "hs_core/authentication.py"}, {"content": "\n\nfrom django.contrib.auth import get_user_model\nfrom django.contrib.auth.backends import ModelBackend\nfrom django.contrib.auth.tokens import default_token_generator, PasswordResetTokenGenerator\nfrom django.utils.http import base36_to_int, urlsafe_base64_decode\nfrom six import text_type\nfrom theme.utils import get_user_from_username_or_email\n\n\nclass WithoutLoggedInDateTokenGenerator(PasswordResetTokenGenerator):\n    \"\"\"\n    Removes last_login from the hash of the PasswordResetTokenGenerator.  Allows use of generated tokens in scenarios\n    where a user logging into their account after the token is generated should not expire the token\n    \"\"\"\n\n    def _make_hash_value(self, user, timestamp):\n        # override this method to remove last_login from hash\n        return (\n            text_type(user.pk) + user.password\n            + text_type(timestamp)\n        )\n\n\nwithout_login_date_token_generator = WithoutLoggedInDateTokenGenerator()\nUser = get_user_model()\n\n\nclass CaseInsensitiveMezzanineBackend(ModelBackend):\n    \"\"\"\n    Extends Django's ``ModelBackend`` to allow login via username,\n    email, or verification token.  Username and email use a case insensitive query.\n    Args are either ``username`` and ``password``, or ``uidb36``\n    and ``token``. In either case, ``is_active`` can also be given.\n    For login, is_active is not given, so that the login form can\n    raise a specific error for inactive users.\n    For password reset, True is given for is_active.\n    For signup verficiation, False is given for is_active.\n    \"\"\"\n\n    def authenticate(self, *args, **kwargs):\n        if kwargs:\n            username_or_email = kwargs.pop(\"username\", None)\n            if username_or_email:\n                password = kwargs.pop(\"password\", None)\n                user = get_user_from_username_or_email(username_or_email, **kwargs)\n                if user and user.check_password(password):\n                    return user\n            else:\n                if 'uidb36' in kwargs:\n                    kwargs[\"id\"] = base36_to_int(kwargs.pop(\"uidb36\"))\n                elif 'uidb64' in kwargs:\n                    # https://docs.djangoproject.com/en/3.2/releases/1.6/#django-contrib-auth-password-reset-uses-base-64-encoding-of-user-pk\n                    uidb64 = kwargs.pop(\"uidb64\")\n                    kwargs[\"id\"] = urlsafe_base64_decode(uidb64).decode()\n                else:\n                    return\n                token = kwargs.pop(\"token\")\n                try:\n                    user = User.objects.get(**kwargs)\n                except User.DoesNotExist:\n                    pass\n                else:\n                    if default_token_generator.check_token(user, token):\n                        return user\n                    elif without_login_date_token_generator.check_token(user, token):\n                        return user\n", "path": "theme/backends.py"}]}