Skip to content

Commit 6241981

Browse files
committed
Add tasks and management command for scim sync
1 parent cdecf86 commit 6241981

File tree

24 files changed

+1769
-931
lines changed

24 files changed

+1769
-931
lines changed

docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ services:
99
ports:
1010
- "${POSTGRES_PORT:-55432}:5432"
1111

12+
redis:
13+
image: redis
14+
ports:
15+
- "${REDIS_PORT:-36379}:6379"
16+
1217
shell:
1318
build:
1419
target: uv

pyproject.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ authors = [
66
{ name = "MIT Open Learning Engineering", email = "odl-devops@mit.edu" }
77
]
88
dependencies = [
9-
"mitol-django-common",
9+
"mitol-django-common[celery]",
1010
"mitol-django-mail",
1111
'mitol-django-authentication[touchstone]; python_version < "3.13"',
1212
'mitol-django-authentication; python_version >= "3.13"',
@@ -22,7 +22,7 @@ dependencies = [
2222
"mitol-django-payment_gateway",
2323
"mitol-django-uvtestapp",
2424
"mitol-django-transcoding",
25-
"mitol-django-scim",
25+
"mitol-django-scim[celery]",
2626
"mitol-django-apigateway",
2727
]
2828
readme = "README.md"
@@ -64,13 +64,15 @@ dev-dependencies = [
6464
"semver",
6565
"toml",
6666
"typing-extensions",
67+
"pytest-responses>=0.5.1",
68+
"more-itertools>=10.6.0",
6769
]
6870

6971
[tool.hatch.metadata]
7072
allow-direct-references = true
7173

7274
[tool.uv.workspace]
73-
members = ["src/*"]
75+
members = ["src/*", "testapp"]
7476

7577
[tool.uv.pip]
7678
no-binary = ["lxml", "xmlsec"]
@@ -214,7 +216,7 @@ lint.ignore = [
214216
"RET507",
215217
"RET508",
216218
"RUF012",
217-
"UP007"
219+
"UP007",
218220
]
219221
lint.typing-modules = ["colour.hints"]
220222

src/common/mitol/common/models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.db import transaction
1212
from django.db.models import (
1313
PROTECT,
14+
CharField,
1415
DateTimeField,
1516
ForeignKey,
1617
JSONField,
@@ -243,3 +244,21 @@ def _clone(self):
243244
self._prefetch_generic_related_lookups
244245
)
245246
return c
247+
248+
249+
class UserGlobalIdMixin:
250+
"""
251+
Mixin that adds a standard global_id definition.
252+
253+
The `global_id` field points to the SSO ID for the user (so, usually the Keycloak
254+
ID, which is a UUID). We store it as a string in case the SSO source changes.
255+
We allow a blank value so we can have out-of-band users - we may want a
256+
Django user that's not connected to an SSO user, for instance.
257+
"""
258+
259+
global_id = CharField(
260+
max_length=36,
261+
blank=True,
262+
default="",
263+
help_text="The SSO ID (usually a Keycloak UUID) for the user.",
264+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Celery settings"""
2+
3+
from mitol.common.envs import get_string
4+
5+
MITOL_CELERY_APP_INSTANCE_PATH = get_string(
6+
name="MITOL_CELERY_APP_INSTANCE_PATH",
7+
default="main.celery.app",
8+
description="Path to the celery app instance",
9+
required=True,
10+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from celery import Celery
2+
from django.conf import settings
3+
from django.core.exceptions import ImproperlyConfigured
4+
from django.utils.module_loading import import_string
5+
6+
7+
def get_celery_app() -> Celery:
8+
"""Get the celery app for the project we're running in"""
9+
app = import_string(settings.MITOL_CELERY_APP_INSTANCE_PATH)
10+
11+
if not isinstance(app, Celery):
12+
msg = (
13+
"Setting MITOL_CELERY_APP_INSTANCE_PATH does not reference "
14+
"an instance of `celery.Celery`"
15+
)
16+
raise ImproperlyConfigured(msg)
17+
18+
return app

src/common/pyproject.toml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@ name = "mitol-django-common"
33
description = "MIT Open Learning django app extensions for common utilities"
44
version = "2025.4.2"
55
dependencies = [
6-
"django-redis~=5.0",
7-
"django-stubs>=1.13.1",
8-
"django-webpack-loader>=0.7.0",
9-
"django>=3.0",
10-
"factory-boy~=3.2",
11-
"pytest>=7,<9",
12-
"pytz>=2020.4",
13-
"requests>=2.20.0",
14-
"typing-extensions",
6+
"django-redis~=5.0",
7+
"django-stubs>=1.13.1",
8+
"django-webpack-loader>=0.7.0",
9+
"django>=3.0",
10+
"factory-boy~=3.2",
11+
"pytest>=7,<9",
12+
"pytz>=2020.4",
13+
"requests>=2.20.0",
14+
"typing-extensions",
1515
]
1616
readme = "README.md"
1717
license = "BSD-3-Clause"
1818
requires-python = ">=3.10"
1919

20+
[project.optional-dependencies]
21+
celery = [
22+
"celery>=5.5.0",
23+
]
24+
2025
[tool.bumpver]
2126
current_version = "2025.4.2"
2227
version_pattern = "YYYY.MM.DD[.INC0]"

src/scim/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
## Prerequisites
44

5-
- You need the following a local [Keycloak](https://www.keycloak.org/) instance running. Note which major version you are running (should be at least 26.x).
5+
- You need the following a local [Keycloak](https://www.keycloak.org/) instance
6+
running. Note which major version you are running (should be at least 26.x).
67
- You should have custom user profile fields setup on your `olapps` realm:
78
- `fullName`: required, otherwise defaults
89
- `emailOptIn`: defaults
910

1011
## Install the scim-for-keycloak plugin
1112

12-
Sign up for an account on https://scim-for-keycloak.de and follow the instructions here: https://scim-for-keycloak.de/documentation/installation/install
13+
Sign up for an account on https://scim-for-keycloak.de and follow the
14+
instructions here:
15+
https://scim-for-keycloak.de/documentation/installation/install
1316

1417
## Configure SCIM
1518

@@ -20,7 +23,8 @@ In the SCIM admin console, do the following:
2023
- In django-admin, go to OAuth Toolkit and create a new access token
2124
- Go to Remote SCIM Provider
2225
- Click the `+` button
23-
- Specify a base URL for your learn API backend: `http://<IP_OR_HOSTNAME>:8063/scim/v2/`
26+
- Specify a base URL for your learn API backend:
27+
`http://<IP_OR_HOSTNAME>:8063/scim/v2/`
2428
- At the bottom of the page, click "Use default configuration"
2529
- Add a new authentication method:
2630
- Type: Long Life Bearer Token
@@ -30,6 +34,8 @@ In the SCIM admin console, do the following:
3034
- Add an attribute named `emailOptIn` with the following settings:
3135
- Type: integer
3236
- Custom Attribute Name: `emailOptIn`
37+
- Add an `emailVerified` attribute (using the option specific to this in the
38+
dropdown)
3339
- On the Realm Assignments tab, assign to the `olapps` realm
3440
- Go to the Synchronization tab and perform one:
3541
- Identifier attribute: email

src/scim/mitol/scim/adapters.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def from_dict(self, d):
131131
self.obj.last_name = d.get("name", {}).get("familyName", "")
132132
self.obj.scim_username = d.get("userName")
133133
self.obj.scim_external_id = d.get("externalId")
134+
self.obj.global_id = self.obj.scim_external_id
134135

135136
def _save_user(self):
136137
self.obj.save()

0 commit comments

Comments
 (0)