Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Django 5.x and Python 3.12 #17

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- master
- py312dj5
pull_request:

jobs:
Expand All @@ -12,12 +13,12 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ target/
# Temp Files
.*.sw*

# virtual environment
venv/

# Other Files
.DS_Store
.idea
Expand Down
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ format:

# Perform initial developer setup
# You will still need to setup tox to work with multiple
# python environements, perhaps with pyenv
# python environments, perhaps with pyenv
install:
pip install --upgrade pip wheel setuptools build
pip install -r requirements.txt
pip install tox

Expand All @@ -39,12 +40,14 @@ test:
test-env:
tox -e ${ENV}

# Run after a dependency / supported verion update
# Run after a dependency / supported version update
# to recreate test environments
test-recreate:
tox -r

build: clean
python -m build

# Upload a new build to pypi
upload_pypi: clean
python setup.py sdist bdist_wheel
upload_pypi: build
twine upload dist/* --skip-existing
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@

The app was designed to replace key features of the built-in `django.contrib.auth` package. Developers may simply replace the appropriate backends and URLs and let Uniauth handle authentication entirely if they wish. However, the app is also fully customizable, and components may be swapped with compatible replacements if desired.

<p align="center">
<img src="https://s3.amazonaws.com/uniauth/documentation/Login+Page.png" />
<p style="text-align: center">
<img src="https://s3.amazonaws.com/uniauth/documentation/Login+Page.png" alt="login panel"/>
</p>

## Features

- Supports Python 2.7, 3.5+
- Supports Django 1.11, 2.x, 3.x, 4.x
- Supports Python 3.8+
- Supports Django 4.x, 5.x
- Supports using a [custom User model](https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#specifying-a-custom-user-model)
- Supports using email addresses as the ["username" field](https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#django.contrib.auth.models.CustomUser.USERNAME_FIELD)
- Users can link multiple email addresses and use any for authentication
Expand All @@ -25,6 +25,7 @@ The app was designed to replace key features of the built-in `django.contrib.aut

## Major Updates

- **1.5.0:** Added support for Django 5.x and Python 3.12; drops unsupported Python and Django
- **1.4.0:** Added support for custom JWT token serializers
- **1.3.1:** Added support for Django 4.x and newer Python versions
- **1.3.0:** Added [JWT Support](https://github.com/lgoodridge/django-uniauth#using-jwt-authentication)
Expand Down Expand Up @@ -117,7 +118,7 @@ The following custom settings are also used:

Uniauth supports any custom User model, so long as the model has `username` and `email` fields. The `email` serves as the primary identifying field within Uniauth, with the `username` being set to an arbitrary unique value to support packages that require it. Once a user's profile has been activated, other apps are free to change the `username` without disrupting Uniauth's behavior.

Users are created by either completing the Sign Up form, or logging in via an `InstitutionAccount`. In the former case, they are given a username beginning with `tmp-`, followed by a unique suffix, and an empty `email` field. When the first email for a user has been verified, their profile is considered fully activated, the `email` field is set to the verified email, and the `username` field is arbitrarily set to that email address as well, unless it is taken. In the latter case, they are given a username describing how they were authenticated, along with the institution they signed into and their ID for that institution. They will keep this username and have an empty `email` field until they link their account to a verified Uniauth profile.
Users are created by either completing the Sign-Up form, or logging in via an `InstitutionAccount`. In the former case, they are given a username beginning with `tmp-`, followed by a unique suffix, and an empty `email` field. When the first email for a user has been verified, their profile is considered fully activated, the `email` field is set to the verified email, and the `username` field is arbitrarily set to that email address as well, unless it is taken. In the latter case, they are given a username describing how they were authenticated, along with the institution they signed in to and their ID for that institution. They will keep this username and have an empty `email` field until they link their account to a verified Uniauth profile.

Users may have multiple email addresses linked to their profile, any of which may be used for authentication (if one of the `LinkedEmail` [Uniauth backends](https://github.com/lgoodridge/django-uniauth#backends) are used), or for password reset. The address set in the user's `email` field is considered the "primary email", and is the only one that must be unique across all users. Users may change which linked email is their primary email address at any point via the `settings` page, so long as that primary email is not taken by another user.

Expand All @@ -131,15 +132,15 @@ Uniauth has the following models:

This model is automatically attached to each User upon creation, and extends the User model with the extra data Uniauth requires. The other Uniauth models all interact with the `UserProfile` model rather than the User model directly. Accessible via `user.uniauth_profile`.

- `get_display_id`: This method returns a more display-friendly ID for the user, using their username. If the User was created via CAS authentication, it will return their username without the institution prefix (so a User with username "cas-exampleinst-id123" would return "id123"). If their username is an email address, it will return everything before the "@" symbol (so "[email protected]" would become "johndoe"). Otherwise the username is returned unmodified. These generated IDs are not guaranteed to be unique.
- `get_display_id`: This method returns a more display-friendly ID for the user, using their username. If the User was created via CAS authentication, it will return their username without the institution prefix (so a User with username "cas-exampleinst-id123" would return "id123"). If their username is an email address, it will return everything before the "@" symbol (so "[email protected]" would become "johndoe"). Otherwise, the username is returned unmodified. These generated IDs are not guaranteed to be unique.

### LinkedEmail:

Represents an email address linked to a User's account. Accessible via `user.uniauth_profile.linked_emails`.

### Institution:

Represents an organization possesing an authentication server that can be logged into. You will need to add an Institution for each CAS server you wish to support. The `add_institution` and `remove_institution` commands are provided to help with this.
Represents an organization possessing an authentication server that can be logged into. You will need to add an Institution for each CAS server you wish to support. The `add_institution` and `remove_institution` commands are provided to help with this.

### InstitutionAccount:

Expand Down Expand Up @@ -172,7 +173,7 @@ Uniauth provides the following management commands:
- `add_institution <name> <cas_server_url>`: Adds an `Institution` with the provided name and CAS server URL to the database. The `name` will be the text displayed in the CAS server dropdown on the Login page, and `cas_server_url` must point to the root URL of a CAS protocol compliant service. The command will return the institution's slug created from the provided name; this slug must be used when referring to the institution in other commands (such as `remove_institution`).
- Example Usage: `python manage.py add_institution "Example Inst" "https://www.example.com/cas/"`
- You may add the `--update-existing` option to update the CAS server URL of an existing institution with that name, or create one if it does not exist.
- `remove_institution <slug>`: Removes the `Institution` with the provided slug from the database. This action removes any `InstitutionAccounts` for that instiutiton in the process.
- `remove_institution <slug>`: Removes the `Institution` with the provided slug from the database. This action removes any `InstitutionAccounts` for that institution in the process.
- `migrate_cas <slug>`: Migrates a project originally using CAS for authentication to using Uniauth. See the [User Migration](https://github.com/lgoodridge/django-uniauth#user-migration) section for more information.
- `migrate_custom`: Migrates a project originally using custom User authentication to using Uniauth. See the [User Migration](https://github.com/lgoodridge/django-uniauth#user-migration) section for more information.
- `flush_tmp_users [days]`: Deletes temporary users more than the specified number of days old from the database. The default number of days is 1.
Expand All @@ -182,7 +183,7 @@ Uniauth provides the following management commands:
The five views you will likely care about the most are `login`, `logout`, `signup`, `password-reset`, and `settings`:

- `/login/`: Displays a page allowing users to log in by entering a username/email and password, or via a supported backend, such as CAS. Also displays links for creating an account directly, and for resetting passwords.
- `/logout/`: Logs out the user. The behavior and redirect location of the log out is determined by the app's settings.
- `/logout/`: Logs out the user. The behavior and redirect location of the log-out is determined by the app's settings.
- `/signup/`: Prompts user for a primary email address, and a password, then sends a verification email to that address to activate the account.
- `/password-reset/`: Prompts user for an email address, then sends an email to that address containing a link for resetting the password. If no users have the entered email address linked to their account, no email is sent. If multiple users have that address linked, an email is sent for each potential user.
- `/settings/`: Allows users to perform account related actions, such as link more email addresses, choose the primary email address, link more Institution Accounts, or change their password.
Expand Down Expand Up @@ -243,10 +244,10 @@ The only URL parameter that is not preserved is the `next` variable, which indic

If you wish to use Uniauth with a project that already has users, a `UserProfile` (and, if applicable, `LinkedEmail` or `InstitutionAccount`) will need to be created for each existing user. You may use one of the provided commands to assist with this, provided your project meets one of the following conditions:

- If you were previously using CAS for authentication, and the username for each user matches the CAS ID (as would be the case if you were using a package like [django-cas-ng](https://github.com/mingchen/django-cas-ng)), you should first [add an Institution](https://github.com/lgoodridge/django-uniauth#commands) for the CAS server you were using, then use the `migrate_cas` command with the slug of the created Institution to peform the migration. A `UserProfile` will be created for all users, and the usernames of all Users will be changed to conform to Uniauth's expectations (to `cas-<institution_slug>-<original_username>`). To get the original username (without the CAS institution prefix), use the `get_display_id` method provided by the `UserProfile` model.
- If you were previously using CAS for authentication, and the username for each user matches the CAS ID (as would be the case if you were using a package like [django-cas-ng](https://github.com/mingchen/django-cas-ng)), you should first [add an Institution](https://github.com/lgoodridge/django-uniauth#commands) for the CAS server you were using, then use the `migrate_cas` command with the slug of the created Institution to perform the migration. A `UserProfile` will be created for all users, and the usernames of all Users will be changed to conform to Uniauth's expectations (to `cas-<institution_slug>-<original_username>`). To get the original username (without the CAS institution prefix), use the `get_display_id` method provided by the `UserProfile` model.
- If you were previously using custom user authentication (as in, Users would sign up with a username / email address and password), you may use the `migrate_custom` command to migrate the users. A `UserProfile` will be created for each migrated user, and a verified `LinkedEmail` will also be created for all users with a non-blank `email` field. Note that any users lacking a username / email or password will not be migrated. Also note that if the `LinkedEmailBackend` is used, users that don't have a `LinkedEmail` created will not be able to log in until one is linked.

If your project does not fit either of these conditions, you will need to manually migrate the users as appropiate. Please create a `UserProfile` for each user, and `LinkedEmails` or `InstitutionAccounts` as appropiate.
If your project does not fit either of these conditions, you will need to manually migrate the users as appropriate. Please create a `UserProfile` for each user, and `LinkedEmails` or `InstitutionAccounts` as appropriate.

## Using JWT Authentication

Expand All @@ -268,7 +269,7 @@ Please refer to [django-rest-framework-simplejwt](https://pypi.org/project/djang

## Demo Application

The source repository contains a `demo_app` directory which demonstrates how to setup a simple Django app to use Uniauth. This app has no functionality, and exists solely to show off the installable `uniauth` app. A quick-start guide for integrating Uniauth can be found [here](https://github.com/lgoodridge/django-uniauth/tree/master/demo_app).
The source repository contains a `demo_app` directory which demonstrates how to set up a simple Django app to use Uniauth. This app has no functionality, and exists solely to show off the installable `uniauth` app. A quick-start guide for integrating Uniauth can be found [here](https://github.com/lgoodridge/django-uniauth/tree/master/demo_app).

## Acknowledgements

Expand Down
7 changes: 4 additions & 3 deletions demo_app/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
## Demo App - Quick Start Guide

This app provides an example of how to setup a project to use Uniauth. It has no functionality, and exists solely to show off the installable `uniauth` app.
This app provides an example of how to set up a project to use Uniauth.
It has no functionality, and exists solely to show off the installable `uniauth` app.

### Installation

# (Recommended) Create a virtual env
# Create a virtual env
mkvirtualenv myenv --python=$(which python3)

# Install the dependencies
pip install django
pip install "django>=5"
pip install python-cas
pip install django-uniauth

Expand Down
2 changes: 1 addition & 1 deletion demo_app/demo_app/templates/demo-app/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Index Page</title>
</head>
Expand Down
10 changes: 6 additions & 4 deletions demo_app/demo_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
Example app URL configuration.
"""

from django.conf.urls import include
from django.contrib import admin
from django.conf.urls import include, url
from django.urls import path

from . import views

urlpatterns = [
url('admin/', admin.site.urls),
url('accounts/', include('uniauth.urls', namespace='uniauth')),
url('^$', views.index, name='index'),
path('admin/', admin.site.urls),
path('accounts/', include('uniauth.urls', namespace='uniauth')),
path('', views.index, name='index'),
]
2 changes: 2 additions & 0 deletions demo_app/demo_app/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from django.shortcuts import render

from uniauth.decorators import login_required


@login_required
def index(request):
return render(request, 'demo-app/index.html')
48 changes: 48 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,54 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "django-uniauth"
version = "1.5.0"
description = "A Django app for managing CAS and custom user authentication."
keywords = ["django", "auth", "authentication", "cas", "sso", "single sign-on"]
readme = "README.md"
requires-python = ">=3.8"
license = { file = "LICENSE.md"}
authors = [
{ name = "Lance Goodridge", email = "[email protected]"},
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: Django",
"Framework :: Django :: 2",
"Framework :: Django :: 3",
"Framework :: Django :: 4",
"Framework :: Django :: 5",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
]
dependencies = [
# Specify explicit dependencies here:
"Django >=4.2.8",
"python-cas >=1.4.0",
"djangorestframework-simplejwt >=4.1.0",
]

[project.urls]
"Source Code" = "https://github.com/lgoodridge/django-uniauth"
"Bug Tracker" = "https://github.com/lgoodridge/django-uniauth/issues"

[tool.black]
line-length = 79

[tool.isort]
profile = 'black'
line_length = 79

[tool.setuptools.package-data]
"uniauth" = ["py.typed"]

[tool.setuptools.packages.find]
namespaces = false
include = ["uniauth*"]
exclude = ["demo_app*"]
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
djangorestframework-simplejwt>=4.1.0
mock==2.0.0; python_version < "3.3"
PyJWT<=1.7.1; python_version < "3.7"
python-cas>=1.4.0

Django>=4.2
djangorestframework
53 changes: 45 additions & 8 deletions runtests.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,52 @@
#!/usr/bin/env python
import os
import sys
from pathlib import Path
from typing import NoReturn, List, Optional

import django
from django.conf import settings
from django.test.utils import get_runner
import django
import os
import sys

if __name__ == "__main__":

def usage(message: str = '') -> NoReturn:
exit_value = 0
if message:
print(message)
exit_value = 1
print("Usage: python runtests.py [test_labels]")
sys.exit(exit_value)


def get_test_labels(argv: Optional[List[str]] = None) -> List[str]:
if argv is None:
argv = sys.argv
if len(argv) > 1:
if ('-h' in argv[1:]) or ('--help' in argv[1:]):
usage()
labels = []
for label in argv[1:]:
if not label.startswith('test_'):
usage('test_labels must start with "test_"')
file_path = Path('tests') / f'{label}.py'
if not file_path.exists():
usage(f'{file_path} for test_label "{label}" does not exist')
labels.append(f'tests.{label}')
else:
labels = ['tests']
return labels


def main() -> int:
test_labels = get_test_labels()
os.environ['DJANGO_SETTINGS_MODULE'] = "tests.settings"
django.setup()
TestRunner = get_runner(settings)
test_runner = TestRunner()
failures = test_runner.run_tests(["tests"])
sys.exit(bool(failures))
test_runner_class = get_runner(settings)
test_runner = test_runner_class()
num_failures = test_runner.run_tests(test_labels=test_labels)
return 0 if num_failures == 0 else 1


if __name__ == "__main__":
retval = main()
sys.exit(retval)
Loading