Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions authentication/backends/ol_open_id_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,3 @@ class OlOpenIdConnectAuth(OpenIdConnectAuth):
"""

name = "ol-oidc"

def get_user_details(self, response):
"""Get the user details from the API response"""
details = super().get_user_details(response)

return {
**details,
"profile": {
"name": response.get("name", ""),
"email_optin": bool(int(response["email_optin"]))
if "email_optin" in response
else None,
},
}
2 changes: 1 addition & 1 deletion authentication/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AuthenticationHooks:
"""Pluggy hooks specs for authentication"""

@hookspec
def user_created(self, user, user_data):
def user_created(self, user):
"""Trigger actions after a user is created"""


Expand Down
7 changes: 2 additions & 5 deletions authentication/pipeline/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from social_core.exceptions import AuthException

from authentication.hooks import get_plugin_manager
from profiles import api as profile_api


def forbid_hijack(
Expand All @@ -24,16 +23,14 @@ def forbid_hijack(
return {}


def user_created_actions(*, user, details, **kwargs):
def user_created_actions(**kwargs):
"""
Trigger plugins when a user is created
"""
if kwargs.get("is_new"):
pm = get_plugin_manager()
hook = pm.hook
hook.user_created(user=user, user_data=details)
else:
profile_api.ensure_profile(user=user, profile_data=details.get("profile", {}))
hook.user_created(user=kwargs["user"])


def user_onboarding(*, backend, **kwargs):
Expand Down
1 change: 0 additions & 1 deletion authentication/pipeline/user_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def test_user_created_actions(mocker, is_new):
kwargs = {
"user": user,
"is_new": is_new,
"details": {},
}

user_actions.user_created_actions(**kwargs)
Expand Down
3 changes: 1 addition & 2 deletions learning_resources/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ class FavoritesListPlugin:
hookimpl = apps.get_app_config("authentication").hookimpl

@hookimpl
def user_created(self, user, user_data): # noqa: ARG002
def user_created(self, user):
"""
Perform functions on a newly created user

Args:
user(User): The user to create the list for
user_data(dict): the user data
"""
UserList.objects.get_or_create(
author=user, title=FAVORITES_TITLE, defaults={"description": "My Favorites"}
Expand Down
2 changes: 1 addition & 1 deletion learning_resources/plugins_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def test_favorites_plugin_user_created(existing_list):
UserListFactory.create(
author=user, title=FAVORITES_TITLE, description="My Favorites"
)
FavoritesListPlugin().user_created(user, user_data={})
FavoritesListPlugin().user_created(user)
user.refresh_from_db()
assert user.user_lists.count() == 1
8 changes: 2 additions & 6 deletions main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,8 @@
"documentationUri": "",
},
],
"USER_ADAPTER": "profiles.scim.adapters.LearnSCIMUser",
"USER_MODEL_GETTER": "profiles.scim.adapters.get_user_model_for_scim",
"USER_FILTER_PARSER": "profiles.scim.filters.LearnUserFilterQuery",
"USER_ADAPTER": "profiles.adapters.SCIMProfile",
"USER_MODEL_GETTER": "profiles.adapters.get_user_model_for_scim",
}


Expand Down Expand Up @@ -298,9 +297,6 @@
),
urlparse(APP_BASE_URL).netloc,
]
SOCIAL_AUTH_PROTECTED_USER_FIELDS = [
"profile", # this avoids an error because profile is a related model
]

SOCIAL_AUTH_PIPELINE = (
# Checks if an admin user attempts to login/register while hijacking another user.
Expand Down
218 changes: 218 additions & 0 deletions profiles/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import copy
import logging

from django.contrib.auth import get_user_model
from django.db import transaction
from django_scim import constants
from django_scim import exceptions as scim_exceptions
from django_scim.adapters import SCIMUser

from profiles.models import Profile

User = get_user_model()


logger = logging.getLogger(__name__)


def get_user_model_for_scim():
"""
Get function for the django_scim library configuration (USER_MODEL_GETTER).

Returns:
model: Profile model.
"""
return Profile


class SCIMProfile(SCIMUser):
"""
Custom adapter to extend django_scim library. This is required in order
to extend the profiles.models.Profile model to work with the
django_scim library.
"""

password_changed = False
activity_changed = False

resource_type = "User"

def __init__(self, obj, request=None):
super().__init__(obj, request)
self._from_dict_copy = None

@property
def is_new_user(self):
"""_summary_

Returns:
bool: True is the user does not currently exist,
False if the user already exists.
"""
return not bool(self.obj.id)

@property
def emails(self):
"""
Return the email of the user per the SCIM spec.
"""
return [{"value": self.obj.user.email, "primary": True}]

@property
def display_name(self):
"""
Return the displayName of the user per the SCIM spec.
"""
if self.obj.first_name and self.obj.last_name:
return f"{self.obj.first_name} {self.obj.last_name}"
return self.obj.user.email

@property
def meta(self):
"""
Return the meta object of the user per the SCIM spec.
"""
return {
"resourceType": self.resource_type,
"created": self.obj.user.date_joined.isoformat(timespec="milliseconds"),
"lastModified": self.obj.updated_at.isoformat(timespec="milliseconds"),
"location": self.location,
}

def to_dict(self):
"""
Return a ``dict`` conforming to the SCIM User Schema,
ready for conversion to a JSON object.
"""
return {
"id": self.id,
"externalId": self.obj.scim_external_id,
"schemas": [constants.SchemaURI.USER],
"userName": self.obj.user.username,
"name": {
"givenName": self.obj.user.first_name,
"familyName": self.obj.user.last_name,
"formatted": self.name_formatted,
},
"displayName": self.display_name,
"emails": self.emails,
"active": self.obj.user.is_active,
"groups": [],
"meta": self.meta,
}

def from_dict(self, d):
"""
Consume a ``dict`` conforming to the SCIM User Schema, updating the
internal user object with data from the ``dict``.

Please note, the user object is not saved within this method. To
persist the changes made by this method, please call ``.save()`` on the
adapter. Eg::

scim_user.from_dict(d)
scim_user.save()
"""
# Store dict for possible later use when saving user
self._from_dict_copy = copy.deepcopy(d)

self.obj.user = User()

self.parse_active(d.get("active"))

self.obj.first_name = d.get("name", {}).get("givenName") or ""

self.obj.last_name = d.get("name", {}).get("familyName") or ""

super().parse_emails(d.get("emails"))

if self.is_new_user and not self.obj.email:
raise scim_exceptions.BadRequestError("Empty email value") # noqa: TRY003 EM101

self.obj.scim_username = d.get("userName")
self.obj.scim_external_id = d.get("externalId") or ""

def parse_active(self, active):
"""
Set User.is_active to the value from the SCIM request.

Args:
active (bool): The value of 'active' from the SCIM request.
"""
if active is not None:
if active != self.obj.user.is_active:
self.activity_changed = True
self.obj.user.is_active = active

def save(self):
"""
Save instances of the Profile and User models.

Raises:
self.reformat_exception: Error while creating or saving Profile or User model.
"""
try:
with transaction.atomic():
self.obj.user.email = self.obj.email
self.obj.user.username = self.obj.email
self.obj.user.first_name = self.obj.first_name
self.obj.user.last_name = self.obj.last_name
self.obj.user.save()
self.obj.name = self.display_name
self.obj.save()
logger.info("User saved. User id %i", self.obj.id)
except Exception as e:
raise self.reformat_exception(e) from e

def delete(self):
"""
Update User's is_active to False.
"""
self.obj.is_active = False
self.obj.save()
logger.info("Deactivated user id %i", self.obj.id)

def handle_add(self, path, value):
"""
Handle add operations per:
https://tools.ietf.org/html/rfc7644#section-3.5.2.1

Args:
path (AttrPath)
value (Union[str, list, dict])
"""
if path == "externalId":
self.obj.scim_external_id = value
self.obj.save()

def handle_replace(self, value):
"""
Handle the replace operations.

All operations happen within an atomic transaction.

Args:
value (Union[str, list, dict])
"""
attr_map = {
"familyName": "last_name",
"givenName": "first_name",
"active": "is_active",
"userName": "scim_username",
"externalId": "scim_external_id",
}

for attr, attr_value in (value or {}).items():
if attr in attr_map:
setattr(self.obj, attr_map.get(attr), attr_value)

elif attr == "emails":
self.parse_email(attr_value)

elif attr == "password":
self.obj.set_password(attr_value)

else:
raise scim_exceptions.SCIMException("Not Implemented", status=409) # noqa: EM101, TRY003

self.obj.save()
10 changes: 4 additions & 6 deletions profiles/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@

from django.apps import apps

from profiles.api import ensure_profile
from profiles.models import Profile


class CreateProfilePlugin:
hookimpl = apps.get_app_config("authentication").hookimpl

@hookimpl
def user_created(self, user, user_data):
def user_created(self, user):
"""
Perform functions on a newly created user

Args:
user(User): the user that was created
user_data(dict): the user data
user(User): The user to create the list for
"""
profile_data = user_data.get("profile", {})
ensure_profile(user, profile_data)
Profile.objects.get_or_create(user=user)
Empty file removed profiles/scim/__init__.py
Empty file.
Loading
Loading