diff --git a/admin/institutions/forms.py b/admin/institutions/forms.py
index ccb7e9c4e77..f482700a8c1 100644
--- a/admin/institutions/forms.py
+++ b/admin/institutions/forms.py
@@ -1,5 +1,5 @@
from django import forms
-from osf.models import Institution
+from osf.models.institution import Institution, SSOAvailability
class InstitutionForm(forms.ModelForm):
@@ -10,6 +10,25 @@ class Meta:
'is_deleted', 'contributors', 'storage_regions',
]
+ def clean(self):
+ super().clean()
+
+ if hasattr(self, 'cleaned_data') and self.changed_data:
+ if not self.cleaned_data['delegation_protocol']:
+ if self.cleaned_data['sso_availability'] != SSOAvailability.UNAVAILABLE.value:
+ self.add_error(None, 'SSO availability must be set to "Unavailable" when no delegation protocol is configured.')
+
+ elif self.cleaned_data['deactivated']:
+ if self.cleaned_data['sso_availability'] != SSOAvailability.HIDDEN.value:
+ self.add_error(None, 'SSO availability must be set to "Hidden" when the institution is deactivated.')
+
+ else:
+ if self.cleaned_data['sso_availability'] not in [
+ SSOAvailability.PUBLIC.value,
+ SSOAvailability.HIDDEN.value
+ ]:
+ self.add_error(None, 'SSO availability must be set to "Public" or "Hidden" when delegation protocol is configured.')
+
class InstitutionalMetricsAdminRegisterForm(forms.Form):
""" A form that finds an existing OSF User, and grants permissions to that
diff --git a/admin/institutions/views.py b/admin/institutions/views.py
index 536d916d937..50b1bcf9d4c 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
@@ -117,6 +118,17 @@ def get_context_data(self, *args, **kwargs):
def get_success_url(self, *args, **kwargs):
return reverse_lazy('institutions:detail', kwargs={'institution_id': self.kwargs.get('institution_id')})
+ def post(self, request, *args, **kwargs):
+ # Override `post` method in `django.views.generic.edit.ProcessFormView` due to custom behavior
+ self.object = self.get_object()
+ form = self.get_form()
+ if form.is_valid():
+ return self.form_valid(form)
+ else:
+ for error in form.non_field_errors():
+ messages.error(request, error)
+ return redirect('institutions:detail', institution_id=self.kwargs.get('institution_id'))
+
class InstitutionExport(PermissionRequiredMixin, View):
permission_required = 'osf.view_institution'
diff --git a/admin/templates/institutions/detail.html b/admin/templates/institutions/detail.html
index 8c4a9e79e15..ef2c9d5ff94 100644
--- a/admin/templates/institutions/detail.html
+++ b/admin/templates/institutions/detail.html
@@ -1,7 +1,33 @@
{% extends "base.html" %}
{% load static %}
{% block top_includes %}
-
+
+
{% endblock %}
{% load comment_extras %}
{% block title %}
@@ -34,6 +60,18 @@
{% if perms.osf.change_institution %}
Manage Admins
{% endif %}
+
+
+
+
×
+
Value copied. You can also copy manually:
+
+
+
@@ -169,5 +207,27 @@ 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/admin/templates/institutions/list.html b/admin/templates/institutions/list.html
index f990c778d25..47e6d09233f 100644
--- a/admin/templates/institutions/list.html
+++ b/admin/templates/institutions/list.html
@@ -20,6 +20,7 @@ List of Institutions
Name |
Description |
Status |
+ SSO Availability |
@@ -37,6 +38,7 @@ List of Institutions
{% else %}
DEACTIVATED |
{% endif %}
+ {{ institution.sso_availability }} |
{% endfor %}
diff --git a/admin_tests/institutions/test_views.py b/admin_tests/institutions/test_views.py
index 13cb1456ab9..6326d3f608e 100644
--- a/admin_tests/institutions/test_views.py
+++ b/admin_tests/institutions/test_views.py
@@ -139,11 +139,24 @@ 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': 'Unavailable',
}
form = InstitutionForm(data=new_data)
assert form.is_valid()
+ def test_institution_form_invalid(self):
+ new_data = {
+ 'name': 'New Name',
+ 'logo_name': 'awesome_logo.png',
+ 'domains': 'http://kris.biz/, http://www.little.biz/',
+ '_id': 'newawesomeprov',
+ 'sso_availability': 'Public',
+ }
+ form = InstitutionForm(data=new_data)
+ assert not form.is_valid()
+ assert {'__all__': ['SSO availability must be set to "Unavailable" when no delegation protocol is configured.']} == form.errors
+
class TestInstitutionExport(AdminTestCase):
def setUp(self):
@@ -214,7 +227,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': 'Unavailable',
}
form = InstitutionForm(data=data)
assert form.is_valid()
diff --git a/api/institutions/serializers.py b/api/institutions/serializers.py
index 6f4bc4f9e15..fc1e48cefdf 100644
--- a/api/institutions/serializers.py
+++ b/api/institutions/serializers.py
@@ -29,10 +29,12 @@ class InstitutionSerializer(JSONAPISerializer):
'id',
'name',
'auth_url',
+ 'sso_availability',
])
name = ser.CharField(read_only=True)
id = ser.CharField(read_only=True, source='_id')
+ sso_availability = ser.CharField(read_only=True)
description = ser.CharField(read_only=True)
auth_url = ser.CharField(read_only=True)
iri = ser.CharField(read_only=True, source='identifier_domain')
diff --git a/api/institutions/views.py b/api/institutions/views.py
index a3c0f93d0c8..d653f5b4e77 100644
--- a/api/institutions/views.py
+++ b/api/institutions/views.py
@@ -73,6 +73,9 @@ class InstitutionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
base_permissions.TokenHasScope,
)
+ # Adding sso_availability to MULTIPLE_VALUES_FIELDS to allow filtering institutions by multiple sso_availability values, e.g. ?filter[sso_availability]=[Unavailable,Hidden]
+ MULTIPLE_VALUES_FIELDS = ListFilterMixin.MULTIPLE_VALUES_FIELDS + ['sso_availability']
+
required_read_scopes = [CoreScopes.INSTITUTION_READ]
required_write_scopes = [CoreScopes.NULL]
model_class = Institution
@@ -85,7 +88,9 @@ class InstitutionList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):
ordering = ('name',)
def get_default_queryset(self):
- return Institution.objects.filter(_id__isnull=False, is_deleted=False)
+ if 'filter[sso_availability]' in self.request.query_params:
+ return Institution.objects.filter(_id__isnull=False, is_deleted=False)
+ return Institution.objects.get_non_hidden_institutions().filter(_id__isnull=False, is_deleted=False)
# overrides ListAPIView
def get_queryset(self):
diff --git a/api_tests/institutions/views/test_institution_list.py b/api_tests/institutions/views/test_institution_list.py
index 74cb0b6bc8f..60e63056b7a 100644
--- a/api_tests/institutions/views/test_institution_list.py
+++ b/api_tests/institutions/views/test_institution_list.py
@@ -15,6 +15,10 @@ def institution_one(self):
def institution_two(self):
return InstitutionFactory()
+ @pytest.fixture()
+ def institution_three(self):
+ return InstitutionFactory()
+
@pytest.fixture()
def url_institution(self):
return f'/{API_BASE}institutions/'
@@ -47,3 +51,53 @@ def test_does_not_return_deleted_institution(
assert len(res.json['data']) == 1
assert institution_one._id not in ids
assert institution_two._id in ids
+
+ def test_sso_availability_filter(
+ self, app, institution_one, institution_two, institution_three, url_institution
+ ):
+ institution_one.sso_availability = 'Unavailable'
+ institution_one.save()
+
+ institution_two.sso_availability = 'Public'
+ institution_two.save()
+
+ institution_three.sso_availability = 'Hidden'
+ institution_three.save()
+
+ res = app.get(f'{url_institution}?filter[sso_availability]=[Unavailable]')
+ assert res.status_code == 200
+
+ ids = [each['id'] for each in res.json['data']]
+ assert len(res.json['data']) == 1
+ assert institution_one._id in ids
+ assert institution_two._id not in ids
+
+ res = app.get(f'{url_institution}?filter[sso_availability]=[Unavailable,Hidden]')
+ assert res.status_code == 200
+
+ ids = [each['id'] for each in res.json['data']]
+ assert len(res.json['data']) == 2
+ assert institution_one._id in ids
+ assert institution_three._id in ids
+ assert institution_two._id not in ids
+
+ def test_default_filter_excludes_institutions_with_sso_availability_hidden(
+ self, app, institution_one, institution_two, institution_three, url_institution
+ ):
+ institution_one.sso_availability = 'Unavailable'
+ institution_one.save()
+
+ institution_two.sso_availability = 'Public'
+ institution_two.save()
+
+ institution_three.sso_availability = 'Hidden'
+ institution_three.save()
+
+ res = app.get(url_institution)
+ assert res.status_code == 200
+
+ ids = [each['id'] for each in res.json['data']]
+ assert len(res.json['data']) == 2
+ assert institution_one._id in ids
+ assert institution_two._id in ids
+ assert institution_three._id not in ids
diff --git a/framework/auth/utils.py b/framework/auth/utils.py
index 520c9489e1b..53c7df21a70 100644
--- a/framework/auth/utils.py
+++ b/framework/auth/utils.py
@@ -11,6 +11,7 @@
from framework import sentry
from website import settings
+from website.util import web_url_for
logger = logging.getLogger(__name__)
@@ -169,3 +170,16 @@ def generate_csl_given_name(given_name, middle_names='', suffix=''):
if suffix:
given = f'{given}, {suffix}'
return given
+
+def get_default_osf_login_url():
+ """Return the default OSF login URL.
+ """
+ next_url = web_url_for(view_name='index', _absolute=True, _angular_route=True)
+ return web_url_for(view_name='auth_login', _absolute=True, next=next_url)
+
+
+def get_default_osf_logout_url():
+ """Return the default OSF logout URL.
+ """
+ next_url = web_url_for(view_name='index', _absolute=True, _angular_route=True)
+ return web_url_for(view_name='auth_logout', _absolute=True, next=next_url)
diff --git a/framework/auth/views.py b/framework/auth/views.py
index 2adeb00b3b6..debbedb0d55 100644
--- a/framework/auth/views.py
+++ b/framework/auth/views.py
@@ -84,7 +84,7 @@ def _reset_password_get(auth, uid=None, token=None, institutional=False):
raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=error_data)
# override routes.py login_url to redirect to my-projects
- service_url = web_url_for('my_projects', _absolute=True)
+ service_url = web_url_for('dashboard', _absolute=True, _angular_route=True)
return {
'uid': user_obj._id,
@@ -142,7 +142,7 @@ def reset_password_post(uid=None, token=None):
status.push_status_message('Password reset', kind='success', trust=False)
# redirect to CAS and authenticate the user automatically with one-time verification key.
return redirect(cas.get_login_url(
- web_url_for('user_account', _absolute=True),
+ web_url_for('user_account', _absolute=True, _angular_route=True),
username=user_obj.username,
verification_key=user_obj.verification_key
))
@@ -176,7 +176,7 @@ def forgot_password_get(auth):
#overriding the routes.py sign in url to redirect to the my-projects after login
context = {}
- context['login_url'] = web_url_for('my_projects', _absolute=True)
+ context['login_url'] = web_url_for('dashboard', _absolute=True, _angular_route=True)
return context
@@ -324,7 +324,7 @@ def login_and_register_handler(auth, login=True, campaign=None, next_url=None, l
# unlike other campaigns, institution login serves as an alternative for authentication
if campaign == 'institution':
if next_url is None:
- next_url = web_url_for('my_projects', _absolute=True)
+ next_url = web_url_for('dashboard', _absolute=True, _angular_route=True)
data['status_code'] = http_status.HTTP_302_FOUND
if auth.logged_in:
data['next_url'] = next_url
@@ -391,7 +391,7 @@ def login_and_register_handler(auth, login=True, campaign=None, next_url=None, l
# `/login/` or `/register/` without any parameter
if auth.logged_in:
data['status_code'] = http_status.HTTP_302_FOUND
- data['next_url'] = web_url_for('my_projects', _absolute=True)
+ data['next_url'] = web_url_for('dashboard', _absolute=True, _angular_route=True)
return data
@@ -614,7 +614,7 @@ def external_login_confirm_email_get(auth, uid, token):
return redirect(campaign_url)
if new:
status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True, id='welcome_message')
- return redirect(web_url_for('my_projects'))
+ return redirect(web_url_for('dashboard', _absolute=True, _angular_route=True))
# token is invalid
if token not in user.email_verifications:
@@ -712,10 +712,10 @@ def confirm_email_get(token, auth=None, **kwargs):
status.push_status_message(language.WELCOME_MESSAGE, kind='default', jumbotron=True, trust=True, id='welcome_message')
if token in auth.user.email_verifications:
status.push_status_message(language.CONFIRM_ALTERNATE_EMAIL_ERROR, kind='danger', trust=True, id='alternate_email_error')
- return redirect(web_url_for('my_projects'))
+ return redirect(web_url_for('dashboard', _absolute=True, _angular_route=True))
status.push_status_message(language.MERGE_COMPLETE, kind='success', trust=False)
- return redirect(web_url_for('user_account'))
+ return redirect(web_url_for('user_account', _absolute=True, _angular_route=True))
try:
user.confirm_email(token, merge=is_merge)
@@ -1053,8 +1053,7 @@ def external_login_email_post():
fullname = session.get('auth_user_fullname', None) or form.name.data
service_url = session.get('service_url', None)
- # TODO: @cslzchen use user tags instead of destination
- destination = 'my_projects'
+ destination = 'dashboard'
for campaign in campaigns.get_campaigns():
if campaign != 'institution':
# Handle different url encoding schemes between `furl` and `urlparse/urllib`.
@@ -1202,6 +1201,10 @@ def validate_next_url(next_url):
:return: True if valid, False otherwise
"""
+ # allow redirection to angular locally
+ if settings.LOCAL_MODE and next_url.startswith(settings.LOCAL_ANGULAR_DOMAIN):
+ 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('//'):
diff --git a/framework/sessions/__init__.py b/framework/sessions/__init__.py
index 5891cb6d69f..bc11b690b12 100644
--- a/framework/sessions/__init__.py
+++ b/framework/sessions/__init__.py
@@ -15,7 +15,6 @@
from osf.utils.fields import ensure_str
from osf.exceptions import InvalidCookieOrSessionError
from website import settings
-from website.util import web_url_for
SessionStore = import_module(django_conf_settings.SESSION_ENGINE).SessionStore
@@ -165,7 +164,7 @@ def create_session(response, data=None):
def before_request():
# TODO: Fix circular import
from framework.auth.core import get_user
- from framework.auth import cas
+ from framework.auth import cas, utils
UserSessionMap = apps.get_model('osf.UserSessionMap')
# Request Type 1: Service ticket validation during CAS login.
@@ -216,7 +215,8 @@ def before_request():
try:
user_session = flask_get_session_from_cookie(cookie)
except InvalidCookieOrSessionError:
- response = redirect(web_url_for('auth_login'))
+ # If invalid session/cookie happens, perform a full logout to clear both CAS and OSF Sessions
+ response = redirect(utils.get_default_osf_logout_url())
response.delete_cookie(settings.COOKIE_NAME, domain=settings.OSF_COOKIE_DOMAIN)
return response
# Case 1: anonymous session that is used for first time external (e.g. ORCiD) login only
diff --git a/osf/management/commands/backfill_sso_availability.py b/osf/management/commands/backfill_sso_availability.py
new file mode 100644
index 00000000000..d86e9362ff6
--- /dev/null
+++ b/osf/management/commands/backfill_sso_availability.py
@@ -0,0 +1,79 @@
+from django.core.management.base import BaseCommand
+from django.db import transaction
+from django.db.models import Q
+
+from osf.models.institution import Institution, SSOAvailability, IntegrationType
+
+
+class Command(BaseCommand):
+ help = 'Backfill sso_availability using fast DB-level updates'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--dry-run',
+ action='store_true',
+ help='Show how many rows would be updated without applying changes',
+ )
+
+ def handle(self, *args, **options):
+ dry_run = options['dry_run']
+
+ # Build querysets
+ qs_no_protocol = Institution.objects.filter(
+ delegation_protocol=IntegrationType.NONE.value
+ ).exclude(
+ sso_availability=SSOAvailability.UNAVAILABLE.value
+ )
+
+ qs_inactive_with_protocol = Institution.objects.filter(
+ ~Q(delegation_protocol=IntegrationType.NONE.value),
+ deactivated__isnull=False
+ ).exclude(
+ sso_availability=SSOAvailability.HIDDEN.value
+ )
+
+ qs_active_with_protocol = Institution.objects.filter(
+ ~Q(delegation_protocol=IntegrationType.NONE.value),
+ deactivated__isnull=True
+ ).exclude(
+ sso_availability=SSOAvailability.PUBLIC.value
+ )
+
+ count_no_protocol = qs_no_protocol.count()
+ count_inactive = qs_inactive_with_protocol.count()
+ count_active = qs_active_with_protocol.count()
+
+ total = count_no_protocol + count_inactive + count_active
+
+ self.stdout.write('Planned updates:')
+ self.stdout.write(f" No protocol → UNAVAILABLE: {count_no_protocol}")
+ self.stdout.write(f" Inactive + protocol → HIDDEN: {count_inactive}")
+ self.stdout.write(f" Active + protocol → PUBLIC: {count_active}")
+ self.stdout.write(f" TOTAL: {total}")
+
+ if dry_run:
+ self.stdout.write(self.style.WARNING('Dry run, no changes applied.'))
+ return
+
+ with transaction.atomic():
+ updated_no_protocol = qs_no_protocol.update(
+ sso_availability=SSOAvailability.UNAVAILABLE.value
+ )
+
+ updated_inactive = qs_inactive_with_protocol.update(
+ sso_availability=SSOAvailability.HIDDEN.value
+ )
+
+ updated_active = qs_active_with_protocol.update(
+ sso_availability=SSOAvailability.PUBLIC.value
+ )
+
+ self.stdout.write(
+ self.style.SUCCESS(
+ 'Done:\n'
+ f" UNAVAILABLE: {updated_no_protocol}\n"
+ f" HIDDEN: {updated_inactive}\n"
+ f" PUBLIC: {updated_active}\n"
+ f" TOTAL: {updated_no_protocol + updated_inactive + updated_active}"
+ )
+ )
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/migrations/0039_merge_20260427_1359.py b/osf/migrations/0039_merge_20260427_1359.py
new file mode 100644
index 00000000000..51979d93ca9
--- /dev/null
+++ b/osf/migrations/0039_merge_20260427_1359.py
@@ -0,0 +1,14 @@
+# Generated by Django 4.2.15 on 2026-04-27 13:59
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('osf', '0038_abstractnode_date_last_indexed_and_more'),
+ ('osf', '0038_institution_sso_availability'),
+ ]
+
+ operations = [
+ ]
diff --git a/osf/models/institution.py b/osf/models/institution.py
index 3671e7bef1f..87cb12eed05 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__)
@@ -46,6 +47,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):
@@ -55,6 +63,9 @@ def get_queryset(self):
def get_all_institutions(self):
return super().get_queryset()
+ def get_non_hidden_institutions(self):
+ return super().get_queryset().filter(deactivated__isnull=True, sso_availability__in=[SSOAvailability.PUBLIC.value, SSOAvailability.UNAVAILABLE.value])
+
class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, GuardianMixin):
objects = InstitutionManager()
@@ -79,6 +90,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',
@@ -194,6 +212,21 @@ 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
+ # Note: admin app can't use `web_url_for()` due to out of context
+ next_url_param = quote(website_settings.DOMAIN, safe='')
+ service_url_param = quote(f'{website_settings.DOMAIN}login?next={next_url_param}', safe='')
+ institution_id_param = quote(self._id, safe='')
+ return (
+ f'{website_settings.CAS_SERVER_URL}/login'
+ f'?campaign=institution'
+ f'&institutionId={institution_id_param}'
+ f'&service={service_url_param}'
+ )
+
def update_search(self):
from website.search.search import update_institution
from website.search.exceptions import SearchUnavailableError
@@ -237,6 +270,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.
@@ -251,6 +289,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/factories.py b/osf_tests/factories.py
index 0b357aed1aa..0d5e5ad0c32 100644
--- a/osf_tests/factories.py
+++ b/osf_tests/factories.py
@@ -28,6 +28,7 @@
from osf import models
from osf.models.sanctions import Sanction
from osf.models.storage import PROVIDER_ASSET_NAME_CHOICES
+from osf.models.institution import SSOAvailability
from osf.utils.names import impute_names_model
from osf.utils.workflows import (
DefaultStates,
@@ -258,6 +259,7 @@ class InstitutionFactory(DjangoModelFactory):
orcid_record_verified_source = ''
delegation_protocol = ''
institutional_request_access_enabled = False
+ sso_availability = SSOAvailability.PUBLIC.value
class Meta:
model = models.Institution
diff --git a/osf_tests/test_institution.py b/osf_tests/test_institution.py
index 039b0ce04dd..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,16 @@ 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()
@@ -145,6 +169,7 @@ def test_reactivate_institution(self):
institution.save()
institution.reactivate()
assert institution.deactivated is None
+ assert institution.sso_availability == 'Unavailable'
def test_send_deactivation_email_call_count(self):
institution = InstitutionFactory()
diff --git a/scripts/populate_institutions.py b/scripts/populate_institutions.py
index f46f7c67113..64bf59bd2b9 100644
--- a/scripts/populate_institutions.py
+++ b/scripts/populate_institutions.py
@@ -12,13 +12,13 @@
from website import settings
from website.app import init_app
-from osf.models import Institution
+from osf.models.institution import Institution, SSOAvailability, IntegrationType
from website.search.search import update_institution
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
-ENVS = ['prod', 'stage', 'stage2', 'stage3', 'test', 'local']
+ENVS = ['prod', 'stage', 'stage2', 'stage3', 'test', 'local', 'auto_generated']
# TODO: Store only the Entity IDs in OSF DB and move the URL building process to CAS
SHIBBOLETH_SP_LOGIN = f'{settings.CAS_SERVER_URL}/Shibboleth.sso/Login?entityID={{}}'
@@ -37,7 +37,10 @@ def encode_uri_component(val):
def update_or_create(inst_data):
- inst = Institution.load(inst_data['_id'])
+ try:
+ inst = Institution.objects.get_all_institutions().get(_id=inst_data['_id'])
+ except Institution.DoesNotExist:
+ inst = None
if inst:
for key, val in inst_data.items():
setattr(inst, key, val)
@@ -53,6 +56,53 @@ def update_or_create(inst_data):
return inst, True
+PROTOCOL_MAP = {
+ IntegrationType.SAML_SHIBBOLETH.value: 'SAML',
+ IntegrationType.CAS_PAC4J.value: 'CAS',
+ IntegrationType.OAUTH_PAC4J.value: 'OAuth',
+ IntegrationType.AFFILIATION_VIA_ORCID.value: 'ORCiD',
+ IntegrationType.NONE.value: 'None',
+}
+
+
+DEACTIVATED_STATES = [
+ None,
+ '2026-01-01T00:00:00+00:00',
+]
+
+
+def get_valid_sso_states(protocol, deactivated):
+ is_active = deactivated is None
+ if not protocol:
+ return [SSOAvailability.UNAVAILABLE.value]
+ if not is_active:
+ return [SSOAvailability.HIDDEN.value]
+ return [SSOAvailability.PUBLIC.value, SSOAvailability.HIDDEN.value]
+
+
+def generate_test_institutions():
+ institutions = []
+
+ for protocol in PROTOCOL_MAP.keys():
+ for deactivated in DEACTIVATED_STATES:
+ for availability in get_valid_sso_states(protocol, deactivated):
+ _id = f"{PROTOCOL_MAP[protocol]}_{availability}_{'a' if deactivated is None else 'i'}".lower()
+ institutions.append({
+ '_id': _id,
+ 'name': f'Test Institution [{PROTOCOL_MAP[protocol] if protocol else "None"} {availability} {"Inactive" if deactivated else "Active"}]',
+ 'description': f'Description for {PROTOCOL_MAP[protocol] if protocol else "None"} {availability} {"Inactive" if deactivated else "Active"}',
+ 'login_url': SHIBBOLETH_SP_LOGIN.format(encode_uri_component(f'{_id}-entity-id')) if protocol == IntegrationType.SAML_SHIBBOLETH.value else None,
+ 'logout_url': SHIBBOLETH_SP_LOGOUT.format(encode_uri_component(f'{settings.DOMAIN}{_id}')) if protocol == IntegrationType.SAML_SHIBBOLETH.value else None,
+ 'domains': [],
+ 'email_domains': [f'{_id}.osf.io'] if not protocol else [],
+ 'delegation_protocol': protocol,
+ 'sso_availability': availability,
+ 'deactivated': deactivated,
+ })
+
+ return institutions
+
+
def main(default_args=False):
if default_args:
@@ -67,7 +117,12 @@ def main(default_args=False):
if not server_env or server_env not in ENVS:
logger.error(f'A valid environment must be specified: {ENVS}')
sys.exit(1)
- institutions = INSTITUTIONS[server_env]
+
+ if server_env == 'auto_generated':
+ logger.info('Generating institutions with all combinations of protocol, availability, and deactivated states for testing purposes.')
+ institutions = generate_test_institutions()
+ else:
+ institutions = INSTITUTIONS[server_env]
if not update_all and not update_ids:
logger.error('Nothing to update or create. Please either specify a list of institutions '
@@ -101,6 +156,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['a2jlab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'albion',
@@ -113,6 +169,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'asu',
@@ -125,6 +182,7 @@ def main(default_args=False):
'domains': ['osf.asu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'brown',
@@ -135,6 +193,7 @@ def main(default_args=False):
'domains': ['osf.brown.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'bt',
@@ -145,6 +204,7 @@ def main(default_args=False):
'domains': ['osf.boystownhospital.org'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'bu',
@@ -155,6 +215,7 @@ def main(default_args=False):
'domains': ['osf.bu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'busara',
@@ -165,6 +226,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['busaracenter.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'callutheran',
@@ -175,6 +237,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'capolicylab',
@@ -185,6 +248,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['capolicylab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'cfa',
@@ -195,6 +259,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['cfa.harvard.edu'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'clrn',
@@ -205,6 +270,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['characterlab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'cmu',
@@ -219,6 +285,7 @@ def main(default_args=False):
'domains': ['osf.library.cmu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'colorado',
@@ -229,6 +296,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'cord',
@@ -239,6 +307,7 @@ def main(default_args=False):
'domains': ['osf.cord.edu'],
'email_domains': [],
'delegation_protocol': 'cas-pac4j',
+ 'sso_availability': 'Public',
},
{
'_id': 'cornell',
@@ -249,6 +318,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'cos',
@@ -259,6 +329,7 @@ def main(default_args=False):
'domains': ['osf.cos.io'],
'email_domains': ['cos.io'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'csic',
@@ -269,6 +340,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'cwru',
@@ -279,6 +351,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'duke',
@@ -289,6 +362,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ecu',
@@ -299,6 +373,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'esip',
@@ -309,6 +384,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['esipfed.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'eur',
@@ -324,6 +400,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ferris',
@@ -334,6 +411,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'fsu',
@@ -344,6 +422,7 @@ def main(default_args=False):
'domains': ['osf.fsu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'gatech',
@@ -354,6 +433,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'gmu',
@@ -364,6 +444,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'gwu',
@@ -374,6 +455,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'harvard',
@@ -384,6 +466,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ibhri',
@@ -394,6 +477,7 @@ def main(default_args=False):
'domains': ['osf.ibhri.org'],
'email_domains': ['ibhri.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'icarehb',
@@ -404,6 +488,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['icarehb.com'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'icer',
@@ -414,6 +499,7 @@ def main(default_args=False):
'domains': ['osf.icer-review.org'],
'email_domains': ['icer-review.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'igdore',
@@ -427,6 +513,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['igdore.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'iit',
@@ -437,6 +524,7 @@ def main(default_args=False):
'domains': ['osf.iit.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'itb',
@@ -447,6 +535,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'jhu',
@@ -457,6 +546,7 @@ def main(default_args=False):
'domains': ['osf.data.jhu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'jmu',
@@ -467,6 +557,7 @@ def main(default_args=False):
'domains': ['osf.jmu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'jpal',
@@ -477,6 +568,7 @@ def main(default_args=False):
'domains': ['osf.povertyactionlab.org'],
'email_domains': ['povertyactionlab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'kuleuven',
@@ -487,6 +579,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ljaf',
@@ -497,6 +590,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['arnoldfoundation.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'mit',
@@ -507,6 +601,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'mq',
@@ -517,6 +612,7 @@ def main(default_args=False):
'domains': ['osf.mq.edu.au'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nationalmaglab',
@@ -527,6 +623,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nesta',
@@ -537,6 +634,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nd',
@@ -547,6 +645,7 @@ def main(default_args=False):
'domains': ['osf.nd.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nyu',
@@ -557,6 +656,7 @@ def main(default_args=False):
'domains': ['osf.nyu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'okstate',
@@ -567,6 +667,7 @@ def main(default_args=False):
'domains': ['osf.library.okstate.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ou',
@@ -577,6 +678,7 @@ def main(default_args=False):
'domains': ['osf.ou.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'oxford',
@@ -587,6 +689,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'via-orcid',
+ 'sso_availability': 'Public',
'orcid_record_verified_source': 'ORCID Integration at the University of Oxford',
},
{
@@ -598,6 +701,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'purdue',
@@ -608,6 +712,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'sc',
@@ -620,6 +725,7 @@ def main(default_args=False):
'domains': ['osf.sc.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'temple',
@@ -630,6 +736,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'thepolicylab',
@@ -640,6 +747,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'thelabatdc',
@@ -650,6 +758,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['dc.gov'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'theworks',
@@ -660,6 +769,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['theworks.info'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'tufts',
@@ -670,6 +780,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ua',
@@ -680,6 +791,7 @@ def main(default_args=False):
'domains': ['osf.arizona.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ubc',
@@ -690,6 +802,7 @@ def main(default_args=False):
'domains': ['osf.openscience.ubc.ca'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uc',
@@ -700,6 +813,7 @@ def main(default_args=False):
'domains': ['osf.uc.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ucla',
@@ -710,6 +824,7 @@ def main(default_args=False):
'domains': ['osf.ucla.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ucsd',
@@ -720,6 +835,7 @@ def main(default_args=False):
'domains': ['osf.ucsd.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ucr',
@@ -730,6 +846,7 @@ def main(default_args=False):
'domains': ['osf.ucr.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uct',
@@ -740,6 +857,7 @@ def main(default_args=False):
'domains': ['osf.uct.ac.za'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ugent',
@@ -750,6 +868,7 @@ def main(default_args=False):
'domains': ['osf.ugent.be'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ugoe',
@@ -760,6 +879,7 @@ def main(default_args=False):
'domains': ['osf.uni-goettingen.de'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'umb',
@@ -770,6 +890,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'umd',
@@ -780,6 +901,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'unc',
@@ -790,6 +912,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'universityofkent',
@@ -800,6 +923,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uol',
@@ -810,6 +934,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uom',
@@ -820,6 +945,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'usc',
@@ -830,6 +956,7 @@ def main(default_args=False):
'domains': ['osf.usc.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ush',
@@ -840,6 +967,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['uvers.ac.id'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'utdallas',
@@ -850,6 +978,7 @@ def main(default_args=False):
'domains': ['osf.utdallas.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uva',
@@ -860,6 +989,7 @@ def main(default_args=False):
'domains': ['osf.virginia.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uw',
@@ -870,6 +1000,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uwstout',
@@ -880,6 +1011,7 @@ def main(default_args=False):
'domains': ['open.uwstout.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'vcu',
@@ -890,6 +1022,7 @@ def main(default_args=False):
'domains': ['osf.research.vcu.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'vt',
@@ -900,6 +1033,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'vua',
@@ -910,6 +1044,7 @@ def main(default_args=False):
'domains': ['osf.vu.nl'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'wustl',
@@ -920,6 +1055,7 @@ def main(default_args=False):
'domains': ['osf.wustl.edu'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
],
'stage': [
@@ -932,6 +1068,7 @@ def main(default_args=False):
'domains': ['staging-osf.cos.io'],
'email_domains': ['cos.io'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'nd',
@@ -942,6 +1079,7 @@ def main(default_args=False):
'domains': ['staging-osf-nd.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'google',
@@ -952,6 +1090,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['gmail.com'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'yahoo',
@@ -961,6 +1100,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['yahoo.com'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'oxford',
@@ -971,6 +1111,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'via-orcid',
+ 'sso_availability': 'Public',
'orcid_record_verified_source': 'ORCID Integration at the University of Oxford',
},
{
@@ -983,6 +1124,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'via-orcid',
+ 'sso_availability': 'Public',
'orcid_record_verified_source': 'OSF Integration',
},
],
@@ -996,6 +1138,7 @@ def main(default_args=False):
'domains': ['staging2-osf.cos.io'],
'email_domains': ['cos.io'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
],
'stage3': [
@@ -1008,6 +1151,7 @@ def main(default_args=False):
'domains': ['staging3-osf.cos.io'],
'email_domains': ['cos.io'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
],
'test': [
@@ -1020,6 +1164,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'a2jlab',
@@ -1030,6 +1175,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['a2jlab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'albion',
@@ -1042,6 +1188,7 @@ def main(default_args=False):
'domains': ['test-osf-ablbion.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'asu',
@@ -1054,6 +1201,7 @@ def main(default_args=False):
'domains': ['test-osf-asu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'brown',
@@ -1064,6 +1212,7 @@ def main(default_args=False):
'domains': ['test-osf-brown.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'bt',
@@ -1074,6 +1223,7 @@ def main(default_args=False):
'domains': ['test-osf-bt.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'bu',
@@ -1084,6 +1234,7 @@ def main(default_args=False):
'domains': ['test-osf-bu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'busara',
@@ -1094,6 +1245,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['busaracenter.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'callutheran',
@@ -1104,6 +1256,7 @@ def main(default_args=False):
'domains': ['test-osf-callutheran.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'callutheran2',
@@ -1114,6 +1267,7 @@ def main(default_args=False):
'domains': ['test-osf-callutheran2.cos.io'],
'email_domains': [],
'delegation_protocol': 'cas-pac4j',
+ 'sso_availability': 'Public',
},
{
'_id': 'capolicylab',
@@ -1124,6 +1278,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['capolicylab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'cfa',
@@ -1134,6 +1289,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['cfa.harvard.edu'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'clrn',
@@ -1144,6 +1300,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['characterlab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'cmu',
@@ -1158,6 +1315,7 @@ def main(default_args=False):
'domains': ['test-osf-cmu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'colorado',
@@ -1168,6 +1326,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'cornell',
@@ -1178,6 +1337,7 @@ def main(default_args=False):
'domains': ['test-osf-cornell.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'cord',
@@ -1188,6 +1348,7 @@ def main(default_args=False):
'domains': ['test-osf-cord.cos.io'],
'email_domains': [],
'delegation_protocol': 'cas-pac4j',
+ 'sso_availability': 'Public',
},
{
'_id': 'cos',
@@ -1198,6 +1359,7 @@ def main(default_args=False):
'domains': ['test-osf.cos.io'],
'email_domains': ['cos.io'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'csic',
@@ -1208,6 +1370,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'cwru',
@@ -1218,6 +1381,7 @@ def main(default_args=False):
'domains': ['test-osf-cwru.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'duke',
@@ -1228,6 +1392,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ecu',
@@ -1238,6 +1403,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'esip',
@@ -1248,6 +1414,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['esipfed.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'eur',
@@ -1263,6 +1430,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ferris',
@@ -1273,6 +1441,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'fsu',
@@ -1283,6 +1452,7 @@ def main(default_args=False):
'domains': ['test-osf-fsu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'gatech',
@@ -1293,6 +1463,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'gmu',
@@ -1303,6 +1474,7 @@ def main(default_args=False):
'domains': ['test-osf-gmu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'gwu',
@@ -1313,6 +1485,7 @@ def main(default_args=False):
'domains': ['test-osf-gwu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'harvard',
@@ -1323,6 +1496,7 @@ def main(default_args=False):
'domains': ['test-osf-harvard.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ibhri',
@@ -1333,6 +1507,7 @@ def main(default_args=False):
'domains': ['test-osf-ibhri.cos.io'],
'email_domains': ['ibhri.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'icarehb',
@@ -1343,6 +1518,7 @@ def main(default_args=False):
'domains': ['test-osf-icarehb.cos.io'],
'email_domains': ['icarehb.com'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'icer',
@@ -1353,6 +1529,7 @@ def main(default_args=False):
'domains': ['test-osf-icer.cos.io'],
'email_domains': ['icer-review.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'igdore',
@@ -1366,6 +1543,7 @@ def main(default_args=False):
'domains': ['test-osf-icer.igdore.io'],
'email_domains': ['igdore.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'iit',
@@ -1376,6 +1554,7 @@ def main(default_args=False):
'domains': ['test-osf-iit.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'itb',
@@ -1386,6 +1565,7 @@ def main(default_args=False):
'domains': ['test-osf-itb.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'jhu',
@@ -1396,6 +1576,7 @@ def main(default_args=False):
'domains': ['test-osf-jhu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'jmu',
@@ -1406,6 +1587,7 @@ def main(default_args=False):
'domains': ['test-osf-jmu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'jpal',
@@ -1416,6 +1598,7 @@ def main(default_args=False):
'domains': ['test-osf-jpal.cos.io'],
'email_domains': ['povertyactionlab.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'kuleuven',
@@ -1426,6 +1609,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ljaf',
@@ -1436,6 +1620,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['arnoldfoundation.org'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'mit',
@@ -1446,6 +1631,7 @@ def main(default_args=False):
'domains': ['test-osf-mit.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'mq',
@@ -1456,6 +1642,7 @@ def main(default_args=False):
'domains': ['test-osf-mq.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nationalmaglab',
@@ -1466,6 +1653,7 @@ def main(default_args=False):
'domains': ['test-osf-nationalmaglab.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nesta',
@@ -1476,6 +1664,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nd',
@@ -1486,6 +1675,7 @@ def main(default_args=False):
'domains': ['test-osf-nd.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'nyu',
@@ -1496,6 +1686,7 @@ def main(default_args=False):
'domains': ['test-osf-nyu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'okstate',
@@ -1506,6 +1697,7 @@ def main(default_args=False):
'domains': ['test-osf-library-okstate.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ou',
@@ -1516,6 +1708,7 @@ def main(default_args=False):
'domains': ['test-osf-ou.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'oxford',
@@ -1526,6 +1719,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'via-orcid',
+ 'sso_availability': 'Public',
'orcid_record_verified_source': 'ORCID Integration at the University of Oxford',
},
{
@@ -1537,6 +1731,7 @@ def main(default_args=False):
'domains': ['test-osf-pu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'purdue',
@@ -1547,6 +1742,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'sc',
@@ -1559,6 +1755,7 @@ def main(default_args=False):
'domains': ['test-osf-sc.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'temple',
@@ -1569,6 +1766,7 @@ def main(default_args=False):
'domains': ['test-osf-temple.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'thepolicylab',
@@ -1579,6 +1777,7 @@ def main(default_args=False):
'domains': ['test-osf-thepolicylab.cos.io'],
'email_domains': ['policylab.io'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'thelabatdc',
@@ -1589,6 +1788,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['dc.gov'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'theworks',
@@ -1599,6 +1799,7 @@ def main(default_args=False):
'domains': [],
'email_domains': ['theworks.info'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'tufts',
@@ -1609,6 +1810,7 @@ def main(default_args=False):
'domains': ['test-osf-tufts.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ua',
@@ -1619,6 +1821,7 @@ def main(default_args=False):
'domains': ['test-osf-ua.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ubc',
@@ -1629,6 +1832,7 @@ def main(default_args=False):
'domains': ['test-osf-ubc.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uc',
@@ -1639,6 +1843,7 @@ def main(default_args=False):
'domains': ['test-osf-uc.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ucla',
@@ -1649,6 +1854,7 @@ def main(default_args=False):
'domains': ['test-osf-ucla.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ucsd',
@@ -1659,6 +1865,7 @@ def main(default_args=False):
'domains': ['test-osf-ucsd.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ucr',
@@ -1669,6 +1876,7 @@ def main(default_args=False):
'domains': ['test-osf-ucr.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uct',
@@ -1679,6 +1887,7 @@ def main(default_args=False):
'domains': ['test-osf-uct.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'umb',
@@ -1689,6 +1898,7 @@ def main(default_args=False):
'domains': ['test-osf-umb.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'umd',
@@ -1699,6 +1909,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ugent',
@@ -1709,6 +1920,7 @@ def main(default_args=False):
'domains': ['test-osf-ugent.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ugoe',
@@ -1719,6 +1931,7 @@ def main(default_args=False):
'domains': ['test-osf-ugoe.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uit',
@@ -1731,6 +1944,7 @@ def main(default_args=False):
'domains': ['test-osf-uit.cos.io'],
'email_domains': ['uit.no'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'unc',
@@ -1741,6 +1955,7 @@ def main(default_args=False):
'domains': ['test-osf-unc.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'universityofkent',
@@ -1751,6 +1966,7 @@ def main(default_args=False):
'domains': ['test-osf-universityofkent.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uol',
@@ -1761,6 +1977,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uom',
@@ -1771,6 +1988,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'usc',
@@ -1781,6 +1999,7 @@ def main(default_args=False):
'domains': ['test-osf-usc.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'ush',
@@ -1791,6 +2010,7 @@ def main(default_args=False):
'domains': ['test-osf-ush.cos.io'],
'email_domains': ['uvers.ac.id'],
'delegation_protocol': '',
+ 'sso_availability': 'Unavailable',
},
{
'_id': 'utdallas',
@@ -1801,6 +2021,7 @@ def main(default_args=False):
'domains': ['test-osf-utdallas.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uva',
@@ -1811,6 +2032,7 @@ def main(default_args=False):
'domains': ['test-osf-virginia.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uw',
@@ -1821,6 +2043,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'uwstout',
@@ -1831,6 +2054,7 @@ def main(default_args=False):
'domains': ['test-osf-uwstout.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'vcu',
@@ -1841,6 +2065,7 @@ def main(default_args=False):
'domains': ['test-osf-research-vcu.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'vt',
@@ -1851,6 +2076,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'vua',
@@ -1861,6 +2087,7 @@ def main(default_args=False):
'domains': ['test-osf-vua.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'wustl',
@@ -1871,6 +2098,7 @@ def main(default_args=False):
'domains': ['test-osf-wustl.cos.io'],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
],
'local': [
@@ -1884,6 +2112,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'cas-pac4j',
+ 'sso_availability': 'Public',
},
{
'_id': 'osftype1',
@@ -1895,6 +2124,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'via-orcid',
+ 'sso_availability': 'Public',
'orcid_record_verified_source': 'OSF Integration',
},
{
@@ -1907,6 +2137,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
'orcid_record_verified_source': '',
},
{
@@ -1920,6 +2151,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'osftype4',
@@ -1932,6 +2164,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'osftype5',
@@ -1944,6 +2177,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
'orcid_record_verified_source': '',
},
{
@@ -1957,6 +2191,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'osftype7',
@@ -1969,6 +2204,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'osftype8',
@@ -1981,6 +2217,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
{
'_id': 'osftype9',
@@ -1993,6 +2230,7 @@ def main(default_args=False):
'domains': [],
'email_domains': [],
'delegation_protocol': 'saml-shib',
+ 'sso_availability': 'Public',
},
],
}
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 487759603c1..240a0cb317a 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -96,7 +96,7 @@ def test_confirm_email(self):
res = self.app.resolve_redirect(res)
assert res.status_code == 302
- assert '/my-projects/' == urlparse(res.location).path
+ assert '/dashboard/' == urlparse(res.location).path
# assert len(get_session()['status']) == 1
def test_get_user_by_id(self):
diff --git a/tests/test_auth_basic_auth.py b/tests/test_auth_basic_auth.py
index e1b9d6e5b8d..86b1d5fbec1 100644
--- a/tests/test_auth_basic_auth.py
+++ b/tests/test_auth_basic_auth.py
@@ -99,4 +99,4 @@ def test_expired_cookie(self):
self.app.set_cookie(settings.COOKIE_NAME, str(cookie))
res = self.app.get(self.reachable_url)
assert res.status_code == 302
- assert '/login/' == res.location
+ assert 'http://localhost:5000/logout/?next=http://localhost:4200/' == res.location
diff --git a/tests/test_auth_views.py b/tests/test_auth_views.py
index 7b24e286c15..965501d2e03 100644
--- a/tests/test_auth_views.py
+++ b/tests/test_auth_views.py
@@ -551,32 +551,32 @@ def setUp(self):
self.no_auth = Auth()
self.user_auth = AuthUserFactory()
self.auth = Auth(user=self.user_auth)
- self.next_url = web_url_for('my_projects', _absolute=True)
+ self.next_url = web_url_for('dashboard', _absolute=True, _angular_route=True)
self.invalid_campaign = 'invalid_campaign'
def test_osf_login_with_auth(self):
# login: user with auth
data = login_and_register_handler(self.auth)
assert data.get('status_code') == http_status.HTTP_302_FOUND
- assert data.get('next_url') == web_url_for('my_projects', _absolute=True)
+ assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True)
def test_osf_login_without_auth(self):
# login: user without auth
data = login_and_register_handler(self.no_auth)
assert data.get('status_code') == http_status.HTTP_302_FOUND
- assert data.get('next_url') == web_url_for('my_projects', _absolute=True)
+ assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True)
def test_osf_register_with_auth(self):
# register: user with auth
data = login_and_register_handler(self.auth, login=False)
assert data.get('status_code') == http_status.HTTP_302_FOUND
- assert data.get('next_url') == web_url_for('my_projects', _absolute=True)
+ assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True)
def test_osf_register_without_auth(self):
# register: user without auth
data = login_and_register_handler(self.no_auth, login=False)
assert data.get('status_code') == http_status.HTTP_200_OK
- assert data.get('next_url') == web_url_for('my_projects', _absolute=True)
+ assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True)
def test_next_url_login_with_auth(self):
# next_url login: user with auth
@@ -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_DOMAIN)
+ assert data.get('status_code') == http_status.HTTP_302_FOUND
+ assert data.get('next_url') == settings.LOCAL_ANGULAR_DOMAIN
+
+ def test_next_url_angular_login_without_auth(self):
+ request.url = web_url_for('auth_login', next=settings.LOCAL_ANGULAR_DOMAIN, _absolute=True)
+ data = login_and_register_handler(self.no_auth, next_url=settings.LOCAL_ANGULAR_DOMAIN)
+ 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)
@@ -610,14 +621,16 @@ def test_institution_login_with_auth(self):
# institution login: user with auth
data = login_and_register_handler(self.auth, campaign='institution')
assert data.get('status_code') == http_status.HTTP_302_FOUND
- assert data.get('next_url') == web_url_for('my_projects', _absolute=True)
+ assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True)
def test_institution_login_without_auth(self):
# institution login: user without auth
data = login_and_register_handler(self.no_auth, campaign='institution')
assert data.get('status_code') == http_status.HTTP_302_FOUND
- assert data.get('next_url') == cas.get_login_url(web_url_for('my_projects', _absolute=True),
- campaign='institution')
+ assert data.get('next_url') == cas.get_login_url(
+ web_url_for('dashboard', _absolute=True, _angular_route=True),
+ campaign='institution'
+ )
def test_institution_login_next_url_with_auth(self):
# institution login: user with auth and next url
@@ -635,13 +648,16 @@ def test_institution_register_with_auth(self):
# institution register: user with auth
data = login_and_register_handler(self.auth, login=False, campaign='institution')
assert data.get('status_code') == http_status.HTTP_302_FOUND
- assert data.get('next_url') == web_url_for('my_projects', _absolute=True)
+ assert data.get('next_url') == web_url_for('dashboard', _absolute=True, _angular_route=True)
def test_institution_register_without_auth(self):
# institution register: user without auth
data = login_and_register_handler(self.no_auth, login=False, campaign='institution')
assert data.get('status_code') == http_status.HTTP_302_FOUND
- assert data.get('next_url') == cas.get_login_url(web_url_for('my_projects', _absolute=True), campaign='institution')
+ assert data.get('next_url') == cas.get_login_url(
+ web_url_for('dashboard', _absolute=True, _angular_route=True),
+ campaign='institution'
+ )
def test_campaign_login_with_auth(self):
for campaign in get_campaigns():
@@ -827,6 +843,20 @@ 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']
+ 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']
+
+ 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):
diff --git a/tests/test_campaigns.py b/tests/test_campaigns.py
index 221ce03f8cf..49838c6244e 100644
--- a/tests/test_campaigns.py
+++ b/tests/test_campaigns.py
@@ -238,7 +238,7 @@ def setUp(self):
super().setUp()
self.url_login = web_url_for('auth_login', campaign='institution')
self.url_register = web_url_for('auth_register', campaign='institution')
- self.service_url = web_url_for('my_projects', _absolute=True)
+ self.service_url = web_url_for('dashboard', _absolute=True, _angular_route=True)
# go to CAS institution login page if not logged in
def test_institution_not_logged_in(self):
diff --git a/tests/test_webtests.py b/tests/test_webtests.py
index 64c5669f3e9..037aca0a137 100644
--- a/tests/test_webtests.py
+++ b/tests/test_webtests.py
@@ -79,35 +79,30 @@ def test_can_see_profile_url(self):
res = self.app.get(self.user.url, follow_redirects=True)
assert self.user.url in res.text
- # `GET /login/` without parameters is redirected to `/my-projects/` page which has `@must_be_logged_in` decorator
- # if user is not logged in, she/he is further redirected to CAS login page
+ # `GET /login/` (legacy BE endpoint) without parameters is redirected to `/dashboard/` (angular FE endpoint).
+ # It's impossible to test external redirects in tests, and it is angular's job to redirect correctly to CAS login.
def test_is_redirected_to_cas_if_not_logged_in_at_login_page(self):
- res = self.app.resolve_redirect(self.app.get('/login/'))
+ res = self.app.get('/login/')
assert res.status_code == 302
- location = res.headers.get('Location')
- assert 'login?service=' in location
+ assert 'dashboard' in res.headers.get('Location')
- def test_is_redirected_to_myprojects_if_already_logged_in_at_login_page(self):
+ def test_is_redirected_to_dashboard_if_already_logged_in_at_login_page(self):
res = self.app.get('/login/', auth=self.user.auth)
assert res.status_code == 302
- assert 'my-projects' in res.headers.get('Location')
+ assert 'dashboard' in res.headers.get('Location')
def test_register_page(self):
res = self.app.get('/register/')
assert res.status_code == 200
- def test_is_redirected_to_myprojects_if_already_logged_in_at_register_page(self):
+ def test_is_redirected_to_dashboard_if_already_logged_in_at_register_page(self):
res = self.app.get('/register/', auth=self.user.auth)
assert res.status_code == 302
- assert 'my-projects' in res.headers.get('Location')
+ assert 'dashboard' in res.headers.get('Location')
def test_sees_projects_in_her_dashboard(self):
- # the user already has a project
- project = ProjectFactory(creator=self.user)
- project.add_contributor(self.user)
- project.save()
- res = self.app.get('/my-projects/', auth=self.user.auth)
- assert 'Projects' in res.text # Projects heading
+ # Deprecated test, dashboard and my-projects are angular pages
+ pass
def test_does_not_see_osffiles_in_user_addon_settings(self):
res = self.app.get('/settings/addons/', auth=self.auth, follow_redirects=True)
@@ -123,10 +118,8 @@ def test_sees_osffiles_in_project_addon_settings(self):
assert 'OSF Storage' in res.text
def test_sees_correct_title_on_dashboard(self):
- # User goes to dashboard
- res = self.app.get('/my-projects/', auth=self.auth, follow_redirects=True)
- title = res.html.title.string
- assert 'OSF | My Projects' == title
+ # Deprecated test, dashboard and my-projects are angular pages
+ pass
def test_can_see_make_public_button_if_admin(self):
# User is a contributor on a project
diff --git a/website/routes.py b/website/routes.py
index 226f03fb1f1..af14a916183 100644
--- a/website/routes.py
+++ b/website/routes.py
@@ -230,11 +230,10 @@ def sitemap_file(path):
def goodbye():
# Redirect to dashboard if logged in
- redirect_url = util.web_url_for('auth_login')
if _get_current_user():
- return redirect(redirect_url)
+ return redirect(util.web_url_for('dashboard', _absolute=True, _angular_route=True))
else:
- return redirect(redirect_url + '?goodbye=true')
+ return redirect(util.web_url_for('index', _absolute=True, _angular_route=True))
def make_url_map(app):
"""Set up all the routes for the OSF app.
@@ -294,12 +293,8 @@ def make_url_map(app):
process_rules(app, [
Rule('/', 'get', website_views.index, notemplate),
- Rule(
- '/dashboard/',
- 'get',
- website_views.dashboard,
- notemplate
- ),
+ Rule('/dashboard/', 'get', website_views.dashboard, notemplate),
+ Rule('/my-projects/', 'get', website_views.my_projects, notemplate),
Rule(
'/metadata//',
@@ -307,12 +302,6 @@ def make_url_map(app):
website_views.metadata_download,
notemplate
),
- Rule(
- '/my-projects/',
- 'get',
- website_views.my_projects,
- OsfWebRenderer('my_projects.mako', trust=False)
- ),
Rule(
'/reproducibility/',
diff --git a/website/settings/defaults.py b/website/settings/defaults.py
index fbe9b939ae1..c2ad5bff701 100644
--- a/website/settings/defaults.py
+++ b/website/settings/defaults.py
@@ -84,12 +84,14 @@ def parent_dir(path):
DEV_MODE = False
DEBUG_MODE = False
SECURE_MODE = not DEBUG_MODE # Set secure cookie
+LOCAL_MODE = False # handles angular and web with different domains in local env
PROTOCOL = 'https://' if SECURE_MODE else 'http://'
DOMAIN = PROTOCOL + 'localhost:5000/'
INTERNAL_DOMAIN = DOMAIN
API_DOMAIN = PROTOCOL + 'localhost:8000/'
RESET_PASSWORD_URL = PROTOCOL + 'localhost:5000/resetpassword/' # TODO set angular reset password url
+LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/' # Only used when LOCAL_MODE is True
PREPRINT_PROVIDER_DOMAINS = {
'enabled': False,
diff --git a/website/settings/local-ci.py b/website/settings/local-ci.py
index a4d250a9792..eec0e7070e2 100644
--- a/website/settings/local-ci.py
+++ b/website/settings/local-ci.py
@@ -12,10 +12,12 @@
DEV_MODE = True
DEBUG_MODE = True # Sets app to debug mode, turns off template caching, etc.
SECURE_MODE = not DEBUG_MODE # Disable osf secure cookie
+LOCAL_MODE = True # handles angular and web with different domains in local env
PROTOCOL = 'https://' if SECURE_MODE else 'http://'
DOMAIN = PROTOCOL + 'localhost:5000/'
API_DOMAIN = PROTOCOL + 'localhost:8000/'
+LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/' # Only used when LOCAL_MODE is True
ENABLE_INSTITUTIONS = True
PREPRINT_PROVIDER_DOMAINS = {
diff --git a/website/settings/local-dist.py b/website/settings/local-dist.py
index 01d26b420c2..fbd7f4a451e 100644
--- a/website/settings/local-dist.py
+++ b/website/settings/local-dist.py
@@ -11,6 +11,7 @@
DEV_MODE = True
DEBUG_MODE = True # Sets app to debug mode, turns off template caching, etc.
SECURE_MODE = not DEBUG_MODE # Disable osf cookie secure
+LOCAL_MODE = True # handles angular and web with different domains in local env
# NOTE: Internal Domains/URLs have been added to facilitate docker development environments
# when localhost inside a container != localhost on the client machine/docker host.
@@ -19,7 +20,7 @@
DOMAIN = PROTOCOL + 'localhost:5000/'
INTERNAL_DOMAIN = DOMAIN
API_DOMAIN = PROTOCOL + 'localhost:8000/'
-LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/'
+LOCAL_ANGULAR_DOMAIN = PROTOCOL + 'localhost:4200/' # Only used when LOCAL_MODE is True
#WATERBUTLER_URL = 'http://localhost:7777'
#WATERBUTLER_INTERNAL_URL = WATERBUTLER_URL
diff --git a/website/util/__init__.py b/website/util/__init__.py
index f74182300cf..70ae6644e90 100644
--- a/website/util/__init__.py
+++ b/website/util/__init__.py
@@ -88,7 +88,7 @@ def api_v2_url(path_str,
return x
# Move to api utils?
-def web_url_for(view_name, _absolute=False, _internal=False, _guid=False, *args, **kwargs):
+def web_url_for(view_name, _absolute=False, _internal=False, _guid=False, _angular_route=False, *args, **kwargs):
"""Reverse URL lookup for web routes (those that use the OsfWebRenderer).
Takes the same arguments as Flask's url_for, with the addition of
`_absolute`, which will make an absolute URL with the correct HTTP scheme
@@ -102,6 +102,11 @@ def web_url_for(view_name, _absolute=False, _internal=False, _guid=False, *args,
# We do NOT use the url_for's _external kwarg because app.config['SERVER_NAME'] alters
# behavior in an unknown way (currently breaks tests). /sloria /jspies
domain = website_settings.INTERNAL_DOMAIN if _internal else website_settings.DOMAIN
+ if website_settings.LOCAL_MODE and _angular_route:
+ # We use `web_url_for()` to build URLs that actually goes to the angular
+ # container. It is not a problem for servers since web and angular shares
+ # the same domain. However, we need to handle this differently locally.
+ domain = website_settings.LOCAL_ANGULAR_DOMAIN
return urljoin(domain, url)
return url
diff --git a/website/views.py b/website/views.py
index 1a4bf4942da..f9601fb0441 100644
--- a/website/views.py
+++ b/website/views.py
@@ -8,8 +8,10 @@
from django.apps import apps
from flask import request, Response
+from framework import sentry
from framework.auth import Auth
-from framework.auth.decorators import must_be_logged_in, is_contributor_or_public_resource
+from framework.auth.utils import get_default_osf_logout_url
+from framework.auth.decorators import is_contributor_or_public_resource
from framework.auth.forms import SignInForm, ForgotPasswordForm
from framework.exceptions import HTTPError
from framework.flask import redirect # VOL-aware redirect
@@ -28,7 +30,6 @@
from osf.utils import permissions
from osf.metadata.tools import pls_gather_metadata_file
-from api.waffle.utils import storage_i18n_flag_active
logger = logging.getLogger(__name__)
@@ -132,21 +133,6 @@ def find_bookmark_collection(user):
return Collection.objects.get(creator=user, deleted__isnull=True, is_bookmark_collection=True)
-@must_be_logged_in
-def my_projects(auth):
- user = auth.user
-
- region_list = get_storage_region_list(user)
-
- bookmark_collection = find_bookmark_collection(user)
- my_projects_id = bookmark_collection._id
- return {'addons_enabled': user.get_addon_names(),
- 'dashboard_id': my_projects_id,
- 'storage_regions': region_list,
- 'storage_flag_is_active': storage_i18n_flag_active(),
- }
-
-
def validate_page_num(page, pages):
if page < 0 or (pages and page >= pages):
raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=dict(
@@ -165,11 +151,30 @@ def paginate(items, total, page, size):
def index():
- return redirect('/my-projects/')
+ """This route is handled by Angular now and web flow should not reach it at all.
+ There is alo no direct call of this view other than via `website/routes`. However,
+ we kept this view to use `web_url_for()` to build correct URL to go to Angular.
+ """
+ sentry.log_message('View "index" should not have been directly called or reached')
+ return redirect(get_default_osf_logout_url())
def dashboard():
- return redirect('/my-projects/')
+ """ This route is handled by Angular now and web flow should not reach it at all.
+ There is alo no direct call of this view other than via `website/routes`. However,
+ we kept this view to use `web_url_for()` to build correct URL to go to Angular.
+ """
+ sentry.log_message('View "dashboard" should not have been directly called or reached')
+ return redirect(get_default_osf_logout_url())
+
+
+def my_projects():
+ """ This route is handled by Angular now and web flow should not reach it at all.
+ There is alo no direct call of this view other than via `website/routes`. However,
+ we kept this view to use `web_url_for()` to build correct URL to go to Angular.
+ """
+ sentry.log_message('View "my_projects" should not have been directly called or reached')
+ return redirect(get_default_osf_logout_url())
def reproducibility():