feat: cas
This commit is contained in:
parent
578a5af3df
commit
498a1063f7
121
README.md
Normal file
121
README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Opus Magnum Submitter with CAS Authentication
|
||||
|
||||
A simple Django application that demonstrates CAS (Central Authentication Service) integration with PolyLAN's CAS server at https://polylan.ch/cas/.
|
||||
|
||||
## Features
|
||||
|
||||
- 🔐 CAS authentication with PolyLAN
|
||||
- 👤 Automatic user creation with custom attributes
|
||||
- 🏷️ CAS groups and permissions storage
|
||||
- 🏠 Protected home page requiring authentication
|
||||
- 🌍 Public page accessible without authentication
|
||||
- 🚪 Clean login/logout functionality
|
||||
- 📱 Responsive web interface
|
||||
- 🛠️ Admin interface for user management
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Install dependencies:**
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
2. **Run database migrations:**
|
||||
```bash
|
||||
cd opus_submitter
|
||||
uv run manage.py migrate
|
||||
```
|
||||
|
||||
3. **Create a superuser (optional, for admin access):**
|
||||
```bash
|
||||
uv run manage.py createsuperuser
|
||||
```
|
||||
|
||||
4. **Start the development server:**
|
||||
```bash
|
||||
uv run manage.py runserver localhost:7777
|
||||
```
|
||||
|
||||
5. **Access the application:**
|
||||
- Open your browser to http://localhost:7777/
|
||||
- Try the public page: http://localhost:7777/public/
|
||||
- Login with CAS to access protected features
|
||||
- Admin interface: http://localhost:7777/admin/ (requires superuser)
|
||||
|
||||
## How It Works
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. User visits a protected page (e.g., home page)
|
||||
2. Django redirects to `/cas/login/`
|
||||
3. CAS redirects to PolyLAN CAS server: `https://polylan.ch/cas/login`
|
||||
4. User enters credentials on PolyLAN
|
||||
5. CAS validates credentials and redirects back with a service ticket
|
||||
6. Django validates the ticket with the CAS server
|
||||
7. User is authenticated and redirected to the requested page
|
||||
|
||||
### Configuration
|
||||
|
||||
The CAS configuration is in `opus_submitter/settings.py`:
|
||||
|
||||
```python
|
||||
# CAS Authentication Settings
|
||||
CAS_SERVER_URL = 'https://polylan.ch/cas/'
|
||||
CAS_VERSION = '3'
|
||||
CAS_CREATE_USER = True
|
||||
CAS_LOGOUT_COMPLETELY = True
|
||||
```
|
||||
|
||||
### URLs
|
||||
|
||||
- `/` - Protected home page (requires authentication)
|
||||
- `/public/` - Public page (no authentication required)
|
||||
- `/cas/login/` - CAS login endpoint
|
||||
- `/cas/logout/` - CAS logout endpoint
|
||||
- `/admin/` - Django admin (requires staff privileges)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
opus_submitter/
|
||||
├── manage.py
|
||||
├── opus_submitter/
|
||||
│ ├── __init__.py
|
||||
│ ├── settings.py # Django settings with CAS configuration
|
||||
│ ├── urls.py # URL routing with CAS endpoints
|
||||
│ ├── wsgi.py
|
||||
│ └── asgi.py
|
||||
└── templates/
|
||||
├── base.html # Base template with navigation
|
||||
├── home.html # Protected home page
|
||||
└── public.html # Public page
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Django 5.2.7+
|
||||
- django-cas-ng 5.0.1+ (CAS client for Django)
|
||||
- requests 2.31.0+ (HTTP library for CAS communication)
|
||||
|
||||
## Development
|
||||
|
||||
To modify the CAS configuration:
|
||||
|
||||
1. Edit `CAS_SERVER_URL` in `settings.py` if using a different CAS server
|
||||
2. Adjust `CAS_VERSION` if needed (supports CAS 1.0, 2.0, and 3.0)
|
||||
3. Set `CAS_CREATE_USER = False` if you don't want automatic user creation
|
||||
|
||||
## Testing
|
||||
|
||||
1. Visit http://127.0.0.1:8000/public/ (should work without login)
|
||||
2. Visit http://127.0.0.1:8000/ (should redirect to CAS login)
|
||||
3. Login with your PolyLAN credentials
|
||||
4. Verify you're redirected back and can see user information
|
||||
5. Test logout functionality
|
||||
|
||||
## Notes
|
||||
|
||||
- This is a development setup with `DEBUG = True`
|
||||
- For production, update `SECRET_KEY`, set `DEBUG = False`, and configure `ALLOWED_HOSTS`
|
||||
- The application automatically creates Django users from CAS authentication
|
||||
- User information is populated from CAS attributes when available
|
||||
0
opus_submitter/accounts/__init__.py
Normal file
0
opus_submitter/accounts/__init__.py
Normal file
32
opus_submitter/accounts/admin.py
Normal file
32
opus_submitter/accounts/admin.py
Normal file
@ -0,0 +1,32 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import CustomUser
|
||||
|
||||
|
||||
@admin.register(CustomUser)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
"""Admin interface for CustomUser."""
|
||||
|
||||
# Add custom fields to the user admin
|
||||
fieldsets = UserAdmin.fieldsets + (
|
||||
('CAS Information', {
|
||||
'fields': ('cas_user_id', 'cas_groups', 'cas_attributes'),
|
||||
}),
|
||||
)
|
||||
|
||||
# Add custom fields to the list display
|
||||
list_display = UserAdmin.list_display + ('cas_user_id', 'get_cas_groups_display')
|
||||
|
||||
# Add search fields
|
||||
search_fields = UserAdmin.search_fields + ('cas_user_id',)
|
||||
|
||||
# Add filters
|
||||
list_filter = UserAdmin.list_filter + ('cas_groups',)
|
||||
|
||||
# Make CAS fields readonly in admin
|
||||
readonly_fields = ('cas_user_id', 'cas_groups', 'cas_attributes')
|
||||
|
||||
def get_cas_groups_display(self, obj):
|
||||
"""Display CAS groups in admin list."""
|
||||
return obj.get_cas_groups_display()
|
||||
get_cas_groups_display.short_description = 'CAS Groups'
|
||||
6
opus_submitter/accounts/apps.py
Normal file
6
opus_submitter/accounts/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'accounts'
|
||||
47
opus_submitter/accounts/migrations/0001_initial.py
Normal file
47
opus_submitter/accounts/migrations/0001_initial.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-28 23:41
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('cas_user_id', models.CharField(blank=True, max_length=50, null=True, unique=True)),
|
||||
('cas_groups', models.JSONField(blank=True, default=list)),
|
||||
('cas_attributes', models.JSONField(blank=True, default=dict)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
opus_submitter/accounts/migrations/__init__.py
Normal file
0
opus_submitter/accounts/migrations/__init__.py
Normal file
60
opus_submitter/accounts/models.py
Normal file
60
opus_submitter/accounts/models.py
Normal file
@ -0,0 +1,60 @@
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
import json
|
||||
|
||||
|
||||
class CustomUser(AbstractUser):
|
||||
"""Custom User model to store CAS attributes from PolyLAN."""
|
||||
|
||||
# Store CAS user ID (the numeric ID from CAS)
|
||||
cas_user_id = models.CharField(max_length=50, blank=True, null=True, unique=True)
|
||||
|
||||
# Store CAS groups as JSON
|
||||
cas_groups = models.JSONField(default=list, blank=True)
|
||||
|
||||
# Additional fields that might come from CAS
|
||||
cas_attributes = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.username} ({self.cas_user_id})"
|
||||
|
||||
def has_cas_group(self, group_name):
|
||||
"""Check if user has a specific CAS group."""
|
||||
return group_name in self.cas_groups
|
||||
|
||||
def get_cas_groups_display(self):
|
||||
"""Get a comma-separated list of CAS groups for display."""
|
||||
return ", ".join(self.cas_groups) if self.cas_groups else "No groups"
|
||||
|
||||
def update_cas_data(self, cas_user_id, attributes):
|
||||
"""Update user with CAS data."""
|
||||
self.cas_user_id = cas_user_id
|
||||
|
||||
# Update basic fields from CAS attributes
|
||||
if "firstname" in attributes:
|
||||
self.first_name = attributes["firstname"]
|
||||
if "lastname" in attributes:
|
||||
self.last_name = attributes["lastname"]
|
||||
if "email" in attributes:
|
||||
self.email = attributes["email"]
|
||||
|
||||
# Store groups
|
||||
if "groups" in attributes:
|
||||
# CAS groups come as a list or single value
|
||||
groups = attributes["groups"]
|
||||
if isinstance(groups, str):
|
||||
self.cas_groups = [groups]
|
||||
elif isinstance(groups, list):
|
||||
self.cas_groups = groups
|
||||
else:
|
||||
self.cas_groups = []
|
||||
|
||||
if "RESPONSABLE_ANIMATION" in self.cas_groups:
|
||||
self.is_staff = True
|
||||
self.is_superuser = True
|
||||
|
||||
# Store all other attributes
|
||||
self.cas_attributes = attributes
|
||||
|
||||
self.save()
|
||||
|
||||
3
opus_submitter/accounts/tests.py
Normal file
3
opus_submitter/accounts/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
opus_submitter/accounts/views.py
Normal file
3
opus_submitter/accounts/views.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@ -20,62 +20,64 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-m0ivj_3gpbf281jl1$&n5pdo!5le(bp4z31u(1&4s=n#!tpy=n'
|
||||
SECRET_KEY = "django-insecure-m0ivj_3gpbf281jl1$&n5pdo!5le(bp4z31u(1&4s=n#!tpy=n"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"accounts",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
# Removed CASMiddleware to avoid conflicts with our custom backend
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'opus_submitter.urls'
|
||||
ROOT_URLCONF = "opus_submitter.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [BASE_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'opus_submitter.wsgi.application'
|
||||
WSGI_APPLICATION = "opus_submitter.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,16 +87,16 @@ DATABASES = {
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
@ -102,9 +104,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -114,9 +116,27 @@ USE_TZ = True
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_URL = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# Custom User Model
|
||||
AUTH_USER_MODEL = "accounts.CustomUser"
|
||||
|
||||
# Simple CAS Configuration
|
||||
CAS_SERVER_URL = "https://polylan.ch/cas/"
|
||||
|
||||
# Authentication backends
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"simple_cas_backend.SimpleCASBackend",
|
||||
]
|
||||
|
||||
# Login/Logout URLs
|
||||
LOGIN_URL = "/cas/login/"
|
||||
LOGOUT_URL = "/cas/logout/"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
LOGOUT_REDIRECT_URL = "/"
|
||||
|
||||
@ -14,9 +14,14 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from simple_cas_views import SimpleCASLoginView, SimpleCASLogoutView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("admin/", admin.site.urls),
|
||||
path("cas/login/", SimpleCASLoginView.as_view(), name="cas_ng_login"),
|
||||
path("cas/logout/", SimpleCASLogoutView.as_view(), name="cas_ng_logout"),
|
||||
]
|
||||
|
||||
92
opus_submitter/simple_cas_backend.py
Normal file
92
opus_submitter/simple_cas_backend.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""
|
||||
Simple CAS 2.0 authentication backend - bare minimum implementation.
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import BaseBackend
|
||||
|
||||
|
||||
class SimpleCASBackend(BaseBackend):
|
||||
"""Simple CAS 2.0 authentication backend."""
|
||||
|
||||
def authenticate(self, request, ticket=None, service=None, **kwargs):
|
||||
"""Authenticate user using CAS ticket."""
|
||||
if not ticket or not service:
|
||||
return None
|
||||
|
||||
# Validate ticket with CAS server
|
||||
cas_user_id, attributes = self.validate_ticket(ticket, service)
|
||||
|
||||
if not cas_user_id:
|
||||
return None
|
||||
|
||||
print(f"CAS User ID: {cas_user_id}")
|
||||
print(f"CAS Attributes: {attributes}")
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# Try to find user by CAS user ID first, then by username
|
||||
username = attributes.get("username", cas_user_id).lower()
|
||||
|
||||
try:
|
||||
# First try to find by CAS user ID
|
||||
user = User.objects.get(cas_user_id=cas_user_id)
|
||||
except User.DoesNotExist:
|
||||
try:
|
||||
# Then try by username
|
||||
user = User.objects.get(username=username)
|
||||
# Update the CAS user ID if found by username
|
||||
user.cas_user_id = cas_user_id
|
||||
user.save()
|
||||
except User.DoesNotExist:
|
||||
# Create new user
|
||||
user = User.objects.create_user(
|
||||
username=username,
|
||||
cas_user_id=cas_user_id,
|
||||
first_name=attributes.get("firstname", ""),
|
||||
last_name=attributes.get("lastname", ""),
|
||||
email=attributes.get("email", ""),
|
||||
)
|
||||
|
||||
# Always update CAS data on login
|
||||
user.update_cas_data(cas_user_id, attributes)
|
||||
|
||||
return user
|
||||
|
||||
def validate_ticket(self, ticket, service):
|
||||
"""Validate CAS ticket and return username and attributes."""
|
||||
validate_url = f"{settings.CAS_SERVER_URL.rstrip('/')}/serviceValidate"
|
||||
|
||||
params = {"ticket": ticket, "service": service, "format": "JSON"}
|
||||
|
||||
try:
|
||||
response = requests.get(validate_url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = json.loads(response.text)
|
||||
|
||||
# Parse CAS 2.0 JSON response
|
||||
service_response = data.get("serviceResponse", {})
|
||||
auth_success = service_response.get("authenticationSuccess")
|
||||
|
||||
if auth_success:
|
||||
cas_user_id = auth_success.get("user", "")
|
||||
attributes = auth_success.get("attributes", {})
|
||||
return cas_user_id, attributes
|
||||
|
||||
return None, None
|
||||
|
||||
except Exception as e:
|
||||
print(f"CAS validation error: {e}")
|
||||
return None, None
|
||||
|
||||
def get_user(self, user_id):
|
||||
"""Get user by ID."""
|
||||
User = get_user_model()
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return None
|
||||
46
opus_submitter/simple_cas_views.py
Normal file
46
opus_submitter/simple_cas_views.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""
|
||||
Simple CAS views - bare minimum implementation.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.shortcuts import redirect
|
||||
from django.http import HttpResponse
|
||||
from django.views import View
|
||||
import urllib.parse
|
||||
|
||||
|
||||
class SimpleCASLoginView(View):
|
||||
"""Simple CAS login view."""
|
||||
|
||||
def get(self, request):
|
||||
ticket = request.GET.get('ticket')
|
||||
|
||||
if ticket:
|
||||
# Coming back from CAS with ticket - validate it
|
||||
service_url = request.build_absolute_uri().split('?')[0] # Remove query params
|
||||
|
||||
user = authenticate(request=request, ticket=ticket, service=service_url)
|
||||
|
||||
if user:
|
||||
login(request, user)
|
||||
return redirect(settings.LOGIN_REDIRECT_URL)
|
||||
else:
|
||||
return HttpResponse("Authentication failed", status=401)
|
||||
|
||||
else:
|
||||
# No ticket - redirect to CAS
|
||||
service_url = request.build_absolute_uri().split('?')[0] # Remove query params
|
||||
cas_login_url = f"{settings.CAS_SERVER_URL.rstrip('/')}/login?service={urllib.parse.quote(service_url)}"
|
||||
return redirect(cas_login_url)
|
||||
|
||||
|
||||
class SimpleCASLogoutView(View):
|
||||
"""Simple CAS logout view."""
|
||||
|
||||
def get(self, request):
|
||||
logout(request)
|
||||
|
||||
# Redirect to CAS logout
|
||||
cas_logout_url = f"{settings.CAS_SERVER_URL.rstrip('/')}/logout"
|
||||
return redirect(cas_logout_url)
|
||||
@ -5,6 +5,7 @@ description = "Opus Magnum submitter with CAS authentication"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"django>=5.2.7",
|
||||
"requests>=2.31.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user