Skip to content

Commit 53ab9ed

Browse files
auth: allow external accounts to login and restrict upload
1 parent 64395cf commit 53ab9ed

11 files changed

Lines changed: 238 additions & 11 deletions

File tree

cds/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1173,7 +1173,7 @@ def _parse_env_bool(var_name, default=None):
11731173
"https://auth.cern.ch/auth/realms/cern/protocol/openid-connect/userinfo",
11741174
)
11751175

1176-
OAUTHCLIENT_CERN_OPENID_ALLOWED_ROLES = ["cern-user"]
1176+
OAUTHCLIENT_CERN_OPENID_ALLOWED_ROLES = ["cern-user", "user"]
11771177

11781178
OAUTHCLIENT_CERN_OPENID_REFRESH_TIMEDELTA = timedelta(minutes=-5)
11791179
"""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: 18 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,20 @@ 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 using the upload_permission check."""
40+
def decorator(f):
41+
from cds.modules.records.permissions import upload_permission
42+
@wraps(f)
43+
def decorated_function(*args, **kwargs):
44+
if not current_user.is_authenticated:
45+
abort(401)
46+
47+
if not upload_permission():
48+
abort(403)
49+
50+
return f(*args, **kwargs)
51+
return decorated_function
52+
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}

cds/modules/records/permissions.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
from flask import current_app
2727
from flask_security import current_user
28-
from invenio_access import Permission
28+
from invenio_access import Permission, action_factory
2929
from invenio_files_rest.models import Bucket, MultipartObject, ObjectVersion
3030
from invenio_records_files.api import FileObject
3131
from invenio_records_files.models import RecordsBuckets
@@ -35,6 +35,8 @@
3535
from .utils import get_user_provides, is_deposit, is_record, lowercase_value
3636

3737

38+
upload_access_action = action_factory("videos-upload-access")
39+
3840
def files_permission_factory(obj, action=None):
3941
"""Permission for files are always based on the type of bucket.
4042
@@ -228,7 +230,7 @@ def can(self):
228230
def create(cls, record, action, user=None):
229231
"""Create a record permission."""
230232
if action in cls.create_actions:
231-
return cls(record, allow, user)
233+
return cls(record, upload_permission, user)
232234
elif action in cls.read_actions:
233235
return cls(record, has_read_record_permission, user)
234236
elif action in cls.read_eos_path_actions:
@@ -359,3 +361,8 @@ def has_admin_permission(user=None, record=None):
359361
"""
360362
# Allow administrators
361363
return Permission(action_admin_access).can()
364+
365+
366+
def upload_permission(*args, **kwargs):
367+
"""Return permission to allow only cern users."""
368+
return Permission(upload_access_action).can()

scripts/setup

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ cds users create test@test.ch -a --password=123456
4141
# Create an admin user
4242
cds users create admin@test.ch -a --password=123456
4343
cds roles create admin
44+
cds roles create cern-user
45+
cds roles add test@test.ch cern-user
4446
cds roles add admin@test.ch admin
4547
cds access allow deposit-admin-access role admin
4648
cds access allow superuser-access role admin
49+
cds access allow videos-upload-access role cern-user
4750

4851
# Create a default files location
4952
cds files location --default videos /tmp/files

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ invenio_oauth2server.scopes =
225225
deposit_actions = cds.modules.invenio_deposit.scopes:actions_scope
226226
invenio_access.actions =
227227
deposit_admin_access = cds.modules.invenio_deposit.permissions:action_admin_access
228+
upload_access_action = cds.modules.records.permissions:upload_access_action
228229
invenio_db.models =
229230
cds_migration_models = cds.modules.legacy.models
230231

tests/unit/conftest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
from cds.modules.invenio_deposit.permissions import action_admin_access
8080
from cds.modules.records.resolver import record_resolver
8181
from cds.modules.redirector.views import api_blueprint as cds_api_blueprint
82+
from cds.modules.records.permissions import upload_access_action
8283

8384

8485
@pytest.yield_fixture(scope="module", autouse=True)
@@ -203,13 +204,35 @@ def users(app, db):
203204
superadmin_role = Role(name="superadmin")
204205
db.session.add(ActionRoles(action=superuser_access.value, role=superadmin_role))
205206
datastore.add_role_to_user(superadmin, superadmin_role)
207+
# Give upload permission to all users
208+
cern_user_role = Role(name="cern-user")
209+
db.session.add(
210+
ActionRoles(action=upload_access_action.value, role=cern_user_role)
211+
)
212+
datastore.add_role_to_user(admin, cern_user_role)
213+
datastore.add_role_to_user(user1, cern_user_role)
214+
datastore.add_role_to_user(user2, cern_user_role)
215+
datastore.add_role_to_user(superadmin, cern_user_role)
206216
db.session.commit()
207217
id_1 = user1.id
208218
id_2 = user2.id
209219
id_4 = admin.id
210220
return [id_1, id_2, id_4]
211221

212222

223+
@pytest.fixture()
224+
def external_user(app, db):
225+
"""Create external user."""
226+
with db.session.begin_nested():
227+
datastore = app.extensions["security"].datastore
228+
user = datastore.create_user(
229+
email="external@gmail.com", password="tester", active=True
230+
)
231+
db.session.commit()
232+
id = user.id
233+
return id
234+
235+
213236
@pytest.fixture()
214237
def u_email(db, users):
215238
"""Valid user email."""

0 commit comments

Comments
 (0)