Skip to content
Merged
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
6 changes: 4 additions & 2 deletions admin_tests/institutions/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
18 changes: 0 additions & 18 deletions osf/migrations/0036_institution_sso_in_progress.py

This file was deleted.

18 changes: 18 additions & 0 deletions osf/migrations/0038_institution_sso_availability.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
24 changes: 23 additions & 1 deletion osf/models/institution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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' # Active, has a delegation protocol and SSO setup has been verified
UNAVAILABLE = 'Unavailable' # Does not have 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):

Expand Down Expand Up @@ -79,6 +86,13 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian
default=''
)

# Institution SSO availability
sso_availability = models.CharField(
choices=[(choice.value, choice.name) for choice in SSOAvailability],
max_length=15,
default=SSOAvailability.HIDDEN.value
)

# Default Storage Region
storage_regions = models.ManyToManyField(
'addons_osfstorage.Region',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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}].'
Expand Down
25 changes: 25 additions & 0 deletions osf_tests/test_institution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -138,13 +152,24 @@ 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()
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()
Expand Down
Loading