Skip to content

Commit 105c5da

Browse files
auth: allow external accounts to login and restrict upload
1 parent 6252774 commit 105c5da

6 files changed

Lines changed: 58 additions & 9 deletions

File tree

cds/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,7 +1168,9 @@ def _parse_env_bool(var_name, default=None):
11681168
"https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/userinfo",
11691169
)
11701170

1171-
OAUTHCLIENT_CERN_OPENID_ALLOWED_ROLES = ["cern-user"]
1171+
OAUTHCLIENT_CERN_OPENID_ALLOWED_ROLES = ["cern-user", "user"]
1172+
1173+
UPLOAD_ALLOWED_ROLES = ["cern-user"]
11721174

11731175
OAUTHCLIENT_CERN_OPENID_REFRESH_TIMEDELTA = timedelta(minutes=-5)
11741176
"""Default interval for refreshing CERN extra data (e.g. groups).

cds/modules/deposit/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"""CDS interface."""
2626

2727

28+
from cds.modules.ldap.decorators import cern_user_required
2829
from flask import (
2930
Blueprint,
3031
abort,
@@ -118,6 +119,7 @@ def to_links_js(pid, deposit=None, dep_type=None):
118119

119120
@blueprint.route("/deposit/reportnumbers/new", methods=["GET", "POST"])
120121
@login_required
122+
@cern_user_required()
121123
def reserve_report_number():
122124
"""Form to reserver a new report number."""
123125
if not has_read_record_eos_path_permission(current_user, None):
@@ -156,6 +158,7 @@ def reserve_report_number():
156158
"/deposit/reportnumbers/assign/<string:depid>", methods=["GET", "POST"]
157159
)
158160
@login_required
161+
@cern_user_required()
159162
def assign_report_number(depid):
160163
"""Form to reserver a new report number."""
161164
if not has_read_record_eos_path_permission(current_user, None):

cds/modules/invenio_deposit/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from flask import request
2929
from invenio_oauth2server import require_api_auth, require_oauth_scopes
3030

31+
from cds.modules.ldap.decorators import cern_user_required
3132
from .scopes import write_scope
3233

3334

@@ -84,6 +85,7 @@ def check_oauth2_scope(can_method, *myscopes):
8485

8586
def check(record, *args, **kwargs):
8687
@require_api_auth()
88+
@cern_user_required()
8789
@require_oauth_scopes(*myscopes)
8890
def can(self):
8991
return can_method(record)

cds/modules/invenio_deposit/views/ui.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
from copy import deepcopy
2929

30+
from cds.modules.ldap.decorators import cern_user_required
3031
from flask import Blueprint, current_app, render_template, request
3132
from flask_login import login_required
3233
from invenio_pidstore.errors import PIDDeletedError
@@ -73,12 +74,14 @@ def tombstone_errorhandler(error):
7374

7475
@blueprint.route("/deposit")
7576
@login_required
77+
@cern_user_required()
7678
def index():
7779
"""List user deposits."""
7880
return render_template(current_app.config["DEPOSIT_UI_INDEX_TEMPLATE"])
7981

8082
@blueprint.route("/deposit/new")
8183
@login_required
84+
@cern_user_required()
8285
def new():
8386
"""Create new deposit."""
8487
deposit_type = request.values.get("type")

cds/modules/ldap/decorators.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from functools import wraps
2323

2424
from flask_login import current_user
25-
from flask_restful import abort
25+
from flask import abort, current_app
2626

2727

2828
def needs_authentication(func):
@@ -33,3 +33,31 @@ def decorated_api_view(*args, **kwargs):
3333
abort(401)
3434
return func(*args, **kwargs)
3535
return decorated_api_view
36+
37+
38+
def cern_user_required():
39+
"""Restrict access based on roles from RemoteAccount.extra_data["roles"]."""
40+
def decorator(f):
41+
@wraps(f)
42+
def decorated_function(*args, **kwargs):
43+
if not current_user.is_authenticated:
44+
abort(401) # Unauthorized (not logged in)
45+
46+
allowed_roles = current_app.config.get("UPLOAD_ALLOWED_ROLES", [])
47+
48+
# Collect roles from all RemoteAccounts
49+
remote_account_roles = []
50+
for ra in getattr(current_user, "remote_accounts", []):
51+
if not ra.extra_data:
52+
continue
53+
roles = ra.extra_data.get("roles", [])
54+
if isinstance(roles, list):
55+
remote_account_roles.extend(roles)
56+
57+
# If user has at least one allowed role → grant access
58+
if not any(role in allowed_roles for role in remote_account_roles):
59+
abort(403) # Forbidden
60+
61+
return f(*args, **kwargs)
62+
return decorated_function
63+
return decorator

cds/modules/oauthclient/cern_openid.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ def find_remote_by_client_id(client_id):
8585

8686
def fetch_extra_data(resource):
8787
"""Return a dict with extra data retrieved from CERN OAuth."""
88-
person_id = resource.get("cern_person_id")
89-
return dict(person_id=person_id, groups=resource["groups"])
88+
data = {"groups": resource.get("groups", [])}
89+
if resource.get("cern_person_id"):
90+
data["person_id"] = resource["cern_person_id"]
91+
return data
9092

9193

9294
def account_roles_and_extra_data(account, resource, refresh_timedelta=None):
@@ -178,10 +180,19 @@ def _account_info(remote, resp):
178180
resp,
179181
)
180182

181-
email = resource["email"]
182-
external_id = str(resource["cern_uid"])
183-
nice = resource["preferred_username"]
184-
name = resource["name"]
183+
email = resource.get("email")
184+
if not email:
185+
raise OAuthCERNRejectedAccountError("No email in userinfo", remote, resp)
186+
187+
external_id = str(resource.get("cern_uid") or resource.get("sub"))
188+
if not external_id:
189+
raise OAuthCERNRejectedAccountError("No external_id in userinfo", remote, resp)
190+
191+
raw_username = resource.get("preferred_username") or email
192+
if "@" in raw_username:
193+
raw_username = raw_username.split("@")[0]
194+
nice = raw_username
195+
name = resource.get("name") or nice
185196

186197
return dict(
187198
user=dict(email=email.lower(), profile=dict(username=nice, full_name=name)),
@@ -231,7 +242,7 @@ def account_setup(remote, token, resp):
231242
resource = get_resource(remote, resp)
232243

233244
with db.session.begin_nested():
234-
external_id = resource.get("cern_uid")
245+
external_id = resource.get("cern_uid") or resource.get("sub")
235246

236247
# Set CERN person ID in extra_data.
237248
token.remote_account.extra_data = {"external_id": external_id}

0 commit comments

Comments
 (0)