From 95bf70c15f54184255f1d114ccc3c42d7acc10a5 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 7 Feb 2024 14:42:02 +0500 Subject: [PATCH 01/19] organization oidc login added --- src/apps/oidc_configurations/__init__.py | 0 src/apps/oidc_configurations/admin.py | 6 + src/apps/oidc_configurations/apps.py | 5 + .../migrations/0001_initial.py | 29 ++++ .../migrations/__init__.py | 0 src/apps/oidc_configurations/models.py | 14 ++ src/apps/oidc_configurations/tests.py | 3 + src/apps/oidc_configurations/urls.py | 10 ++ src/apps/oidc_configurations/views.py | 144 ++++++++++++++++++ src/apps/profiles/views.py | 6 + src/settings/base.py | 1 + src/templates/oidc/oidc_complete.html | 16 ++ src/templates/registration/login.html | 22 +++ src/urls.py | 2 + 14 files changed, 258 insertions(+) create mode 100644 src/apps/oidc_configurations/__init__.py create mode 100644 src/apps/oidc_configurations/admin.py create mode 100644 src/apps/oidc_configurations/apps.py create mode 100644 src/apps/oidc_configurations/migrations/0001_initial.py create mode 100644 src/apps/oidc_configurations/migrations/__init__.py create mode 100644 src/apps/oidc_configurations/models.py create mode 100644 src/apps/oidc_configurations/tests.py create mode 100644 src/apps/oidc_configurations/urls.py create mode 100644 src/apps/oidc_configurations/views.py create mode 100644 src/templates/oidc/oidc_complete.html diff --git a/src/apps/oidc_configurations/__init__.py b/src/apps/oidc_configurations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/oidc_configurations/admin.py b/src/apps/oidc_configurations/admin.py new file mode 100644 index 000000000..5ea6e683f --- /dev/null +++ b/src/apps/oidc_configurations/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .models import Auth_Organization + +admin.site.register(Auth_Organization) + +# Register your models here. diff --git a/src/apps/oidc_configurations/apps.py b/src/apps/oidc_configurations/apps.py new file mode 100644 index 000000000..3d757062b --- /dev/null +++ b/src/apps/oidc_configurations/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OidcConfigurationsConfig(AppConfig): + name = 'oidc_configurations' diff --git a/src/apps/oidc_configurations/migrations/0001_initial.py b/src/apps/oidc_configurations/migrations/0001_initial.py new file mode 100644 index 000000000..266976235 --- /dev/null +++ b/src/apps/oidc_configurations/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2.17 on 2024-02-04 14:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Auth_Organization', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('client_id', models.CharField(max_length=255)), + ('client_secret', models.CharField(max_length=255)), + ('authorization_url', models.URLField()), + ('token_url', models.URLField()), + ('user_info_url', models.URLField()), + ('redirect_url', models.URLField()), + ('button_bg_color', models.CharField(default='#2C3E4C', max_length=20)), + ('button_text_color', models.CharField(default='#FFFFFF', max_length=20)), + ], + ), + ] diff --git a/src/apps/oidc_configurations/migrations/__init__.py b/src/apps/oidc_configurations/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/oidc_configurations/models.py b/src/apps/oidc_configurations/models.py new file mode 100644 index 000000000..07525ca3c --- /dev/null +++ b/src/apps/oidc_configurations/models.py @@ -0,0 +1,14 @@ +# oidc_configurations/models.py +from django.db import models + + +class Auth_Organization(models.Model): + name = models.CharField(max_length=255) + client_id = models.CharField(max_length=255) + client_secret = models.CharField(max_length=255) + authorization_url = models.URLField() + token_url = models.URLField() + user_info_url = models.URLField() + redirect_url = models.URLField() + button_bg_color = models.CharField(max_length=20, default='#2C3E4C') + button_text_color = models.CharField(max_length=20, default='#FFFFFF') diff --git a/src/apps/oidc_configurations/tests.py b/src/apps/oidc_configurations/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/src/apps/oidc_configurations/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/apps/oidc_configurations/urls.py b/src/apps/oidc_configurations/urls.py new file mode 100644 index 000000000..7bfae4f99 --- /dev/null +++ b/src/apps/oidc_configurations/urls.py @@ -0,0 +1,10 @@ +# oidc_configurations/urls.py +from django.urls import path +from .views import organization_oidc_login, oidc_complete + +app_name = 'oidc_configurations' + +urlpatterns = [ + path('organization_oidc_login/', organization_oidc_login, name='organization_oidc_login'), + path('complete//', oidc_complete, name='oidc_complete'), +] diff --git a/src/apps/oidc_configurations/views.py b/src/apps/oidc_configurations/views.py new file mode 100644 index 000000000..63f59989e --- /dev/null +++ b/src/apps/oidc_configurations/views.py @@ -0,0 +1,144 @@ +# oidc_configurations/views.py +import base64 +import requests +from django.shortcuts import render, redirect, get_object_or_404 +from .models import Auth_Organization + + +def organization_oidc_login(request): + # Check if this is a post request and it contains organization_oauth2_login + if request.method == 'POST' and 'organization_oidc_login' in request.POST: + # Get auth organization id from the request + auth_organization_id = request.POST.get('organization_oidc_login') + + # Get auth organization using its id + organization = get_object_or_404(Auth_Organization, pk=auth_organization_id) + + if organization: + # Create a redirect url consisiting of + # - authorization_url + # - client_id + # - response_type + # - scope + # - redirect_uri + oidc_auth_url = ( + f"{organization.authorization_url}?" + f"client_id={organization.client_id}&" + f"response_type=code&" + "scope=openid profile email&" + f"redirect_uri={organization.redirect_url}" + ) + + # Redirect the user to the OAuth2 provider's authorization URL + return redirect(oidc_auth_url) + + # Handle other cases or render a different template if needed + return render(request, 'registration/login.html') + + +def oidc_complete(request, auth_organization_id): + + # create empty context + context = {} + + # Get error or authorization code from the query string + error = request.GET.get('error', None) + error_description = request.GET.get('error_description', None) + authorization_code = request.GET.get('code', None) + + if error: + context["error"] = error + + if error_description: + context["error_description"] = error_description + + # Token exhange process + if authorization_code: + + print(f"\n\n\n Authentication Code: {authorization_code} \n\n\n") + + try: + # STEP 1: Get auth organization using its id + organization = get_object_or_404(Auth_Organization, pk=auth_organization_id) + if organization: + + # Get access token + access_token, token_error = get_access_token(organization, authorization_code) + if token_error: + context["error"] = token_error + + print(f"\n\n\n Access Token: {access_token} \n\n\n") + + # STEP 2: Make a POST request to the user info endpoint to get user info + user_info, user_info_error = get_user_info(organization, access_token) + if user_info_error: + context["error"] = user_info_error + + print(f"\n\n\n User Info: {user_info} \n\n\n") + + print(user_info) + # STEP 3: Check in db if this user exists then login, if user is new create a new user and then login + + else: + context["error"] = "Invalid Organization ID!" + except Exception as e: + context["error"] = f"{e}" + + return render(request, 'oidc/oidc_complete.html', context) + + +def get_access_token(organization, authorization_code): + + token_url = organization.token_url + client_id = organization.client_id + client_secret = organization.client_secret + redirect_url = organization.redirect_url + + auth_header = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode("utf-8") + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Authorization": f"Basic {auth_header}", + } + data = { + "grant_type": "authorization_code", + "code": authorization_code, + "redirect_uri": redirect_url, + } + + # response = requests.post(token_url, data=data, headers=headers) + + # try: + # token_data = response.json() + # return token_data.get('access_token'), None + # except Exception as e: + # return None, e + + try: + response = requests.post(token_url, data=data, headers=headers) + response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx) + token_data = response.json() + access_token = token_data.get('access_token') + return access_token, None + except requests.exceptions.RequestException as e: + print(f"Error during token request: {e}") + return None, e + except Exception as e: + print(f"Error parsing token response: {e}") + return None, e + + +def get_user_info(organization, access_token): + + user_info_url = organization.user_info_url + + headers = { + 'Authorization': f'Bearer {access_token}', + } + + response = requests.get(user_info_url, headers=headers) + + try: + user_info = response.json() + return user_info, None + except Exception as e: + return None, e diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index 33ab6235d..fda7e124a 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -21,6 +21,7 @@ UserNotificationSerializer from .forms import SignUpForm, LoginForm from .models import User, Organization, Membership +from oidc_configurations.models import Auth_Organization from .tokens import account_activation_token @@ -172,6 +173,11 @@ def log_in(request): else: context['form'] = form + # Fetch auth_organizations from the database + auth_organizations = Auth_Organization.objects.all() + if auth_organizations: + context['auth_organizations'] = auth_organizations + if not context.get('form'): context['form'] = LoginForm() return render(request, 'registration/login.html', context) diff --git a/src/settings/base.py b/src/settings/base.py index d5047db82..513ae7918 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -60,6 +60,7 @@ 'health', 'forums', 'announcements', + 'oidc_configurations', ) INSTALLED_APPS = THIRD_PARTY_APPS + OUR_APPS diff --git a/src/templates/oidc/oidc_complete.html b/src/templates/oidc/oidc_complete.html new file mode 100644 index 000000000..293d52823 --- /dev/null +++ b/src/templates/oidc/oidc_complete.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+ {% if error %} +

OIDC Error

+
+

{{ error }}

+ {% if error_description %} +

{{ error_description }}

+ {% endif %} +
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/src/templates/registration/login.html b/src/templates/registration/login.html index 55d8f10cf..99bf8033c 100644 --- a/src/templates/registration/login.html +++ b/src/templates/registration/login.html @@ -58,5 +58,27 @@

+ + + {% if auth_organizations %} +
+

Organization Login

+ {% for organization in auth_organizations %} +
+ {% csrf_token %} + +
+ {% endfor %} + +
+ {% endif %} {% endblock %} \ No newline at end of file diff --git a/src/urls.py b/src/urls.py index ea610fd46..a74ff4b5b 100644 --- a/src/urls.py +++ b/src/urls.py @@ -30,6 +30,8 @@ path('accounts/', include('profiles.urls_accounts')), path('admin/', admin.site.urls), path('social/', include('social_django.urls', namespace='social')), + path('oidc/', include('oidc_configurations.urls')), + ] From 93a8af6d8da3eaef615bff1560a5c39981d372f9 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Wed, 7 Feb 2024 14:48:01 +0500 Subject: [PATCH 02/19] unused test file removed --- src/apps/oidc_configurations/tests.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 src/apps/oidc_configurations/tests.py diff --git a/src/apps/oidc_configurations/tests.py b/src/apps/oidc_configurations/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/src/apps/oidc_configurations/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From 4004cec2f146e7c2b9a7fcb9deec4e5ceb23581f Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Thu, 8 Feb 2024 14:34:51 +0500 Subject: [PATCH 03/19] http client --- src/apps/oidc_configurations/views.py | 40 ++++++++++++++++++++------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/apps/oidc_configurations/views.py b/src/apps/oidc_configurations/views.py index 63f59989e..c6a302cfa 100644 --- a/src/apps/oidc_configurations/views.py +++ b/src/apps/oidc_configurations/views.py @@ -1,6 +1,8 @@ # oidc_configurations/views.py import base64 +import http.client import requests +from urllib.parse import urlparse from django.shortcuts import render, redirect, get_object_or_404 from .models import Auth_Organization @@ -19,17 +21,15 @@ def organization_oidc_login(request): # - authorization_url # - client_id # - response_type - # - scope # - redirect_uri oidc_auth_url = ( f"{organization.authorization_url}?" f"client_id={organization.client_id}&" f"response_type=code&" - "scope=openid profile email&" f"redirect_uri={organization.redirect_url}" ) - # Redirect the user to the OAuth2 provider's authorization URL + # Redirect the user to the OIDC provider's authorization URL return redirect(oidc_auth_url) # Handle other cases or render a different template if needed @@ -104,6 +104,9 @@ def get_access_token(organization, authorization_code): "code": authorization_code, "redirect_uri": redirect_url, } + print("token url: ", token_url) + print("data: ", data) + print("header: ", headers) # response = requests.post(token_url, data=data, headers=headers) @@ -113,17 +116,34 @@ def get_access_token(organization, authorization_code): # except Exception as e: # return None, e + + # try: + # response = requests.request("POST", token_url, data=data, headers=headers) + # response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx) + # token_data = response.json() + # access_token = token_data.get('access_token') + # return access_token, None + # except requests.exceptions.RequestException as e: + # print(f"Error during token request: {e}") + # return None, e + # except Exception as e: + # print(f"Error parsing token response: {e}") + # return None, e + try: - response = requests.post(token_url, data=data, headers=headers) - response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx) - token_data = response.json() + parsed_url = urlparse(token_url) + conn = http.client.HTTPConnection(parsed_url.hostname, parsed_url.port) + conn.request("POST", parsed_url.path, data, headers) + response = conn.getresponse() + token_data = response.read().decode("utf-8") access_token = token_data.get('access_token') + conn.close() + print("Response:", token_data) + # Parse token_data if needed + # access_token = ... return access_token, None - except requests.exceptions.RequestException as e: - print(f"Error during token request: {e}") - return None, e except Exception as e: - print(f"Error parsing token response: {e}") + print(f"Error during token request: {e}") return None, e From 0c2e12a54c130a562f66327ba68d630f6d02acd3 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 20 Feb 2024 14:28:15 +0500 Subject: [PATCH 04/19] some changes --- docker-compose.yml | 17 +++++++++++++++++ src/apps/oidc_configurations/views.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0b5c2c6ee..46516aa9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -232,3 +232,20 @@ services: options: max-size: "20k" max-file: "10" + + + #----------------------------------------------- + # OIDC + #----------------------------------------------- + + oidc: + image: oidc_server + command: bash -c "cd /app/ && python manage.py runserver 0.0.0.0:9100" + ports: + - 9100:9100 + stdin_open: true + tty: true + logging: + options: + max-size: "20k" + max-file: "10" \ No newline at end of file diff --git a/src/apps/oidc_configurations/views.py b/src/apps/oidc_configurations/views.py index c6a302cfa..908b40987 100644 --- a/src/apps/oidc_configurations/views.py +++ b/src/apps/oidc_configurations/views.py @@ -25,7 +25,8 @@ def organization_oidc_login(request): oidc_auth_url = ( f"{organization.authorization_url}?" f"client_id={organization.client_id}&" - f"response_type=code&" + "response_type=code&" + "scope=openid profile email&" f"redirect_uri={organization.redirect_url}" ) From f6e5a8b4e5ff4df27e8d6bb7dfeff9a7ce9b3558 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Tue, 20 Feb 2024 22:03:19 +0500 Subject: [PATCH 05/19] oidc login and signup added --- docker-compose.yml | 19 +-- src/apps/oidc_configurations/views.py | 144 +++++++++++------- .../migrations/0013_auto_20240220_1526.py | 25 +++ src/apps/profiles/models.py | 5 + 4 files changed, 124 insertions(+), 69 deletions(-) create mode 100644 src/apps/profiles/migrations/0013_auto_20240220_1526.py diff --git a/docker-compose.yml b/docker-compose.yml index 46516aa9f..08e7a5c84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -231,21 +231,4 @@ services: logging: options: max-size: "20k" - max-file: "10" - - - #----------------------------------------------- - # OIDC - #----------------------------------------------- - - oidc: - image: oidc_server - command: bash -c "cd /app/ && python manage.py runserver 0.0.0.0:9100" - ports: - - 9100:9100 - stdin_open: true - tty: true - logging: - options: - max-size: "20k" - max-file: "10" \ No newline at end of file + max-file: "10" \ No newline at end of file diff --git a/src/apps/oidc_configurations/views.py b/src/apps/oidc_configurations/views.py index 908b40987..e0af3a67e 100644 --- a/src/apps/oidc_configurations/views.py +++ b/src/apps/oidc_configurations/views.py @@ -5,6 +5,12 @@ from urllib.parse import urlparse from django.shortcuts import render, redirect, get_object_or_404 from .models import Auth_Organization +from django.contrib.auth import get_user_model, authenticate, login +import re +User = get_user_model() +from django.contrib.auth.backends import ModelBackend + +BACKEND = 'django.contrib.auth.backends.ModelBackend' def organization_oidc_login(request): @@ -56,30 +62,43 @@ def oidc_complete(request, auth_organization_id): # Token exhange process if authorization_code: - print(f"\n\n\n Authentication Code: {authorization_code} \n\n\n") - try: # STEP 1: Get auth organization using its id organization = get_object_or_404(Auth_Organization, pk=auth_organization_id) + if organization: - # Get access token + # STEP 2: Get access token access_token, token_error = get_access_token(organization, authorization_code) + if token_error: context["error"] = token_error - - print(f"\n\n\n Access Token: {access_token} \n\n\n") - - # STEP 2: Make a POST request to the user info endpoint to get user info - user_info, user_info_error = get_user_info(organization, access_token) - if user_info_error: - context["error"] = user_info_error - - print(f"\n\n\n User Info: {user_info} \n\n\n") - - print(user_info) - # STEP 3: Check in db if this user exists then login, if user is new create a new user and then login - + else: + # STEP 3: Get user info + user_info, user_info_error = get_user_info(organization, access_token) + if user_info_error: + context["error"] = user_info_error + else: + + # get email and nickname (username) of the user + user_email = user_info.get("email", None) + user_nickname = user_info.get("nickname", None) + if user_email: + # get user with this email + user = get_user_by_email(user_email) + # STEP 4: Check if user exists and user is created using oidc and oidc orgnaization matches this one + if user: + if user.is_created_using_oidc and user.oidc_organization.id == auth_organization_id: + login(request, user, backend=BACKEND) + # Redirect the user home page + return redirect('pages:home') + else: + context["error"] = "User account cannot be authenticated using this Organization." + else: + register_and_authenticate_user(request, user_email, user_nickname, organization) + + else: + context["error"] = "Unable to extract email from user info! Please contact platform" else: context["error"] = "Invalid Organization ID!" except Exception as e: @@ -105,47 +124,19 @@ def get_access_token(organization, authorization_code): "code": authorization_code, "redirect_uri": redirect_url, } - print("token url: ", token_url) - print("data: ", data) - print("header: ", headers) - - # response = requests.post(token_url, data=data, headers=headers) - - # try: - # token_data = response.json() - # return token_data.get('access_token'), None - # except Exception as e: - # return None, e - - - # try: - # response = requests.request("POST", token_url, data=data, headers=headers) - # response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx) - # token_data = response.json() - # access_token = token_data.get('access_token') - # return access_token, None - # except requests.exceptions.RequestException as e: - # print(f"Error during token request: {e}") - # return None, e - # except Exception as e: - # print(f"Error parsing token response: {e}") - # return None, e try: - parsed_url = urlparse(token_url) - conn = http.client.HTTPConnection(parsed_url.hostname, parsed_url.port) - conn.request("POST", parsed_url.path, data, headers) - response = conn.getresponse() - token_data = response.read().decode("utf-8") + response = requests.request("POST", token_url, data=data, headers=headers) + response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx) + token_data = response.json() access_token = token_data.get('access_token') - conn.close() - print("Response:", token_data) - # Parse token_data if needed - # access_token = ... return access_token, None - except Exception as e: + except requests.exceptions.RequestException as e: print(f"Error during token request: {e}") return None, e + except Exception as e: + print(f"Error parsing token response: {e}") + return None, e def get_user_info(organization, access_token): @@ -163,3 +154,54 @@ def get_user_info(organization, access_token): return user_info, None except Exception as e: return None, e + + +def register_and_authenticate_user(request, user_email, user_nickname, organization): + + if not user_nickname: + username = re.sub(r'[^a-zA-Z0-9]', '', user_email.split('@')[0]) + else: + username = user_nickname + + # Ensure the username is unique + username = create_unique_username(username) + + # Create a new user + user = User.objects.create( + username=username, + email=user_email, + is_created_using_oidc=True, + oidc_organization=organization, + ) + + if user: + # login user + login(request, user, backend=BACKEND) + # Redirect to the home page + return redirect('pages:home') + else: + # Handle authentication failure + return redirect('accounts:login') + + +def create_unique_username(username): + # Check if the username already exists + if User.objects.filter(username=username).exists(): + # If the username already exists, modify it to make it unique + suffix = 1 + new_username = f"{username}_{suffix}" + while User.objects.filter(username=new_username).exists(): + suffix += 1 + new_username = f"{username}_{suffix}" + return new_username + else: + # If the username doesn't exist, use it as is + return username + + +def get_user_by_email(email): + try: + user = User.objects.get(email=email) + return user + except User.DoesNotExist: + return None diff --git a/src/apps/profiles/migrations/0013_auto_20240220_1526.py b/src/apps/profiles/migrations/0013_auto_20240220_1526.py new file mode 100644 index 000000000..57f3aef69 --- /dev/null +++ b/src/apps/profiles/migrations/0013_auto_20240220_1526.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.17 on 2024-02-20 15:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('oidc_configurations', '0001_initial'), + ('profiles', '0012_user_quota'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_created_using_oidc', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='user', + name='oidc_organization', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='authorized_users', to='oidc_configurations.Auth_Organization'), + ), + ] diff --git a/src/apps/profiles/models.py b/src/apps/profiles/models.py index 518230f92..b150e54e4 100644 --- a/src/apps/profiles/models.py +++ b/src/apps/profiles/models.py @@ -16,6 +16,7 @@ When, DecimalField, ) +from oidc_configurations.models import Auth_Organization PROFILE_DATA_BLACKLIST = [ 'password', @@ -72,6 +73,10 @@ class User(ChaHubSaveMixin, AbstractBaseUser, PermissionsMixin): is_staff = models.BooleanField(default=False) quota = models.BigIntegerField(default=settings.DEFAULT_USER_QUOTA, null=False) + # Fields for OIDC authentication + is_created_using_oidc = models.BooleanField(default=False) + oidc_organization = models.ForeignKey(Auth_Organization, null=True, blank=True, on_delete=models.SET_NULL, related_name="authorized_users") + # Notifications organizer_direct_message_updates = models.BooleanField(default=True) allow_forum_notifications = models.BooleanField(default=True) From 9935f2c2f1d6547967193d916eb3aa7aefbd778e Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Mon, 4 Mar 2024 11:18:04 +0500 Subject: [PATCH 06/19] oidc flow completed --- .../oidc_configurations/migrations/0001_initial.py | 10 +++++----- src/apps/oidc_configurations/models.py | 8 ++++---- src/apps/oidc_configurations/views.py | 13 +++++++------ ..._20240220_1526.py => 0013_auto_20240304_0616.py} | 2 +- 4 files changed, 17 insertions(+), 16 deletions(-) rename src/apps/profiles/migrations/{0013_auto_20240220_1526.py => 0013_auto_20240304_0616.py} (93%) diff --git a/src/apps/oidc_configurations/migrations/0001_initial.py b/src/apps/oidc_configurations/migrations/0001_initial.py index 266976235..085e64983 100644 --- a/src/apps/oidc_configurations/migrations/0001_initial.py +++ b/src/apps/oidc_configurations/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.17 on 2024-02-04 14:21 +# Generated by Django 2.2.17 on 2024-03-04 06:16 from django.db import migrations, models @@ -18,10 +18,10 @@ class Migration(migrations.Migration): ('name', models.CharField(max_length=255)), ('client_id', models.CharField(max_length=255)), ('client_secret', models.CharField(max_length=255)), - ('authorization_url', models.URLField()), - ('token_url', models.URLField()), - ('user_info_url', models.URLField()), - ('redirect_url', models.URLField()), + ('authorization_url', models.CharField(max_length=255)), + ('token_url', models.CharField(max_length=255)), + ('user_info_url', models.CharField(max_length=255)), + ('redirect_url', models.CharField(max_length=255)), ('button_bg_color', models.CharField(default='#2C3E4C', max_length=20)), ('button_text_color', models.CharField(default='#FFFFFF', max_length=20)), ], diff --git a/src/apps/oidc_configurations/models.py b/src/apps/oidc_configurations/models.py index 07525ca3c..9e2b0c66c 100644 --- a/src/apps/oidc_configurations/models.py +++ b/src/apps/oidc_configurations/models.py @@ -6,9 +6,9 @@ class Auth_Organization(models.Model): name = models.CharField(max_length=255) client_id = models.CharField(max_length=255) client_secret = models.CharField(max_length=255) - authorization_url = models.URLField() - token_url = models.URLField() - user_info_url = models.URLField() - redirect_url = models.URLField() + authorization_url = models.CharField(max_length=255) + token_url = models.CharField(max_length=255) + user_info_url = models.CharField(max_length=255) + redirect_url = models.CharField(max_length=255) button_bg_color = models.CharField(max_length=20, default='#2C3E4C') button_text_color = models.CharField(max_length=20, default='#FFFFFF') diff --git a/src/apps/oidc_configurations/views.py b/src/apps/oidc_configurations/views.py index e0af3a67e..0e4724743 100644 --- a/src/apps/oidc_configurations/views.py +++ b/src/apps/oidc_configurations/views.py @@ -1,14 +1,12 @@ # oidc_configurations/views.py import base64 -import http.client import requests -from urllib.parse import urlparse from django.shortcuts import render, redirect, get_object_or_404 from .models import Auth_Organization -from django.contrib.auth import get_user_model, authenticate, login +from django.contrib.auth import get_user_model, login import re + User = get_user_model() -from django.contrib.auth.backends import ModelBackend BACKEND = 'django.contrib.auth.backends.ModelBackend' @@ -95,7 +93,7 @@ def oidc_complete(request, auth_organization_id): else: context["error"] = "User account cannot be authenticated using this Organization." else: - register_and_authenticate_user(request, user_email, user_nickname, organization) + return register_and_authenticate_user(request, user_email, user_nickname, organization) else: context["error"] = "Unable to extract email from user info! Please contact platform" @@ -165,6 +163,8 @@ def register_and_authenticate_user(request, user_email, user_nickname, organizat # Ensure the username is unique username = create_unique_username(username) + username = "ihsaan8" + user_email = "ihsaan8+test@gmail.com" # Create a new user user = User.objects.create( @@ -179,8 +179,9 @@ def register_and_authenticate_user(request, user_email, user_nickname, organizat login(request, user, backend=BACKEND) # Redirect to the home page return redirect('pages:home') + else: - # Handle authentication failure + # Handle authentication failure i.e. go back to login return redirect('accounts:login') diff --git a/src/apps/profiles/migrations/0013_auto_20240220_1526.py b/src/apps/profiles/migrations/0013_auto_20240304_0616.py similarity index 93% rename from src/apps/profiles/migrations/0013_auto_20240220_1526.py rename to src/apps/profiles/migrations/0013_auto_20240304_0616.py index 57f3aef69..121ca477c 100644 --- a/src/apps/profiles/migrations/0013_auto_20240220_1526.py +++ b/src/apps/profiles/migrations/0013_auto_20240304_0616.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.17 on 2024-02-20 15:26 +# Generated by Django 2.2.17 on 2024-03-04 06:16 from django.db import migrations, models import django.db.models.deletion From df0ceab72e38911bf0c235a5b7111467d4e3c58f Mon Sep 17 00:00:00 2001 From: Chris Harris Date: Tue, 5 Mar 2024 19:45:21 +0000 Subject: [PATCH 07/19] Fix revoking tasks on custom queues As custom queues have a different vhost we need to use an appropriate celery configuration with that vhost including in the broker URL. --- src/apps/competitions/models.py | 9 +++++++-- src/celery_config.py | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/apps/competitions/models.py b/src/apps/competitions/models.py index 03d8b65fc..924c10cec 100644 --- a/src/apps/competitions/models.py +++ b/src/apps/competitions/models.py @@ -12,7 +12,7 @@ from django.urls import reverse from django.utils.timezone import now -from celery_config import app +from celery_config import app, app_for_vhost from chahub.models import ChaHubSaveMixin from leaderboards.models import SubmissionScore from profiles.models import User, Organization @@ -644,7 +644,12 @@ def cancel(self, status=CANCELLED): if self.has_children: for sub in self.children.all(): sub.cancel(status=status) - app.control.revoke(self.celery_task_id, terminate=True) + celery_app = app + # If a custom queue is set, we need to fetch the appropriate celery app + if self.phase.competition.queue: + celery_app = app_for_vhost(str(self.phase.competition.queue.vhost)) + + celery_app.control.revoke(self.celery_task_id, terminate=True) self.status = status self.save() return True diff --git a/src/celery_config.py b/src/celery_config.py index 76c52a7a4..e3b7e141e 100644 --- a/src/celery_config.py +++ b/src/celery_config.py @@ -1,5 +1,8 @@ from celery import Celery from kombu import Queue, Exchange +from django.conf import settings +import urllib.parse +import copy app = Celery() @@ -11,3 +14,25 @@ # Mostly defining queue here so we can set x-max-priority Queue('compute-worker', Exchange('compute-worker'), routing_key='compute-worker', queue_arguments={'x-max-priority': 10}), ] + +_vhost_apps = {} +# Function to get the app for a vhost +def app_for_vhost(vhost): + if vhost not in _vhost_apps: + # Take the CELERY_BROKER_URL and replace the vhost with the vhhost for this queue + broker_url = settings.CELERY_BROKER_URL + # This is require to work around https://bugs.python.org/issue18828 + scheme = urllib.parse.urlparse(broker_url).scheme + urllib.parse.uses_relative.append(scheme) + urllib.parse.uses_netloc.append(scheme) + broker_url = urllib.parse.urljoin(broker_url, vhost) + + vhost_app = Celery() + # Copy the settings so we can modify the broker url to include the vhost + django_settings = copy.copy(settings) + django_settings.CELERY_BROKER_URL = broker_url + vhost_app.config_from_object(django_settings, namespace='CELERY') + vhost_app.task_queues = app.conf.task_queues + _vhost_apps[vhost] = vhost_app + + return _vhost_apps[vhost] \ No newline at end of file From 48cb7f88b764121342182daf56a940acfec31d90 Mon Sep 17 00:00:00 2001 From: Chris Harris Date: Tue, 5 Mar 2024 08:06:50 -0800 Subject: [PATCH 08/19] Prevent LimitOverrunError with large output lines If a submission writes a output line larger than the stream buffer size ( default 64k ) a LimitOverrunError will be raise. Rather than using readline(...) use readutil(....) and in the case of a overrun just return the current buffer, the rest of the line will be returned with the next read. Signed-off-by: Chris Harris --- compute_worker/compute_worker.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/compute_worker/compute_worker.py b/compute_worker/compute_worker.py index 491b84e5f..24e7d25bc 100644 --- a/compute_worker/compute_worker.py +++ b/compute_worker/compute_worker.py @@ -507,12 +507,25 @@ async def _run_container_engine_cmd(self, engine_cmd, kind): websocket = await websockets.connect(self.websocket_url) websocket_errors = (socket.gaierror, websockets.WebSocketException, websockets.ConnectionClosedError, ConnectionRefusedError) + # Function to read a line, if the line is larger than the buffer size we will + # return the buffer so we can continue reading until we get a newline, rather + # than getting a LimitOverrunError + async def _readline_or_chunk(stream): + try: + return await stream.readuntil(b"\n") + except asyncio.exceptions.IncompleteReadError as e: + # Just return what has been read so far + return e.partial + except asyncio.exceptions.LimitOverrunError as e: + # If we get a LimitOverrunError, we will return the buffer so we can continue reading + return await stream.read(e.consumed) + while any(v["continue"] for k, v in self.logs[kind].items() if k in ['stdout', 'stderr']): try: logs = [self.logs[kind][key] for key in ('stdout', 'stderr')] for value in logs: try: - out = await asyncio.wait_for(value["stream"].readline(), timeout=.1) + out = await asyncio.wait_for(_readline_or_chunk(value["stream"]), timeout=.1) if out: value["data"] += out print("WS: " + str(out)) From 0fe72d5bebbf82a896d63ad51cf645d113ea5d30 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Sun, 17 Mar 2024 00:31:35 +0500 Subject: [PATCH 09/19] terms and condition check added --- src/apps/oidc_configurations/views.py | 11 +++-------- src/templates/registration/login.html | 4 ++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/apps/oidc_configurations/views.py b/src/apps/oidc_configurations/views.py index 0e4724743..6b04be8ff 100644 --- a/src/apps/oidc_configurations/views.py +++ b/src/apps/oidc_configurations/views.py @@ -86,12 +86,9 @@ def oidc_complete(request, auth_organization_id): user = get_user_by_email(user_email) # STEP 4: Check if user exists and user is created using oidc and oidc orgnaization matches this one if user: - if user.is_created_using_oidc and user.oidc_organization.id == auth_organization_id: - login(request, user, backend=BACKEND) - # Redirect the user home page - return redirect('pages:home') - else: - context["error"] = "User account cannot be authenticated using this Organization." + login(request, user, backend=BACKEND) + # Redirect the user home page + return redirect('pages:home') else: return register_and_authenticate_user(request, user_email, user_nickname, organization) @@ -163,8 +160,6 @@ def register_and_authenticate_user(request, user_email, user_nickname, organizat # Ensure the username is unique username = create_unique_username(username) - username = "ihsaan8" - user_email = "ihsaan8+test@gmail.com" # Create a new user user = User.objects.create( diff --git a/src/templates/registration/login.html b/src/templates/registration/login.html index 99bf8033c..6664f41df 100644 --- a/src/templates/registration/login.html +++ b/src/templates/registration/login.html @@ -66,6 +66,10 @@

Organization Login

{% for organization in auth_organizations %}
{% csrf_token %} +
+ + +

margin-bottom:5px;"> Login with {{ organization.name }} - - {% endfor %} + {% endfor %} + {% endif %} From 1b4dde2f272cd906895791dbccda622dec3eca1d Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Thu, 11 Apr 2024 15:40:51 +0500 Subject: [PATCH 11/19] removed sandbox property from iframe to allow links in the iframe --- src/static/riot/competitions/detail/submission_modal.tag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/riot/competitions/detail/submission_modal.tag b/src/static/riot/competitions/detail/submission_modal.tag index 7bb5eb840..55cb5dde4 100644 --- a/src/static/riot/competitions/detail/submission_modal.tag +++ b/src/static/riot/competitions/detail/submission_modal.tag @@ -136,7 +136,7 @@
Save
From 362f443ec2b257ee9d20543421a9224d991b0271 Mon Sep 17 00:00:00 2001 From: Ihsan Ullah Date: Thu, 11 Apr 2024 20:16:17 +0500 Subject: [PATCH 12/19] Detailed results title removed --- src/static/riot/competitions/detail/_detailed_results.tag | 1 - 1 file changed, 1 deletion(-) diff --git a/src/static/riot/competitions/detail/_detailed_results.tag b/src/static/riot/competitions/detail/_detailed_results.tag index 0136e6689..bb2f87e30 100644 --- a/src/static/riot/competitions/detail/_detailed_results.tag +++ b/src/static/riot/competitions/detail/_detailed_results.tag @@ -1,5 +1,4 @@ -

Detailed Results