diff --git a/assets/js/components/deposit/overrides/CreatibutorsRemoteSelectItem.js b/assets/js/components/deposit/overrides/CreatibutorsRemoteSelectItem.js new file mode 100644 index 0000000..87067cf --- /dev/null +++ b/assets/js/components/deposit/overrides/CreatibutorsRemoteSelectItem.js @@ -0,0 +1,50 @@ +// This file is part of CDS RDM +// Copyright (C) 2024 CERN. +// +// CDS RDM is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import _get from "lodash/get"; +import _truncate from "lodash/truncate"; +import _upperCase from "lodash/upperCase"; +import React from "react"; +import PropTypes from "prop-types"; +import { Header, Image } from "semantic-ui-react"; + +export const CDSCreatibutorsRemoteSelectItem = ({ creatibutor, isOrganization, idString, affNames }) => { + + if (creatibutor.props?.is_cern) { + const cmp = ( + + + {creatibutor.props.email} + + ) + idString.push(cmp) + } + + return ( +
+ {creatibutor.name} {idString.length ? <>({idString}) : null} + + {isOrganization ? creatibutor.acronym : affNames} + +
+ ); +}; + +CDSCreatibutorsRemoteSelectItem.propTypes = { + creatibutor: PropTypes.object.isRequired, + isOrganization: PropTypes.bool.isRequired, + idString: PropTypes.array, + affNames: PropTypes.string, +}; + +CDSCreatibutorsRemoteSelectItem.defaultProps = { + idString: [], + affNames: "", +}; \ No newline at end of file diff --git a/assets/js/invenio_app_rdm/overridableRegistry/mapping.js b/assets/js/invenio_app_rdm/overridableRegistry/mapping.js index 16eeb49..cd12b5a 100644 --- a/assets/js/invenio_app_rdm/overridableRegistry/mapping.js +++ b/assets/js/invenio_app_rdm/overridableRegistry/mapping.js @@ -2,10 +2,12 @@ import { CDSCarouselItem } from "../../components/communities_carousel/overrides import { CDSCommunitiesCarousel } from "../../components/communities_carousel/overrides/CommunitiesCarousel"; import { CDSRecordsList } from "../../components/frontpage/overrides/RecordsList"; import { CDSRecordsResultsListItem } from "../../components/frontpage/overrides/RecordsResultsListItem"; +import { CDSCreatibutorsRemoteSelectItem } from "../../components/deposit/overrides/CreatibutorsRemoteSelectItem"; export const overriddenComponents = { "InvenioAppRdm.RecordsList.layout": CDSRecordsList, "InvenioAppRdm.RecordsResultsListItem.layout": CDSRecordsResultsListItem, "InvenioCommunities.CommunitiesCarousel.layout": CDSCommunitiesCarousel, "InvenioCommunities.CarouselItem.layout": CDSCarouselItem, + "CreatibutorsModal.RemoteSelectItem.content": CDSCreatibutorsRemoteSelectItem, }; diff --git a/site/cds_rdm/ldap/api.py b/site/cds_rdm/ldap/api.py index becabf1..4b95651 100644 --- a/site/cds_rdm/ldap/api.py +++ b/site/cds_rdm/ldap/api.py @@ -18,7 +18,7 @@ from invenio_users_resources.services.users.tasks import reindex_users from cds_rdm.ldap.client import LdapClient -from cds_rdm.ldap.user_importer import LdapUserImporter +from cds_rdm.ldap.user_importer import LdapUserImporter, update_or_create_names_vocabularies from cds_rdm.ldap.utils import InvenioUser, serialize_ldap_user, user_exists @@ -102,6 +102,17 @@ def update_invenio_users_from_ldap(remote_accounts, ldap_users_map, log_func): if has_changed: invenio_user.update(ldap_user) + try: + update_or_create_names_vocabularies(ldap_user) + except Exception as e: + log_func( + "update_names_vocabularies_error", + dict( + user_id=invenio_user.user_id, + person_id=ldap_user["remote_account_person_id"], + ), + is_error=True, + ) user_ids.append(invenio_user.user_id) db.session.commit() log_func( diff --git a/site/cds_rdm/ldap/client.py b/site/cds_rdm/ldap/client.py index 8f40bb2..5b87bd8 100644 --- a/site/cds_rdm/ldap/client.py +++ b/site/cds_rdm/ldap/client.py @@ -41,6 +41,8 @@ class LdapClient(object): "uidNumber", "cn", "name", + "sn", + "givenName", ] def __init__(self, ldap_url=None): diff --git a/site/cds_rdm/ldap/user_importer.py b/site/cds_rdm/ldap/user_importer.py index 485b58a..ba3d60c 100644 --- a/site/cds_rdm/ldap/user_importer.py +++ b/site/cds_rdm/ldap/user_importer.py @@ -13,8 +13,46 @@ from invenio_db import db from invenio_oauthclient.models import RemoteAccount, UserIdentity from invenio_userprofiles.models import UserProfile +from invenio_records_resources.proxies import current_service_registry +from invenio_access.permissions import system_identity +from sqlalchemy.orm.exc import NoResultFound +def update_or_create_names_vocabularies(ldap_user): + """Update names vocabularies.""" + names_service = current_service_registry.get("names") + name_data = { + "id": ldap_user["remote_account_person_id"], + "name": ldap_user["user_profile_full_name"], + "given_name": ldap_user["given_name"], + "family_name": ldap_user["family_name"], + "props": { + "email": ldap_user["user_email"], + "username": ldap_user["user_username"], + "department": ldap_user["remote_account_department"], + "is_cern": True, + }, + "affiliations": [{"name": "CERN"}], + } + + try: + fetched_name = names_service.read(system_identity, ldap_user["remote_account_person_id"]) + # Determine if any updates are necessary + fetched_name_dict = fetched_name.to_dict() + update_needed = False + for key, value in name_data.items(): + if key not in fetched_name_dict or fetched_name_dict[key] != value: + update_needed = True + break + + # Perform update only if necessary + if update_needed: + name = names_service.update(system_identity, fetched_name.id, name_data) + except NoResultFound: + name = names_service.create(system_identity, name_data) + + return name + class LdapUserImporter: """Import ldap users to Invenio RDM records. @@ -70,7 +108,7 @@ def create_invenio_remote_account(self, user_id, ldap_user): keycloak_id=keycloak_id, person_id=employee_id, department=department ), ) - + def import_user(self, ldap_user): """Create Invenio users from LDAP export.""" user = self.create_invenio_user(ldap_user) @@ -85,6 +123,8 @@ def import_user(self, ldap_user): remote_account = self.create_invenio_remote_account(user_id, ldap_user) db.session.add(remote_account) + update_or_create_names_vocabularies(ldap_user) + # Automatically confirm the user confirm_user(user) return user_id diff --git a/site/cds_rdm/ldap/utils.py b/site/cds_rdm/ldap/utils.py index c8e6e86..80e0364 100644 --- a/site/cds_rdm/ldap/utils.py +++ b/site/cds_rdm/ldap/utils.py @@ -14,6 +14,7 @@ from invenio_userprofiles import UserProfile from cds_rdm.ldap.errors import InvalidLdapUser +import hashlib def serialize_ldap_user(ldap_user_data, log_func=None): @@ -33,6 +34,8 @@ def serialize(ldap_user_data): cern_account_type=decoded_data["cernAccountType"], remote_account_person_id=str(decoded_data["employeeID"]), remote_account_department=decoded_data["department"], + family_name=decoded_data["sn"], + given_name=decoded_data["givenName"], ) return serialized_data @@ -125,3 +128,14 @@ def update(self, ldap_user): self.user.email = ldap_user["user_email"] self.user.username = ldap_user["user_username"] self.user_profile.full_name = ldap_user["user_profile_full_name"] + + +def hash_value(value, length=16): + """Return a hashed version of the value.""" + # TODO: we should use a secret to encrypt the value (the repo is public) + value_bytes = value.encode('utf-8') + hash_object = hashlib.sha256() + hash_object.update(value_bytes) + hashed_id = hash_object.hexdigest()[:length] + + return hashed_id \ No newline at end of file diff --git a/site/cds_rdm/oidc.py b/site/cds_rdm/oidc.py index eb88cc6..15150ca 100644 --- a/site/cds_rdm/oidc.py +++ b/site/cds_rdm/oidc.py @@ -14,10 +14,25 @@ from invenio_oauthclient.contrib.keycloak.handlers import get_user_info from invenio_userprofiles.forms import confirm_register_form_preferences_factory from werkzeug.local import LocalProxy +from invenio_records_resources.proxies import current_service_registry +from invenio_access.permissions import system_identity _security = LocalProxy(lambda: current_app.extensions["security"]) +def sync_names(orcid, person_id): + """Sync names by merging the ORCID and CERN data.""" + names_service = current_service_registry.get("names") + cern_name = names_service.read(system_identity, person_id) + orcid_name = names_service.resolve(system_identity, orcid, "orcid") + orcid_data = orcid_name.to_dict() + cern_data = cern_name.to_dict() + orcid_data["props"] = cern_data["props"] + # Merge the extra props in the ORCID record and delete the CERN one + names_service.update(system_identity, orcid_name.id, orcid_data) + names_service.delete(system_identity, cern_name.id) + + def confirm_registration_form(*args, **kwargs): """Confirm form.""" Form = confirm_register_form_preferences_factory(_security.confirm_register_form) @@ -44,17 +59,22 @@ def cern_groups_serializer(remote, groups, **kwargs): def cern_setup_handler(remote, token, resp): """Perform additional setup after the user has been logged in.""" token_user_info, _ = get_user_info(remote, resp) - + cern_person_id = token_user_info.get("cern_person_id", None) + orcid = token_user_info.get("eduperson_orcid", None) + if orcid and cern_person_id: + sync_names(orcid, cern_person_id) with db.session.begin_nested(): # fetch the user's Keycloak ID and set it in extra_data keycloak_id = token_user_info["sub"] token.remote_account.extra_data = {"keycloak_id": keycloak_id} # only available to CERN users - cern_person_id = token_user_info.get("cern_person_id", None) if cern_person_id: token.remote_account.extra_data["person_id"] = cern_person_id + if orcid: + token.remote_account.extra_data["orcid"] = orcid + user = token.remote_account.user external_id = {"id": keycloak_id, "method": remote.name}