From 224f38bf1fa9747ecb6d7218f1539d8045192e63 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Wed, 11 Feb 2026 17:18:13 +0200 Subject: [PATCH 01/43] allow angular url to be in next_url --- framework/auth/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/framework/auth/views.py b/framework/auth/views.py index 0338654fac3..973ebded1ab 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -39,6 +39,9 @@ from osf import features +ANGULAR_URL = 'localhost:4200' + + @block_bing_preview @collect_auth def reset_password_get(auth, uid=None, token=None): @@ -1209,6 +1212,9 @@ def validate_next_url(next_url): # disable external domain using `//`: the browser allows `//` as a shortcut for non-protocol specific requests # like http:// or https:// depending on the use of SSL on the page already. + if ANGULAR_URL in next_url: + return True + if next_url.startswith('//'): return False From 7b67b1271bb1a1d25f3f99e8b1874a3b474c69b7 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Thu, 12 Feb 2026 13:13:26 +0200 Subject: [PATCH 02/43] renamed constant --- framework/auth/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/auth/views.py b/framework/auth/views.py index 973ebded1ab..3965ac6adcf 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -39,7 +39,7 @@ from osf import features -ANGULAR_URL = 'localhost:4200' +LOCAL_ANGULAR_URL = 'localhost:4200' @block_bing_preview @@ -1212,7 +1212,7 @@ def validate_next_url(next_url): # disable external domain using `//`: the browser allows `//` as a shortcut for non-protocol specific requests # like http:// or https:// depending on the use of SSL on the page already. - if ANGULAR_URL in next_url: + if LOCAL_ANGULAR_URL in next_url: return True if next_url.startswith('//'): From 235dac643e976d690c5709b6410c4b237a90e9f1 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Fri, 13 Feb 2026 14:45:44 +0200 Subject: [PATCH 03/43] added tests and debug mode --- framework/auth/views.py | 5 +---- tests/test_auth_views.py | 11 +++++++++++ website/settings/defaults.py | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/framework/auth/views.py b/framework/auth/views.py index 3965ac6adcf..ecf6540e5e6 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -39,9 +39,6 @@ from osf import features -LOCAL_ANGULAR_URL = 'localhost:4200' - - @block_bing_preview @collect_auth def reset_password_get(auth, uid=None, token=None): @@ -1212,7 +1209,7 @@ def validate_next_url(next_url): # disable external domain using `//`: the browser allows `//` as a shortcut for non-protocol specific requests # like http:// or https:// depending on the use of SSL on the page already. - if LOCAL_ANGULAR_URL in next_url: + if settings.LOCAL_ANGULAR_URL in next_url and settings.DEBUG_MODE: return True if next_url.startswith('//'): diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py index 8e8cc5fafb1..aa248ccdaaf 100644 --- a/tests/test_auth_views.py +++ b/tests/test_auth_views.py @@ -584,6 +584,17 @@ def test_next_url_login_with_auth(self): assert data.get('status_code') == http_status.HTTP_302_FOUND assert data.get('next_url') == self.next_url + def test_next_url_angular_login_with_auth(self): + data = login_and_register_handler(self.auth, next_url=settings.LOCAL_ANGULAR_URL) + assert data.get('status_code') == http_status.HTTP_302_FOUND + assert data.get('next_url') == settings.LOCAL_ANGULAR_URL + + def test_next_url_angular_login_without_auth(self): + request.url = web_url_for('auth_login', next=settings.LOCAL_ANGULAR_URL, _absolute=True) + data = login_and_register_handler(self.no_auth, next_url=settings.LOCAL_ANGULAR_URL) + assert data.get('status_code') == http_status.HTTP_302_FOUND + assert data.get('next_url') == cas.get_login_url(request.url) + def test_next_url_login_without_auth(self): # login: user without auth request.url = web_url_for('auth_login', next=self.next_url, _absolute=True) diff --git a/website/settings/defaults.py b/website/settings/defaults.py index d09e583c181..40ecad5f0c2 100644 --- a/website/settings/defaults.py +++ b/website/settings/defaults.py @@ -90,6 +90,7 @@ def parent_dir(path): INTERNAL_DOMAIN = DOMAIN API_DOMAIN = PROTOCOL + 'localhost:8000/' RESET_PASSWORD_URL = PROTOCOL + 'localhost:5000/resetpassword/' # TODO set angular reset password url +LOCAL_ANGULAR_URL = 'localhost:4200' PREPRINT_PROVIDER_DOMAINS = { 'enabled': False, From 23dd9268f625b3f42d30e86efc97081c0ad9d4a9 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Wed, 11 Feb 2026 17:18:13 +0200 Subject: [PATCH 04/43] angular url --- framework/auth/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/framework/auth/views.py b/framework/auth/views.py index ecf6540e5e6..6ffc327f8bc 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -39,6 +39,9 @@ from osf import features +ANGULAR_URL = 'localhost:4200' + + @block_bing_preview @collect_auth def reset_password_get(auth, uid=None, token=None): From cb141e15afa5c3287566d9b5a477de0fd650e986 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Thu, 12 Feb 2026 13:13:26 +0200 Subject: [PATCH 05/43] angular url --- framework/auth/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/auth/views.py b/framework/auth/views.py index 6ffc327f8bc..afadbe44346 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -39,7 +39,7 @@ from osf import features -ANGULAR_URL = 'localhost:4200' +LOCAL_ANGULAR_URL = 'localhost:4200' @block_bing_preview From b16005c3b4e63c5bbe43edb86d18ad3874f1c0d4 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Fri, 13 Feb 2026 14:45:44 +0200 Subject: [PATCH 06/43] added tests and debug mode --- framework/auth/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/framework/auth/views.py b/framework/auth/views.py index afadbe44346..ecf6540e5e6 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -39,9 +39,6 @@ from osf import features -LOCAL_ANGULAR_URL = 'localhost:4200' - - @block_bing_preview @collect_auth def reset_password_get(auth, uid=None, token=None): From d48f9ae9cbcc74b22b71c0675d3b35f243c72988 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Mon, 16 Feb 2026 17:50:19 +0200 Subject: [PATCH 07/43] edited comments --- framework/auth/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/auth/views.py b/framework/auth/views.py index ecf6540e5e6..90904fa0c16 100644 --- a/framework/auth/views.py +++ b/framework/auth/views.py @@ -1207,11 +1207,12 @@ def validate_next_url(next_url): :return: True if valid, False otherwise """ - # disable external domain using `//`: the browser allows `//` as a shortcut for non-protocol specific requests - # like http:// or https:// depending on the use of SSL on the page already. + # allow redirection to angular locally if settings.LOCAL_ANGULAR_URL in next_url and settings.DEBUG_MODE: return True + # disable external domain using `//`: the browser allows `//` as a shortcut for non-protocol specific requests + # like http:// or https:// depending on the use of SSL on the page already. if next_url.startswith('//'): return False From 261a680855e5b8b398243c6b34bdbef26fff4b92 Mon Sep 17 00:00:00 2001 From: Ihor Sokhan Date: Mon, 16 Feb 2026 16:43:15 +0200 Subject: [PATCH 08/43] added sso_in_progress field to Institution model --- .../0036_institution_sso_in_progress.py | 18 ++++++++++++++++++ osf/models/institution.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 osf/migrations/0036_institution_sso_in_progress.py diff --git a/osf/migrations/0036_institution_sso_in_progress.py b/osf/migrations/0036_institution_sso_in_progress.py new file mode 100644 index 00000000000..784188c4806 --- /dev/null +++ b/osf/migrations/0036_institution_sso_in_progress.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.17 on 2026-02-16 14:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0035_merge_20251215_1451'), + ] + + operations = [ + migrations.AddField( + model_name='institution', + name='sso_in_progress', + field=models.BooleanField(default=False), + ), + ] diff --git a/osf/models/institution.py b/osf/models/institution.py index 39d57637da5..fa2c42aa552 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -125,6 +125,7 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian default='', help_text='Full URL where institutional admins can access archived metrics reports.', ) + sso_in_progress = models.BooleanField(default=False) class Meta: # custom permissions for use in the OSF Admin App From 5d474ef48d19d7020c742a4521cde68628dc23f9 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Fri, 13 Feb 2026 17:30:01 +0200 Subject: [PATCH 09/43] Add tests --- tests/test_auth_views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py index aa248ccdaaf..a30a43a5299 100644 --- a/tests/test_auth_views.py +++ b/tests/test_auth_views.py @@ -838,6 +838,22 @@ def test_logout_with_no_parameter(self): assert resp.status_code == http_status.HTTP_302_FOUND assert cas.get_logout_url(self.goodbye_url) == resp.headers['Location'] + @mock.patch('framework.auth.views.settings.LOCAL_ANGULAR_URL', 'http://localhost:4200') + def test_logout_with_angular_next_url_logged_in(self): + angular_url = 'http://localhost:4200/' + logout_url = web_url_for('auth_logout', _absolute=True, next=angular_url) + resp = self.app.get(logout_url, auth=self.auth_user.auth) + assert resp.status_code == http_status.HTTP_302_FOUND + assert cas.get_logout_url(logout_url) == resp.headers['Location'] + + @mock.patch('framework.auth.views.settings.LOCAL_ANGULAR_URL', 'http://localhost:4200') + def test_logout_with_angular_next_url_logged_out(self): + angular_url = 'http://localhost:4200/' + logout_url = web_url_for('auth_logout', _absolute=True, next=angular_url) + resp = self.app.get(logout_url, auth=None) + assert resp.status_code == http_status.HTTP_302_FOUND + assert angular_url == resp.headers['Location'] + class TestResetPassword(OsfTestCase): From 179bed01446b76b7e6ec351fa37d4fd300802786 Mon Sep 17 00:00:00 2001 From: Ostap Zherebetskyi Date: Thu, 19 Mar 2026 17:28:29 +0200 Subject: [PATCH 10/43] Add SSO availability field and update institution reactivation logic --- admin_tests/institutions/test_views.py | 6 +++-- .../0036_institution_sso_in_progress.py | 18 -------------- .../0038_institution_sso_availability.py | 18 ++++++++++++++ osf/models/institution.py | 24 ++++++++++++++++++- osf_tests/test_institution.py | 10 ++++++++ 5 files changed, 55 insertions(+), 21 deletions(-) delete mode 100644 osf/migrations/0036_institution_sso_in_progress.py create mode 100644 osf/migrations/0038_institution_sso_availability.py diff --git a/admin_tests/institutions/test_views.py b/admin_tests/institutions/test_views.py index 13cb1456ab9..c3c8a3c3fab 100644 --- a/admin_tests/institutions/test_views.py +++ b/admin_tests/institutions/test_views.py @@ -139,7 +139,8 @@ def test_institution_form(self): 'name': 'New Name', 'logo_name': 'awesome_logo.png', 'domains': 'http://kris.biz/, http://www.little.biz/', - '_id': 'newawesomeprov' + '_id': 'newawesomeprov', + 'sso_availability': 'Public', } form = InstitutionForm(data=new_data) assert form.is_valid() @@ -214,7 +215,8 @@ def test_monthly_reporter_called_on_create(self, mock_monthly_reporter_do): 'email_domains': FakeList('domain_name', n=1), 'orcid_record_verified_source': '', 'delegation_protocol': '', - 'institutional_request_access_enabled': False + 'institutional_request_access_enabled': False, + 'sso_availability': 'Public', } form = InstitutionForm(data=data) assert form.is_valid() diff --git a/osf/migrations/0036_institution_sso_in_progress.py b/osf/migrations/0036_institution_sso_in_progress.py deleted file mode 100644 index 784188c4806..00000000000 --- a/osf/migrations/0036_institution_sso_in_progress.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.17 on 2026-02-16 14:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('osf', '0035_merge_20251215_1451'), - ] - - operations = [ - migrations.AddField( - model_name='institution', - name='sso_in_progress', - field=models.BooleanField(default=False), - ), - ] diff --git a/osf/migrations/0038_institution_sso_availability.py b/osf/migrations/0038_institution_sso_availability.py new file mode 100644 index 00000000000..c4be5de4001 --- /dev/null +++ b/osf/migrations/0038_institution_sso_availability.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2026-03-13 11:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0037_notification_refactor_post_release'), + ] + + operations = [ + migrations.AddField( + model_name='institution', + name='sso_availability', + field=models.CharField(choices=[('Public', 'PUBLIC'), ('Unavailable', 'UNAVAILABLE'), ('Hidden', 'HIDDEN')], default='Hidden', max_length=15), + ), + ] diff --git a/osf/models/institution.py b/osf/models/institution.py index 8dccdf6919a..bae41a8a177 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -46,6 +46,13 @@ class SsoFilterCriteriaAction(Enum): CONTAINS = 'contains' # Type 2: SSO releases a multi-value attribute, of which one value matches IN = 'in' # Type 3: SSO releases a single-value attribute that have multiple valid values +class SSOAvailability(Enum): + """Defines 3 SSO availability states for institutions. + """ + PUBLIC = 'Public' + UNAVAILABLE = 'Unavailable' + HIDDEN = 'Hidden' + class InstitutionManager(models.Manager): @@ -79,6 +86,13 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian default='' ) + # Institution SSO availability + sso_availability = models.CharField( + choices=[(el.value, el.name) for el in SSOAvailability], + max_length=15, + default='Hidden' + ) + # Default Storage Region storage_regions = models.ManyToManyField( 'addons_osfstorage.Region', @@ -125,7 +139,6 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian default='', help_text='Full URL where institutional admins can access archived metrics reports.', ) - sso_in_progress = models.BooleanField(default=False) class Meta: # custom permissions for use in the OSF Admin App @@ -238,6 +251,11 @@ def deactivate(self): """ if not self.deactivated: self.deactivated = timezone.now() + if not self.delegation_protocol: + self.sso_availability = SSOAvailability.UNAVAILABLE.value + else: + self.sso_availability = SSOAvailability.HIDDEN.value + self.save() # Django mangers aren't used when querying on related models. Thus, we can query # affiliated users and send notification emails after the institution has been deactivated. @@ -252,6 +270,10 @@ def reactivate(self): """ if self.deactivated: self.deactivated = None + if not self.delegation_protocol: + self.sso_availability = SSOAvailability.UNAVAILABLE.value + else: + self.sso_availability = SSOAvailability.HIDDEN.value self.save() else: message = f'Action rejected - reactivating an active institution [{self._id}].' diff --git a/osf_tests/test_institution.py b/osf_tests/test_institution.py index 039b0ce04dd..9006769ebbe 100644 --- a/osf_tests/test_institution.py +++ b/osf_tests/test_institution.py @@ -139,12 +139,22 @@ def test_deactivate_institution(self): assert institution.deactivated is not None assert mock__send_deactivation_email.called + def test_reactivate_sso_institution(self): + institution = InstitutionFactory() + institution.delegation_protocol = 'saml-shib' + institution.deactivated = timezone.now() + institution.save() + institution.reactivate() + assert institution.deactivated is None + assert institution.sso_availability == 'Hidden' + def test_reactivate_institution(self): institution = InstitutionFactory() institution.deactivated = timezone.now() institution.save() institution.reactivate() assert institution.deactivated is None + assert institution.sso_availability == 'Unavailable' def test_send_deactivation_email_call_count(self): institution = InstitutionFactory() From 1d74ad3a2fcb6c07ca7bcd18f101689eead02651 Mon Sep 17 00:00:00 2001 From: Ostap Zherebetskyi Date: Thu, 19 Mar 2026 17:34:01 +0200 Subject: [PATCH 11/43] Update SSO availability logic and add tests for institution deactivation --- osf/models/institution.py | 10 +++++----- osf_tests/test_institution.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/osf/models/institution.py b/osf/models/institution.py index bae41a8a177..c1679892f69 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -49,9 +49,9 @@ class SsoFilterCriteriaAction(Enum): class SSOAvailability(Enum): """Defines 3 SSO availability states for institutions. """ - PUBLIC = 'Public' - UNAVAILABLE = 'Unavailable' - HIDDEN = 'Hidden' + PUBLIC = 'Public' # Active and has a delegation protocol + UNAVAILABLE = 'Unavailable' # Does not have a delegation protocol + HIDDEN = 'Hidden' # Inactive and has a delegation protocol class InstitutionManager(models.Manager): @@ -88,9 +88,9 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian # Institution SSO availability sso_availability = models.CharField( - choices=[(el.value, el.name) for el in SSOAvailability], + choices=[(choice.value, choice.name) for choice in SSOAvailability], max_length=15, - default='Hidden' + default=SSOAvailability.HIDDEN.value ) # Default Storage Region diff --git a/osf_tests/test_institution.py b/osf_tests/test_institution.py index 9006769ebbe..867723cf291 100644 --- a/osf_tests/test_institution.py +++ b/osf_tests/test_institution.py @@ -128,6 +128,20 @@ def test_deactivated_institution_in_all_institutions(self): institution.save() assert institution in Institution.objects.get_all_institutions() + def test_deactivate_sso_institution(self): + institution = InstitutionFactory() + institution.delegation_protocol = 'saml-shib' + institution.save() + with mock.patch.object( + institution, + '_send_deactivation_email', + return_value=None + ) as mock__send_deactivation_email: + institution.deactivate() + assert institution.deactivated is not None + assert mock__send_deactivation_email.called + assert institution.sso_availability == 'Hidden' + def test_deactivate_institution(self): institution = InstitutionFactory() with mock.patch.object( @@ -138,6 +152,7 @@ def test_deactivate_institution(self): institution.deactivate() assert institution.deactivated is not None assert mock__send_deactivation_email.called + assert institution.sso_availability == 'Unavailable' def test_reactivate_sso_institution(self): institution = InstitutionFactory() From 26efc85c0cd9656df1156226e2d1b55c7e84b661 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Thu, 19 Mar 2026 11:43:39 -0400 Subject: [PATCH 12/43] Apply suggestions from @cslzchen Co-authored-by: Longze Chen --- osf/models/institution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osf/models/institution.py b/osf/models/institution.py index c1679892f69..edf553b6d0f 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -49,9 +49,9 @@ class SsoFilterCriteriaAction(Enum): class SSOAvailability(Enum): """Defines 3 SSO availability states for institutions. """ - PUBLIC = 'Public' # Active and has a delegation protocol + PUBLIC = 'Public' # Active, has a delegation protocol and SSO setup has been verified UNAVAILABLE = 'Unavailable' # Does not have a delegation protocol - HIDDEN = 'Hidden' # Inactive and has a delegation protocol + HIDDEN = 'Hidden' # 1) Inactive and has a delegation protocol, or 2) active, has a delegation protocol and SSO setup is in-progress class InstitutionManager(models.Manager): From 73dfa281a65c60e049f9fc301551eef3699889a8 Mon Sep 17 00:00:00 2001 From: Ostap Zherebetskyi Date: Fri, 20 Mar 2026 16:32:39 +0200 Subject: [PATCH 13/43] Add CAS login URL property and implement copy modal in institution detail view --- admin/institutions/views.py | 1 + admin/templates/institutions/detail.html | 70 +++++++++++++++++++++++- osf/models/institution.py | 13 +++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/admin/institutions/views.py b/admin/institutions/views.py index 46e6a0a7745..7ab8994e41f 100644 --- a/admin/institutions/views.py +++ b/admin/institutions/views.py @@ -56,6 +56,7 @@ def get_context_data(self, *args, **kwargs): institution_dict = model_to_dict(institution) kwargs.setdefault('page_number', self.request.GET.get('page', '1')) kwargs['institution'] = institution_dict + kwargs['cas_login_url'] = institution.cas_login_url kwargs['logo_path'] = institution.logo_path kwargs['banner_path'] = institution.banner_path fields = institution_dict diff --git a/admin/templates/institutions/detail.html b/admin/templates/institutions/detail.html index 2ce1ad20a03..b2170aafaa5 100644 --- a/admin/templates/institutions/detail.html +++ b/admin/templates/institutions/detail.html @@ -1,7 +1,35 @@ {% extends "base.html" %} {% load static %} {% block top_includes %} - + + {% endblock %} {% load comment_extras %} {% block title %} @@ -33,6 +61,20 @@ {% if perms.osf.change_institution %} Manage Admins {% endif %} + + {% if cas_login_url %} + + + + {% endif %} @@ -168,5 +210,31 @@

Are you sure you want to run monthly report for this institution?

}); }); }); + + window.openCopyPopup = function(text) { + const modal = document.getElementById("copy-modal"); + const input = document.getElementById("copy-input"); + + input.value = text; + + modal.classList.add("show_modal"); + + navigator.clipboard.writeText(text).catch(() => {}); + + input.focus(); + input.select(); + }; + + window.closeCopyPopup = function() { + document.getElementById("copy-modal").classList.remove("show_modal"); + }; + + // Close on outside click + window.onclick = function(event) { + const modal = document.getElementById("copy-modal"); + if (event.target === modal) { + modal.classList.remove("show_modal"); + } + }; {% endblock %} diff --git a/osf/models/institution.py b/osf/models/institution.py index c1679892f69..c5752cb7d43 100644 --- a/osf/models/institution.py +++ b/osf/models/institution.py @@ -24,6 +24,7 @@ from .validators import validate_email from osf.utils.fields import NonNaiveDateTimeField, LowercaseEmailField from website import settings as website_settings +from urllib.parse import quote logger = logging.getLogger(__name__) @@ -208,6 +209,18 @@ def banner_path(self): except InstitutionAssetFile.DoesNotExist: return '/static/img/institutions/banners/placeholder-banner.png' + @property + def cas_login_url(self): + if self.delegation_protocol == IntegrationType.NONE.value: + return None + if 'localhost' in website_settings.DOMAIN: + next_param = quote(website_settings.PROTOCOL + website_settings.LOCAL_ANGULAR_URL, safe='') + else: + next_param = quote(website_settings.DOMAIN, safe='') + service_url = quote(f'{website_settings.DOMAIN}login?next={next_param}', safe='') + + return f'{website_settings.CAS_SERVER_URL}/login?campaign=institution&institutionId={self._id}&service={service_url}' + def update_search(self): from website.search.search import update_institution from website.search.exceptions import SearchUnavailableError From ff32e01c1962ac6dd3e276408e3ef6a505e2dbac Mon Sep 17 00:00:00 2001 From: Ostap Zherebetskyi Date: Mon, 23 Mar 2026 16:39:22 +0200 Subject: [PATCH 14/43] Refactor SSO URL generation and clean up unused code in institution model --- admin/templates/institutions/detail.html | 8 -------- osf/models/institution.py | 1 - 2 files changed, 9 deletions(-) diff --git a/admin/templates/institutions/detail.html b/admin/templates/institutions/detail.html index b2170aafaa5..c28ca7e7703 100644 --- a/admin/templates/institutions/detail.html +++ b/admin/templates/institutions/detail.html @@ -5,7 +5,6 @@