Skip to content

Commit 1e0c6f4

Browse files
committed
sso: integrate CERN login
1 parent ffa8425 commit 1e0c6f4

File tree

16 files changed

+342
-104
lines changed

16 files changed

+342
-104
lines changed

README.md

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,118 @@
66

77
# Invenio-CERN-sync
88

9-
Integrates CERN databases and login with Invenio.
9+
Integrates CERN databases and SSO login with Invenio.
1010

11-
## Users sync
11+
## SSO login
1212

13-
This module connects to LDAP to fetch users, updates already existing users
14-
and inserts missing ones.
13+
This module provides configurable integration with the CERN SSO login.
1514

15+
To integrate the CERN SSO, add this to your application configuration:
1616

17+
```python
18+
from invenio_cern_sync.sso import cern_remote_app_name, cern_keycloak
19+
OAUTHCLIENT_REMOTE_APPS = {
20+
cern_remote_app_name: cern_keycloak.remote_app,
21+
}
1722

23+
CERN_APP_CREDENTIALS = {
24+
"consumer_key": "CHANGE ME",
25+
"consumer_secret": "CHANGE ME",
26+
}
1827

19-
To get the extra user fields stored in the user profile, set the following:
28+
from invenio_cern_sync.sso.api import confirm_registration_form
29+
OAUTHCLIENT_SIGNUP_FORM = confirm_registration_form
2030

21-
from invenio_cern_sync.users.profile import CERNUserProfileSchema
22-
ACCOUNTS_USER_PROFILE_SCHEMA = CERNUserProfileSchema()
31+
OAUTHCLIENT_CERN_REALM_URL = cern_keycloak.realm_url
32+
OAUTHCLIENT_CERN_USER_INFO_URL = cern_keycloak.user_info_url
33+
OAUTHCLIENT_CERN_VERIFY_EXP = True
34+
OAUTHCLIENT_CERN_VERIFY_AUD = False
35+
OAUTHCLIENT_CERN_USER_INFO_FROM_ENDPOINT = True
36+
```
2337

24-
You can also provide your own schema.
38+
Define, use the env var to inject the right configuration
39+
for your env (local, prod, etc.):
2540

41+
- INVENIO_CERN_SYNC_KEYCLOAK_BASE_URL
42+
- INVENIO_SITE_UI_URL
2643

27-
Define
28-
- ACCOUNTS_DEFAULT_USER_VISIBILITY
29-
- ACCOUNTS_DEFAULT_EMAIL_VISIBILITY
44+
45+
## Sync users and groups
46+
47+
You can sync users and groups from the CERN AuthZ service or LDAP
48+
with the local Invenio db.
49+
50+
First, decide what fields you would like to get from the CERN database.
51+
By default, only the field in `invenio_cern_sync.users.profile.CERNUserProfileSchema`
52+
are kept when syncing.
53+
54+
If you need to customize that, you will need to:
55+
56+
1. Provide your own schema class, and assign it the config var `ACCOUNTS_USER_PROFILE_SCHEMA`
57+
2. Change the mappers, to serialize the fetched users from the CERN format to your
58+
local format. If you are using AuthZ, assign your custom serializer func
59+
to `CERN_SYNC_AUTHZ_USERPROFILE_MAPPER`.
60+
If you are using LDAP, assign it to `CERN_SYNC_LDAP_USERPROFILE_MAPPER`.
61+
3. You can also customize what extra data can be stored in the RemoteAccount.extra_data fields
62+
via the config `CERN_SYNC_AUTHZ_USER_EXTRADATA_MAPPER` or `CERN_SYNC_LDAP_USER_EXTRADATA_MAPPER`.
63+
64+
If are only using the CERN SSO as unique login method, you will probably also configure:
65+
66+
```python
67+
ACCOUNTS_DEFAULT_USER_VISIBILITY = True
68+
ACCOUNTS_DEFAULT_EMAIL_VISIBILITY = True
69+
```
70+
71+
### AuthZ
72+
73+
In your app, define the following configuration:
74+
75+
```python
76+
CERN_SYNC_KEYCLOAK_BASE_URL = "<url>"
77+
CERN_SYNC_AUTHZ_BASE_URL = "<url>"
78+
```
79+
80+
The above `CERN_APP_CREDENTIALS` configuration must be already configured.
81+
You will also need to make sure that those credentials are allowed to fetch
82+
the entire CERN database of user and groups.
83+
84+
Then, create a new celery task and sync users:
85+
86+
```python
87+
from invenio_cern_sync.users.sync import sync
88+
89+
def sync_users_task():
90+
user_ids = sync(method="AuthZ")
91+
# you can optionally pass extra kwargs for the AuthZ client APIs.
92+
93+
# make sure that you re-index users if needed. For example, in InvenioRDM:
94+
# from invenio_users_resources.services.users.tasks import reindex_users
95+
# reindex_users.delay(user_ids)
96+
```
97+
98+
To fetch groups:
99+
100+
```python
101+
from invenio_cern_sync.groups.sync import sync
102+
103+
def sync_groups_task():
104+
roles_ids = sync()
105+
```
106+
107+
### LDAP
108+
109+
You can use LDAP instead. Define the LDAP url:
110+
111+
```python
112+
CERN_SYNC_LDAP_URL = <url>
113+
```
114+
115+
Then, create a new celery task and sync users:
116+
117+
```python
118+
from invenio_cern_sync.users.sync import sync
119+
120+
def sync_users_task():
121+
user_ids = sync(method="LDAP")
122+
# you can optionally pass extra kwargs for the LDAP client APIs.
123+
```

invenio_cern_sync/authz/client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,12 @@ class KeycloakService:
4545
def __init__(self, base_url=None, client_id=None, client_secret=None):
4646
"""Constructor."""
4747
self.base_url = base_url or current_app.config["CERN_SYNC_KEYCLOAK_BASE_URL"]
48-
self.client_id = client_id or current_app.config["CERN_SYNC_KEYCLOAK_CLIENT_ID"]
48+
self.client_id = (
49+
client_id or current_app.config["CERN_APP_CREDENTIALS"]["consumer_key"]
50+
)
4951
self.client_secret = (
50-
client_secret or current_app.config["CERN_SYNC_KEYCLOAK_CLIENT_SECRET"]
52+
client_secret
53+
or current_app.config["CERN_APP_CREDENTIALS"]["consumer_secret"]
5154
)
5255

5356
def get_authz_token(self):
@@ -75,7 +78,6 @@ def get_authz_token(self):
7578
"cernGroup", # "CA"
7679
"cernSection", # "IR"
7780
"instituteName", # "CERN"
78-
"instituteAbbreviation", # "CERN"
7981
"preferredCernLanguage", # "EN"
8082
"orcid",
8183
"primaryAccountEmail",

invenio_cern_sync/authz/mapper.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,18 @@
1111
def userprofile_mapper(cern_identity):
1212
"""Map the CERN Identity fields to the Invenio user profile schema.
1313
14-
:param cern_identity: the identity dict
15-
:param profile_schema: the Invenio user profile schema to map to
16-
:return: a serialized dict, containing all the keys that will appear in the
17-
User.profile JSON column. Any unwanted key should be removed.
18-
"""
14+
The returned dict structure must match the user profile schema defined via
15+
the config ACCOUNTS_USER_PROFILE_SCHEMA."""
1916
return dict(
17+
affiliations=cern_identity["instituteName"],
2018
cern_department=cern_identity["cernDepartment"],
2119
cern_group=cern_identity["cernGroup"],
2220
cern_section=cern_identity["cernSection"],
2321
family_name=cern_identity["lastName"],
2422
full_name=cern_identity["displayName"],
2523
given_name=cern_identity["firstName"],
26-
institute_abbreviation=cern_identity["instituteAbbreviation"],
27-
institute=cern_identity["instituteName"],
2824
mailbox=cern_identity.get("postOfficeBox", ""),
25+
orcid=cern_identity.get("orcid", ""),
2926
person_id=cern_identity["personId"],
3027
)
3128

invenio_cern_sync/config.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,14 @@
1313
from .ldap.mapper import userprofile_mapper as ldap_userprofile_mapper
1414

1515
###################################################################################
16-
# Required config
17-
18-
CERN_SYNC_KEYCLOAK_CLIENT_ID = ""
19-
"""Set the unique id/name of the CERN SSO app, also called `consumer_key`.
20-
21-
This corresponds to the RemoteAccount `client_id` column.
22-
"""
23-
24-
CERN_SYNC_REMOTE_APP_NAME = None
25-
"""Set the configured remote (oauth) app name for the CERN login.
26-
27-
This corresponds to the UserIdentity `method` column.
28-
"""
29-
30-
###################################################################################
16+
# CERN AuthZ
3117
# Required config when using the AuthZ method to sync users, or when syncing groups
3218

33-
CERN_SYNC_KEYCLOAK_BASE_URL = ""
34-
"""."""
35-
36-
CERN_SYNC_KEYCLOAK_CLIENT_SECRET = ""
37-
"""."""
19+
CERN_SYNC_KEYCLOAK_BASE_URL = "https://keycloak-qa.cern.ch/"
20+
"""Base URL of the CERN SSO Keycloak endpoint."""
3821

39-
CERN_SYNC_AUTHZ_BASE_URL = ""
40-
"""."""
22+
CERN_SYNC_AUTHZ_BASE_URL = "https://authorization-service-api-qa.web.cern.ch/"
23+
"""Base URL of the Authorization Service API endpoint."""
4124

4225
CERN_SYNC_AUTHZ_USERPROFILE_MAPPER = authz_userprofile_mapper
4326
"""Map the AuthZ response to Invenio user profile schema.
@@ -50,6 +33,7 @@
5033

5134

5235
###################################################################################
36+
# CERN LDAP
5337
# Required config when using the LDAP method to sync users
5438

5539
CERN_SYNC_LDAP_URL = None

invenio_cern_sync/ldap/client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
"cernAccountType",
1919
"cernActiveStatus",
2020
"cernGroup",
21-
"cernInstituteAbbreviation",
2221
"cernInstituteName",
2322
"cernSection",
2423
"cn", # username

invenio_cern_sync/ldap/mapper.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,16 @@
1313
def userprofile_mapper(ldap_user):
1414
"""Map the LDAP fields to the Invenio user profile schema.
1515
16-
:param ldap_user: the ldap dict
17-
:param profile_schema: the Invenio user profile schema to map to
18-
:return: a serialized dict, containing all the keys that will appear in the
19-
User.profile JSON column. Any unwanted key should be removed.
20-
"""
16+
The returned dict structure must match the user profile schema defined via
17+
the config ACCOUNTS_USER_PROFILE_SCHEMA."""
2118
return dict(
19+
affiliations=first_or_default(ldap_user, "cernInstituteName"),
2220
cern_department=first_or_default(ldap_user, "division"),
2321
cern_group=first_or_default(ldap_user, "cernGroup"),
2422
cern_section=first_or_default(ldap_user, "cernSection"),
2523
family_name=first_or_default(ldap_user, "sn"),
2624
full_name=first_or_default(ldap_user, "displayName"),
2725
given_name=first_or_default(ldap_user, "givenName"),
28-
institute_abbreviation=first_or_default(ldap_user, "cernInstituteAbbreviation"),
29-
institute=first_or_default(ldap_user, "cernInstituteName"),
3026
mailbox=first_or_default(ldap_user, "postOfficeBox"),
3127
person_id=first_or_default(ldap_user, "employeeID"),
3228
)

invenio_cern_sync/sso/__init__.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2024 CERN.
4+
#
5+
# Invenio-CERN-sync is free software; you can redistribute it and/or modify it under
6+
# the terms of the MIT License; see LICENSE file for more details.
7+
8+
"""Invenio-CERN-sync SSO module."""
9+
10+
###################################################################################
11+
# CERN SSO
12+
# Pre-configured settings for CERN SSO
13+
14+
import os
15+
from urllib.parse import quote
16+
17+
from invenio_oauthclient.contrib.keycloak import KeycloakSettingsHelper
18+
19+
from .api import (
20+
cern_groups_handler,
21+
cern_groups_serializer,
22+
cern_info_handler,
23+
cern_info_serializer,
24+
cern_setup_handler,
25+
)
26+
27+
_base_url = os.environ.get(
28+
"INVENIO_CERN_SYNC_KEYCLOAK_BASE_URL", "https://keycloak-qa.cern.ch/"
29+
)
30+
_site_ui_url = os.environ.get("INVENIO_SITE_UI_URL", "https://127.0.0.1")
31+
32+
cern_remote_app_name = "cern" # corresponds to the UserIdentity `method` column
33+
34+
cern_keycloak = KeycloakSettingsHelper(
35+
title="CERN",
36+
description="CERN SSO authentication",
37+
base_url=_base_url,
38+
realm="cern",
39+
app_key="CERN_APP_CREDENTIALS", # config key for the app credentials
40+
logout_url="{}auth/realms/cern/protocol/openid-connect/logout?redirect_uri={}".format(
41+
_base_url, quote(_site_ui_url)
42+
),
43+
)
44+
45+
handlers = cern_keycloak.get_handlers()
46+
handlers["signup_handler"] = {
47+
**handlers["signup_handler"],
48+
"info": cern_info_handler,
49+
"info_serializer": cern_info_serializer,
50+
"groups_serializer": cern_groups_serializer,
51+
"groups": cern_groups_handler,
52+
"setup": cern_setup_handler,
53+
}
54+
rest_handlers = cern_keycloak.get_rest_handlers()
55+
rest_handlers["signup_handler"] = {
56+
**rest_handlers["signup_handler"],
57+
"info": cern_info_handler,
58+
"info_serializer": cern_info_serializer,
59+
"groups_serializer": cern_groups_serializer,
60+
"groups": cern_groups_handler,
61+
"setup": cern_setup_handler,
62+
}

0 commit comments

Comments
 (0)