diff --git a/src/django_gauth/_checks.py b/src/django_gauth/_checks.py new file mode 100644 index 0000000..51eab25 --- /dev/null +++ b/src/django_gauth/_checks.py @@ -0,0 +1,86 @@ +# myapp/checks.py +import logging +from enum import Enum +from typing import Any, Callable + +from django.conf import settings # pylint: disable=E0401 +from django.core.checks import Error # pylint: disable=E0401 + +logger = logging.getLogger(__name__) + +__app_label__ = "django_gauth" + + +class ErrorCodes(Enum): + """Error Codes for app checks""" + + E001 = ("MISSING_REQUIRED_SETTINGS", "Please define the required project settings") + E002 = ( + "MISSING_REQUIRED_MIDDLEWARE", + "Please include required middleware in settings", + ) + E003 = ( + "MISSING_REQUIRED_GOOGLE_CREDENTIALS", + "Please include required google oauth2 web client \ + credentials ( GOOGLE_CLIENT_ID & GOOGLE_CLIENT_SECRET )", + ) + E004 = ( + "INVALID_GAUTH_SCOPE", + "Please set valid oauth2 SCOPE" + ) + +formulate_check_id: Callable = lambda code: f"{__app_label__}.{code}" + + +def check_project_settings( + app_configs: object, **kwargs: Any # pylint: disable=W0613 +) -> list: + errors = [] + if not hasattr(settings, "SECRET_KEY"): + errors.append( + Error( + "SECRET_KEY setting not defined. Required for app:{__app_label__} to work.", + hint="Define SECRET_KEY in your project settings.py", + id=formulate_check_id(ErrorCodes.E001.name), + ) + ) + if not hasattr(settings, "GOOGLE_CLIENT_ID"): + errors.append( + Error( + "GOOGLE_CLIENT_ID is not defined in settings."\ + + f"Required for app:{__app_label__} to work.", + hint="Define GOOGLE_CLIENT_ID in your project settings.py", + id=formulate_check_id(ErrorCodes.E003.name), + ) + ) + if not hasattr(settings, "GOOGLE_CLIENT_SECRET"): + errors.append( + Error( + "GOOGLE_CLIENT_SECRET is not defined in settings."\ + + f"Required for app:{__app_label__} to work.", + hint="Define GOOGLE_CLIENT_SECRET in your project settings.py", + id=formulate_check_id(ErrorCodes.E003.name), + ) + ) + + return errors + + +def check_project_middlewares( + app_configs: object, **kwargs: Any # pylint: disable=W0613 +) -> list: + errors = [] + + session_middleware_path = "django.contrib.sessions.middleware.SessionMiddleware" + + if not session_middleware_path in settings.MIDDLEWARE: + errors.append( + Error( + "Django SessionMiddleware is not included in settings. "\ + + "Required for app:{__app_label__} to work.", + hint=f"Define {session_middleware_path} in your "\ + + "project`s MIDDLEWARE variable in settings.py", + id=formulate_check_id(ErrorCodes.E002.name), + ) + ) + return errors diff --git a/src/django_gauth/admin.py b/src/django_gauth/admin.py index f5f91df..30bae19 100644 --- a/src/django_gauth/admin.py +++ b/src/django_gauth/admin.py @@ -1,18 +1,19 @@ import pprint -from django.contrib import admin # pylint: disable=E0401 + +from django.contrib import admin # pylint: disable=E0401 from django.contrib.sessions.models import Session # pylint: disable=E0401 + # pylint: disable=R0903 class SessionAdmin(admin.ModelAdmin): """ Session information on admin panel """ + list_display = ["session_key", "_session_data", "expire_date"] readonly_fields = ["_session_data"] - exclude = [ - "session_data" - ] # This line ensures that encoded session in not shown . - # Comment this if you want to see the encoded session data as well . + exclude = ["session_data"] # This line ensures that encoded session in not shown . + # Comment this if you want to see the encoded session data as well . def _session_data(self, obj): diff --git a/src/django_gauth/apps.py b/src/django_gauth/apps.py index 4115cde..064d183 100644 --- a/src/django_gauth/apps.py +++ b/src/django_gauth/apps.py @@ -1,9 +1,124 @@ -from django.apps import AppConfig # pylint: disable=E0401 +import warnings +from typing import Any + +from django.apps import AppConfig # pylint: disable=E0401 + +# pylint: disable=E0602 +from django.conf import settings # pylint: disable=E0401 +from django.core.checks import Info # pylint: disable=E0401 +from django.core.checks import register # pylint: disable=E0401 +from django.core.checks import Tags as DjangoTags # pylint: disable=E0401 +from django.core.checks import Warning as SysCheckWarning # pylint: disable=E0401 + +from django_gauth import defaults +from django_gauth._checks import ( + check_project_middlewares, + check_project_settings, + formulate_check_id, + ErrorCodes +) + +warnings.simplefilter("default") + + +# pylint: disable=R0903 +class Tags(DjangoTags): + """Extending with Custom Tags + + NOTE : Do this if none of the existing tags work for you: + https://docs.djangoproject.com/en/3.1/ref/checks/#builtin-tags + """ + + django_gauth_compatibility = "django_gauth_compatibility" + # pylint: disable=R0903 class DjangoGauthConfig(AppConfig): """ App Configurator @ django_gauth """ + default_auto_field = "django.db.models.BigAutoField" name = "django_gauth" + + def ready(self) -> None: + register(Tags.compatibility)(check_project_middlewares) + register(Tags.django_gauth_compatibility)(check_project_settings) + + +@register(Tags.django_gauth_compatibility) +def set_defaults( + app_configs: object, **kwargs: Any # pylint: disable=W0613 +) -> list: + errors = [] + if not hasattr(settings, "SCOPE"): + setattr(settings, "SCOPE", []) + errors.append( + SysCheckWarning( + "SCOPE setting is not defined. Defaulting to `[]`."\ + + "It may affect the normal flow of oauth and might not run as expected."\ + + "Please rectify ASAP.", + hint=( + "See https://masterpiece93.github.io/django-gauth/settings/ "\ + + "for more information." + ), + id=formulate_check_id(ErrorCodes.E004.name), + ) + ) + + if not hasattr(settings, "GOOGLE_AUTH_FINAL_REDIRECT_URL"): + setattr( + settings, + "GOOGLE_AUTH_FINAL_REDIRECT_URL", + defaults.GOOGLE_AUTH_FINAL_REDIRECT_URL, + ) + _msg = "GOOGLE_AUTH_FINAL_REDIRECT_URL settings is not defined."\ + +f"Defaulting to `{defaults.GOOGLE_AUTH_FINAL_REDIRECT_URL}`" + warnings.warn(_msg) + else: + if not settings.GOOGLE_AUTH_FINAL_REDIRECT_URL: + _msg = "GOOGLE_AUTH_FINAL_REDIRECT_URL setting is set to"\ + + f"`{settings.GOOGLE_AUTH_FINAL_REDIRECT_URL}` which is logically incorrect." + info = Info(_msg) + errors.append(info) + if not hasattr(settings, "CREDENTIALS_SESSION_KEY_NAME"): + setattr( + settings, + "CREDENTIALS_SESSION_KEY_NAME", + defaults.CREDENTIALS_SESSION_KEY_NAME, + ) + _msg = "CREDENTIALS_SESSION_KEY_NAME settings is not defined."\ + + "Defaulting to `{defaults.CREDENTIALS_SESSION_KEY_NAME}`" + warnings.warn(_msg) + else: + if not settings.CREDENTIALS_SESSION_KEY_NAME: + _msg = "CREDENTIALS_SESSION_KEY_NAME setting is set to"\ + + "`{settings.CREDENTIALS_SESSION_KEY_NAME}` which is logically incorrect." + info = Info(_msg) + errors.append(info) + if not hasattr(settings, "STATE_KEY_NAME"): + setattr(settings, "STATE_KEY_NAME", defaults.STATE_KEY_NAME) + _msg = "STATE_KEY_NAME settings is not defined."\ + + "Defaulting to `{defaults.STATE_KEY_NAME}`" + warnings.warn(_msg) + else: + if not settings.STATE_KEY_NAME: + _msg = f"STATE_KEY_NAME setting is set to `{settings.STATE_KEY_NAME}`"\ + + "which is logically incorrect." + info = Info(_msg) + errors.append(info) + + if not hasattr(settings, "FINAL_REDIRECT_KEY_NAME"): + setattr( + settings, "FINAL_REDIRECT_KEY_NAME", defaults.FINAL_REDIRECT_KEY_NAME + ) + _msg = "FINAL_REDIRECT_KEY_NAME settings is not defined."\ + + "Defaulting to `{defaults.FINAL_REDIRECT_KEY_NAME}`" + warnings.warn(_msg) + else: + if not settings.FINAL_REDIRECT_KEY_NAME: + _msg = "FINAL_REDIRECT_KEY_NAME setting is set to"\ + + "`{settings.FINAL_REDIRECT_KEY_NAME}` which is logically incorrect." + info = Info(_msg) + errors.append(info) + return errors diff --git a/src/django_gauth/urls.py b/src/django_gauth/urls.py index 6a4faa7..f500214 100644 --- a/src/django_gauth/urls.py +++ b/src/django_gauth/urls.py @@ -1,6 +1,6 @@ # urls -from django.urls import path # pylint: disable=E0401 +from django.urls import path # pylint: disable=E0401 from . import views diff --git a/src/django_gauth/utilities.py b/src/django_gauth/utilities.py index 414962f..4372b0b 100644 --- a/src/django_gauth/utilities.py +++ b/src/django_gauth/utilities.py @@ -2,8 +2,8 @@ from typing import Any, Dict, Tuple, Union from urllib.parse import urlparse -from django.conf import Settings, settings # pylint: disable=E0401 -from google.oauth2.credentials import Credentials # pylint: disable=E0401 +from django.conf import Settings, settings # pylint: disable=E0401 +from google.oauth2.credentials import Credentials # pylint: disable=E0401 __all__ = [ "credentials_to_dict", @@ -60,8 +60,8 @@ def check_gauth_authentication(session: Settings) -> Tuple[bool, object]: def is_valid_google_url(url: str) -> bool: - VALID_SCHEME = "https" # pylint: disable=C0103 - VALID_DOMAIN = "docs.google.com" # pylint: disable=C0103 + VALID_SCHEME = "https" # pylint: disable=C0103 + VALID_DOMAIN = "docs.google.com" # pylint: disable=C0103 try: result = urlparse(url) return ( diff --git a/src/django_gauth/views.py b/src/django_gauth/views.py index 871f4c8..5e5c0ad 100644 --- a/src/django_gauth/views.py +++ b/src/django_gauth/views.py @@ -3,46 +3,14 @@ ~@ankit.kumar05 """ -from django.conf import settings # pylint: disable=E0401 -from django.shortcuts import redirect, render # pylint: disable=E0401 -from django.urls import reverse # pylint: disable=E0401 -from google.auth.transport import requests # pylint: disable=E0401 -from google.oauth2 import id_token # pylint: disable=E0401 -from google_auth_oauthlib.flow import Flow # pylint: disable=E0401 - -from django_gauth import defaults -from django_gauth.utilities import check_gauth_authentication, credentials_to_dict +from django.conf import settings # pylint: disable=E0401 +from django.shortcuts import redirect, render # pylint: disable=E0401 +from django.urls import reverse # pylint: disable=E0401 +from google.auth.transport import requests # pylint: disable=E0401 +from google.oauth2 import id_token # pylint: disable=E0401 +from google_auth_oauthlib.flow import Flow # pylint: disable=E0401 -if hasattr(settings, "SCOPE") and settings.SCOPE: - SCOPE = settings.SCOPE -else: - SCOPE = [] - -if ( - hasattr(settings, "GOOGLE_AUTH_FINAL_REDIRECT_URL") - and settings.GOOGLE_AUTH_FINAL_REDIRECT_URL -): - GOOGLE_AUTH_FINAL_REDIRECT_URL = settings.GOOGLE_AUTH_FINAL_REDIRECT_URL -else: - GOOGLE_AUTH_FINAL_REDIRECT_URL = defaults.GOOGLE_AUTH_FINAL_REDIRECT_URL - -if ( - hasattr(settings, "CREDENTIALS_SESSION_KEY_NAME") - and settings.CREDENTIALS_SESSION_KEY_NAME -): - CREDENTIALS_SESSION_KEY_NAME = settings.CREDENTIALS_SESSION_KEY_NAME -else: - CREDENTIALS_SESSION_KEY_NAME = defaults.CREDENTIALS_SESSION_KEY_NAME - -if hasattr(settings, "STATE_KEY_NAME") and settings.STATE_KEY_NAME: - STATE_KEY_NAME = settings.STATE_KEY_NAME -else: - STATE_KEY_NAME = defaults.STATE_KEY_NAME - -if hasattr(settings, "FINAL_REDIRECT_KEY_NAME") and settings.FINAL_REDIRECT_KEY_NAME: - FINAL_REDIRECT_KEY_NAME = settings.STATE_KEY_NAME -else: - FINAL_REDIRECT_KEY_NAME = defaults.FINAL_REDIRECT_KEY_NAME +from django_gauth.utilities import check_gauth_authentication, credentials_to_dict def index(request): # type: ignore @@ -78,7 +46,7 @@ def login(request): # type: ignore } # if you need additional scopes, add them here , - scopes=SCOPE, + scopes=settings.SCOPE, ) # flow.redirect_uri = get_redirect_uri(request) # use this when @@ -88,13 +56,13 @@ def login(request): # type: ignore access_type="offline", prompt="select_account", include_granted_scopes="true" ) - request.session[STATE_KEY_NAME] = state + request.session[settings.STATE_KEY_NAME] = state if ( "final_redirect" not in request.session - or not request.session[FINAL_REDIRECT_KEY_NAME] + or not request.session[settings.FINAL_REDIRECT_KEY_NAME] ): - request.session[FINAL_REDIRECT_KEY_NAME] = ( - GOOGLE_AUTH_FINAL_REDIRECT_URL + request.session[settings.FINAL_REDIRECT_KEY_NAME] = ( + settings.GOOGLE_AUTH_FINAL_REDIRECT_URL or request.build_absolute_uri(reverse("django_gauth:index")) ) # directs where to land after login is successful. @@ -106,7 +74,7 @@ def callback(request): # type: ignore - Google IDP response control transfer """ # pull the state from the session - session_state = request.session.get(STATE_KEY_NAME) + session_state = request.session.get(settings.STATE_KEY_NAME) redirect_uri = request.build_absolute_uri(reverse("django_gauth:callback")) authorization_response = request.build_absolute_uri() # Flow Creation @@ -135,15 +103,17 @@ def callback(request): # type: ignore credentials = flow.credentials # verify token, while also retrieving information about the user id_info = id_token.verify_oauth2_token( - id_token=credentials._id_token, # pylint: disable=W0212 + id_token=credentials._id_token, # pylint: disable=W0212 request=requests.Request(), audience=settings.GOOGLE_CLIENT_ID, clock_skew_in_seconds=5, ) # session setting request.session["id_info"] = id_info - request.session[CREDENTIALS_SESSION_KEY_NAME] = credentials_to_dict(credentials) + request.session[settings.CREDENTIALS_SESSION_KEY_NAME] = credentials_to_dict( + credentials + ) # redirecting to the final redirect (i.e., logged in page) - redirect_response = redirect(request.session[FINAL_REDIRECT_KEY_NAME]) + redirect_response = redirect(request.session[settings.FINAL_REDIRECT_KEY_NAME]) return redirect_response