Skip to content

Commit

Permalink
feat: settings and auth_backends
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Sep 18, 2023
1 parent 0c81bc9 commit be3d33b
Show file tree
Hide file tree
Showing 16 changed files with 538 additions and 210 deletions.
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ flask = "==2.2.3"
importlib-metadata = "==4.13.0" # TODO: remove. needed by old portal
django-formtools = "==2.2" # TODO: remove. needed by old portal
django-otp = "==1.0.2" # TODO: remove. needed by old portal
# https://pypi.org/user/codeforlife/
cfl-common = "==6.36.0" # TODO: remove

[dev-packages]
black = "==23.1.0"
Expand Down
242 changes: 234 additions & 8 deletions Pipfile.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions codeforlife/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from .version import __version__

from . import kurono
from . import service
from . import user
from . import (
kurono,
service,
user,
)
19 changes: 19 additions & 0 deletions codeforlife/settings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
These are the common settings that need to be imported in all django projects
throughout our system. Place the following at the END of every settings.py file.
`from codeforlife.settings import *`
If you need to reference a specific CFL setting in a project's setting:
`
# Place this at the top of your file.
from codeforlife import settings as cfl_settings
# Do something with EXAMPLE_SETTING from codeforlife's settings.
cfl_settings.EXAMPLE_SETTING
`
"""

from .custom import *
from .django import *
from .third_party import *
26 changes: 26 additions & 0 deletions codeforlife/settings/custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
This file contains all of our custom settings we define for our own purposes.
"""

import os

# The name of the current service.
SERVICE_NAME = os.environ["SERVICE_NAME"] # *required

# If the current service the root service. This will only be true for portal.
SERVICE_IS_ROOT = bool(int(os.getenv("SERVICE_IS_ROOT", "0")))

# The protocol, domain and port of the current service.
SERVICE_PROTOCOL = os.getenv("SITE_PROTOCOL", "http")
SERVICE_DOMAIN = os.getenv("SITE_DOMAIN", "localhost")
SERVICE_PORT = int(os.getenv("SITE_PORT", "8000"))

# The base url of the current service.
# The root service does not need its name included in the base url.
SERVICE_BASE_URL = f"{SERVICE_PROTOCOL}://{SERVICE_DOMAIN}:{SERVICE_PORT}"
if not SERVICE_IS_ROOT:
SERVICE_BASE_URL += f"/{SERVICE_NAME}"


# The api url of the current service.
SERVICE_API_URL = f"{SERVICE_BASE_URL}/api"
54 changes: 54 additions & 0 deletions codeforlife/settings/django.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""
This file contains all of the settings Django supports out of the box.
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

import os

from .custom import SERVICE_NAME

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.getenv("DEBUG", "1")))

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv("SECRET_KEY", "replace-me")

# Authentication backends
# https://docs.djangoproject.com/en/3.2/ref/settings/#authentication-backends

AUTHENTICATION_BACKENDS = [
"user.auth_backends.EmailAndPasswordBackend",
"user.auth_backends.UserIdAndLoginIdBackend",
"user.auth_backends.UsernameAndPasswordAndClassIdBackend",
]

# Sessions
# https://docs.djangoproject.com/en/3.2/topics/http/sessions/

SESSION_COOKIE_AGE = 60 * 60
SESSION_SAVE_EVERY_REQUEST = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "None"
SESSION_COOKIE_DOMAIN = "localhost" if DEBUG else "codeforlife.education"

# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# CSRF
# https://docs.djangoproject.com/en/3.2/ref/csrf/

CSRF_COOKIE_NAME = f"{SERVICE_NAME}_csrftoken"
CSRF_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SECURE = True
3 changes: 3 additions & 0 deletions codeforlife/settings/third_party.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
This file contains custom settings defined by third party extensions.
"""
120 changes: 60 additions & 60 deletions codeforlife/user/admin.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,60 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .models import User


class CustomUserAdmin(UserAdmin):
model = User

list_display = (
"username",
"email",
"is_active",
"is_staff",
"is_superuser",
"last_login",
)

list_filter = ("is_active", "is_staff", "is_superuser")

fieldsets = (
(None, {"fields": ("username", "email", "password")}),
(
"Permissions",
{
"fields": (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Dates", {"fields": ("last_login", "date_joined")}),
)

add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)

search_fields = ("email",)

ordering = ("email",)


admin.site.register(User, CustomUserAdmin)
# from django.contrib import admin
# from django.contrib.auth.admin import UserAdmin

# from .models import User


# class CustomUserAdmin(UserAdmin):
# model = User

# list_display = (
# "username",
# "email",
# "is_active",
# "is_staff",
# "is_superuser",
# "last_login",
# )

# list_filter = ("is_active", "is_staff", "is_superuser")

# fieldsets = (
# (None, {"fields": ("username", "email", "password")}),
# (
# "Permissions",
# {
# "fields": (
# "is_staff",
# "is_active",
# "is_superuser",
# "groups",
# "user_permissions",
# )
# },
# ),
# ("Dates", {"fields": ("last_login", "date_joined")}),
# )

# add_fieldsets = (
# (
# None,
# {
# "classes": ("wide",),
# "fields": (
# "username",
# "email",
# "password1",
# "password2",
# "is_staff",
# "is_active",
# ),
# },
# ),
# )

# search_fields = ("email",)

# ordering = ("email",)


# admin.site.register(User, CustomUserAdmin)
5 changes: 5 additions & 0 deletions codeforlife/user/auth_backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .email_and_password import EmailAndPasswordBackend
from .user_id_and_login_id import UserIdAndLoginIdBackend
from .username_and_password_and_class_id import (
UsernameAndPasswordAndClassIdBackend,
)
35 changes: 35 additions & 0 deletions codeforlife/user/auth_backends/email_and_password.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import typing as t

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.base_user import AbstractBaseUser
from django.core.handlers.wsgi import WSGIRequest

User = get_user_model()


class EmailAndPasswordBackend(BaseBackend):
def authenticate(
self,
request: WSGIRequest,
email: t.Optional[str] = None,
password: t.Optional[str] = None,
**kwargs
) -> t.Optional[AbstractBaseUser]:
if email is None or password is None:
return

try:
user = User.objects.get(email=email)
if getattr(user, "is_active", True) and user.check_password(
password
):
return user
except User.DoesNotExist:
return

def get_user(self, user_id: int) -> t.Optional[AbstractBaseUser]:
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
39 changes: 39 additions & 0 deletions codeforlife/user/auth_backends/user_id_and_login_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import typing as t

from common.helpers.generators import get_hashed_login_id
from common.models import Student
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.base_user import AbstractBaseUser
from django.core.handlers.wsgi import WSGIRequest

User = get_user_model()


class UserIdAndLoginIdBackend(BaseBackend):
def authenticate(
self,
request: WSGIRequest,
user_id: t.Optional[int] = None,
login_id: t.Optional[str] = None,
**kwargs
) -> t.Optional[AbstractBaseUser]:
if user_id is None or login_id is None:
return

user = self.get_user(user_id)
if user and getattr(user, "is_active", True):
# Check the url against the student's stored hash.
student = Student.objects.get(new_user=user)
if (
student.login_id
# TODO: refactor this
and get_hashed_login_id(login_id) == student.login_id
):
return user

def get_user(self, user_id: int) -> t.Optional[AbstractBaseUser]:
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import typing as t

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.base_user import AbstractBaseUser
from django.core.handlers.wsgi import WSGIRequest

User = get_user_model()


class UsernameAndPasswordAndClassIdBackend(BaseBackend):
def authenticate(
self,
request: WSGIRequest,
username: t.Optional[str] = None,
password: t.Optional[str] = None,
class_id: t.Optional[str] = None,
**kwargs
) -> t.Optional[AbstractBaseUser]:
if username is None or password is None or class_id is None:
return

try:
user = User.objects.get(
username=username,
new_student__class_field__access_code=class_id,
)
if getattr(user, "is_active", True) and user.check_password(
password
):
return user
except User.DoesNotExist:
return

def get_user(self, user_id: int) -> t.Optional[AbstractBaseUser]:
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
Loading

0 comments on commit be3d33b

Please sign in to comment.