Skip to content

Commit

Permalink
update user group handling, update docs (#1367)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Jul 9, 2024
1 parent 7931cd6 commit 286c14d
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Added
- Remote project access status updating in project detail view (#1358)
- ``SidebarContentAjaxView`` for sidebar and project dropdown content retrieval (#1366)
- ``UserDropdownContentAjaxView`` for user dropdown content retrieval (#1366, #1392)
- OpenID Connect (OIDC) authentication support (#1367)
- ``SODARUser.get_auth_type()`` helper (#1367)
- **Timeline**
- ``sodar_uuid`` field in ``TimelineEventObjectRef`` model (#1415)
- REST API views (#1350)
Expand Down
16 changes: 10 additions & 6 deletions docs/source/app_projectroles_settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,10 @@ Critical settings which should be provided through environment variables:
Secret for the OIDC provider.
``SOCIAL_AUTH_OIDC_USERNAME_KEY``
If the username key of the browser is expected to be something other than
the default ``username``, it can be configured here.
the default ``username``, it can be configured here. The values in this must
be unique and should preferably be human readable. If the OIDC provider does
not return such a username directly, it is possible to e.g. use the user
email as a unique user name.

If you want to provide a custom template for directing login to your OIDC
provider, add it as ``{PROJECTROLES_TEMPLATE_INCLUDE_PATH}/_login_oidc.html``
Expand All @@ -542,11 +545,12 @@ SAML SSO Configuration (Removed in v1.0)
.. note::

SAML support has been removed in SODAR Core v1.0. It has been replaced with
OpenID Connect (OIDC) support. The library previously used in SODAR Core is
incompatible with Django v4.2. We are unaware of SODAR Core based projects
requiring SAML at this time. If there are specific needs to use SAML on a
SODAR Core based site, we are happy to review pull requests to reintroduce
it. Please note the implementation has to support Django v4.2+.
the possibility to set up OpenID Connect (OIDC) authentication. The library
previously used for SAML in SODAR Core is incompatible with Django v4.x. We
are unaware of SODAR Core based projects requiring SAML at this time. If
there are specific needs to use SAML on a SODAR Core based site, we are
happy to review pull requests to reintroduce it. Please note the
implementation has to support Django v4.2+.


Global JS/CSS Include Modifications (Optional)
Expand Down
20 changes: 10 additions & 10 deletions docs/source/major_changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Release Highlights

- Upgrade to Django v4.2 and Postgres v16
- Add Python v3.11 support
- Add OpenID Connect (OIDC) authentication support
- Add app specific and semantic REST API versioning
- Add REST API versioning independent from repo/site versions
- Add timeline REST API
Expand All @@ -40,7 +41,7 @@ Release Highlights
- Rename base test classes
- Remove app setting max length limit
- Remove Python v3.8 support
- Remove SAML SSO support
- Remove SAML authentication support

Breaking Changes
================
Expand Down Expand Up @@ -86,16 +87,15 @@ General Python Dependencies
``requirements`` directory for up-to-date package versions and upgrade your
project.

SAML SSO Support Removed
------------------------
SAML Authentication Support Removed
-----------------------------------

Support for SAML SSO authentication has been removed in this release. It will
soon be replaced with support OpenID Connect authentication. The library we
previously used is no longer compatible with Django v4.2 and we are not aware of
SODAR Core based projects requiring SAML at this time. If there are specific
needs to use SAML on a SODAR Core based site, we are happy to review pull
requests to re-introduce it. Please note the implementation has to support
Django v4.2+.
SAML support has been removed and replaced with the possibility to set up OpenID
Connect (OIDC) authentication. The library previously used for SAML in SODAR
Core is incompatible with Django v4.x. We are unaware of SODAR Core based
projects requiring SAML at this time. If there are specific needs to use SAML on
a SODAR Core based site, we are happy to review pull requests to reintroduce it.
Please note the implementation has to support Django v4.2+.

REST API Versioning Overhaul
----------------------------
Expand Down
5 changes: 5 additions & 0 deletions projectroles/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,13 @@
'CATEGORY': {'default': 'category', 'plural': 'categories'},
'PROJECT': {'default': 'project', 'plural': 'projects'},
},
# User types
'USER_TYPE_LOCAL': 'LOCAL',
'USER_TYPE_LDAP': 'LDAP',
'USER_TYPE_OIDC': 'OIDC',
# System user group
'SYSTEM_USER_GROUP': 'system',
'OIDC_USER_GROUP': 'oidc',
# Project modification
'PROJECT_ACTION_CREATE': 'CREATE',
'PROJECT_ACTION_UPDATE': 'UPDATE',
Expand Down
51 changes: 43 additions & 8 deletions projectroles/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Models for the projectroles app"""

import logging
import re
import uuid

from django.apps import apps
Expand Down Expand Up @@ -34,6 +33,9 @@
PROJECT_ROLE_GUEST = SODAR_CONSTANTS['PROJECT_ROLE_GUEST']
PROJECT_ROLE_FINDER = SODAR_CONSTANTS['PROJECT_ROLE_FINDER']
APP_SETTING_SCOPE_SITE = SODAR_CONSTANTS['APP_SETTING_SCOPE_SITE']
USER_TYPE_LOCAL = SODAR_CONSTANTS['USER_TYPE_LOCAL']
USER_TYPE_LDAP = SODAR_CONSTANTS['USER_TYPE_LDAP']
USER_TYPE_OIDC = SODAR_CONSTANTS['USER_TYPE_OIDC']

# Local constants
ROLE_RANKING = {
Expand Down Expand Up @@ -1344,27 +1346,60 @@ def get_form_label(self, email=False):
ret += ' <{}>'.format(self.email)
return ret

def get_auth_type(self):
"""
Return user authentication type: OIDC, LDAP or local.
:return: String which may equal USER_TYPE_OIDC, USER_TYPE_LDAP or
USER_TYPE_LOCAL.
"""
groups = [g.name for g in self.groups.all()]
if 'oidc' in groups:
return USER_TYPE_OIDC
elif (
self.username.find('@') != -1
and self.username.split('@')[1].lower() in groups
):
return USER_TYPE_LDAP
return USER_TYPE_LOCAL

def is_local(self):
"""
Return True if user is of type USER_TYPE_LOCAL.
:return: Boolean
"""
return self.get_auth_type() == USER_TYPE_LOCAL

def set_group(self):
"""Set user group based on user name or social auth provider"""
social_auth = getattr(self, 'social_auth', None)
social_auth = getattr(self, 'social_auth')
if social_auth:
social_auth = social_auth.first()
ldap_domains = [
getattr(settings, 'AUTH_LDAP_USERNAME_DOMAIN').upper(),
getattr(settings, 'AUTH_LDAP2_USERNAME_DOMAIN').upper(),
]
# OIDC user group
if social_auth and social_auth.provider == AUTH_PROVIDER_OIDC:
group_name = AUTH_PROVIDER_OIDC
elif self.username.find('@') != -1:
# LDAP domain user groups
elif (
self.username.find('@') != -1
and self.username.split('@')[1] in ldap_domains
):
group_name = self.username.split('@')[1].lower()
# System user group for local users
else:
group_name = SODAR_CONSTANTS['SYSTEM_USER_GROUP']
group, created = Group.objects.get_or_create(name=group_name)
if group not in self.groups.all():
group.user_set.add(self)
logger.info(
'Set user group "{}" for {}'.format(group_name, self.username)
)
return group_name

# TODO: Add get_user_type(), replace is_local()

def is_local(self):
return not bool(re.search('@[A-Za-z0-9._-]+$', self.username))

def update_full_name(self):
"""
Update full name of user.
Expand Down
28 changes: 28 additions & 0 deletions projectroles/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import uuid

from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.forms.models import model_to_dict
from django.urls import reverse
Expand Down Expand Up @@ -41,6 +42,10 @@
REMOTE_LEVEL_VIEW_AVAIL = SODAR_CONSTANTS['REMOTE_LEVEL_VIEW_AVAIL']
REMOTE_LEVEL_READ_ROLES = SODAR_CONSTANTS['REMOTE_LEVEL_READ_ROLES']
REMOTE_LEVEL_REVOKED = SODAR_CONSTANTS['REMOTE_LEVEL_REVOKED']
USER_TYPE_LOCAL = SODAR_CONSTANTS['USER_TYPE_LOCAL']
USER_TYPE_LDAP = SODAR_CONSTANTS['USER_TYPE_LDAP']
USER_TYPE_OIDC = SODAR_CONSTANTS['USER_TYPE_OIDC']
OIDC_USER_GROUP = SODAR_CONSTANTS['OIDC_USER_GROUP']

# Local constants
SECRET = 'rsd886hi8276nypuvw066sbvv0rb2a6x'
Expand Down Expand Up @@ -1609,6 +1614,29 @@ def test_get_form_label_first_last(self):
'Full Name (testuser) <[email protected]>',
)

def test_get_auth_type_local(self):
"""Test get_auth_type() with local user"""
self.assertEqual(self.user.get_auth_type(), USER_TYPE_LOCAL)

@override_settings(AUTH_LDAP_USERNAME_DOMAIN='TEST')
def test_get_auth_type_ldap(self):
"""Test get_auth_type() with LDAP user"""
self.user.username = 'testuser@TEST'
self.user.save() # NOTE: set_group() is called on user save()
self.assertEqual(self.user.get_auth_type(), USER_TYPE_LDAP)

def test_get_auth_type_ldap_no_group(self):
"""Test get_auth_type() with LDAP username but no user group"""
self.user.username = 'testuser@TEST'
self.user.save()
self.assertEqual(self.user.get_auth_type(), USER_TYPE_LOCAL)

def test_get_auth_type_oidc(self):
"""Test get_auth_type() with OIDC user"""
group, _ = Group.objects.get_or_create(name=OIDC_USER_GROUP)
group.user_set.add(self.user)
self.assertEqual(self.user.get_auth_type(), USER_TYPE_OIDC)

def test_update_full_name(self):
"""Test update_full_name()"""
self.assertEqual(self.user.name, '')
Expand Down

0 comments on commit 286c14d

Please sign in to comment.