From d36f07db8ed17301720d1eba32ed9cc2c5f58e9b Mon Sep 17 00:00:00 2001 From: Johannes Lares Date: Mon, 8 May 2023 13:25:48 +0300 Subject: [PATCH 1/6] Add record ownership automatically --- .../modules/management/ownership/errors.py | 28 +++++++++ b2share/modules/management/ownership/ext.py | 2 + .../modules/management/ownership/receivers.py | 39 ++++++++++++ b2share/modules/management/ownership/utils.py | 59 +++++++++++++++++++ .../management/test_ownership_receivers.py | 40 +++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 b2share/modules/management/ownership/errors.py create mode 100644 b2share/modules/management/ownership/receivers.py create mode 100644 b2share/modules/management/ownership/utils.py create mode 100644 tests/b2share_unit_tests/management/test_ownership_receivers.py diff --git a/b2share/modules/management/ownership/errors.py b/b2share/modules/management/ownership/errors.py new file mode 100644 index 000000000..7db9c8298 --- /dev/null +++ b/b2share/modules/management/ownership/errors.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# This file is part of EUDAT B2Share. +# Copyright (C) 2023 CSC, EUDAT ltd. +# +# B2Share is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# B2Share is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with B2Share; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""Error classes for B2SHARE ownership module""" + +class UserAlreadyOwner(Exception): + """User is already an owner of a record""" + pass diff --git a/b2share/modules/management/ownership/ext.py b/b2share/modules/management/ownership/ext.py index 93a486939..680673027 100644 --- a/b2share/modules/management/ownership/ext.py +++ b/b2share/modules/management/ownership/ext.py @@ -27,6 +27,8 @@ from .views import blueprint from .cli import ownership as ownership_cmd +# from .receivers import register_receivers +from .receivers import * class _B2ShareOwnership(object): diff --git a/b2share/modules/management/ownership/receivers.py b/b2share/modules/management/ownership/receivers.py new file mode 100644 index 000000000..75219ae3e --- /dev/null +++ b/b2share/modules/management/ownership/receivers.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# +# This file is part of EUDAT B2Share. +# Copyright (C) 2023 CSC, EUDAT ltd. +# +# B2Share is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# B2Share is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with B2Share; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""Function registering signal events""" + +from invenio_records.signals import before_record_insert +from invenio_accounts.models import User +from flask import current_app + +@before_record_insert.connect +def add_ownership_automatically(sender, **kwargs): + for email in current_app.config.get('DEFAULT_OWNERSHIP', []): + user = User.query.filter(User.email == email).one_or_none() + if user == None: + current_app.logger.error(f"User not found: {email}") + continue + print(f"Adding ownership to a new record for user: {user.id}") + if user.id not in sender['_deposit']['owners']: + sender['_deposit']['owners'].append(user.id) diff --git a/b2share/modules/management/ownership/utils.py b/b2share/modules/management/ownership/utils.py new file mode 100644 index 000000000..97d17a251 --- /dev/null +++ b/b2share/modules/management/ownership/utils.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# This file is part of EUDAT B2Share. +# Copyright (C) 2023 CSC, EUDAT ltd. +# +# B2Share is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# B2Share is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with B2Share; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""Utility functions for B2SHARE ownership module""" + +from invenio_db import db +from invenio_pidstore.models import PersistentIdentifier +from b2share.modules.records.api import B2ShareRecord + +from .errors import UserAlreadyOwner + +def get_record_by_pid(pid): + """Get a record by a pid + + :param pid: Record b2rec PID + :return: B2ShareRecord object + """ + pid = PersistentIdentifier.get('b2rec', pid) + return B2ShareRecord.get_record(pid.object_uuid) + +def add_ownership_to_record(rec, user_id): + """Add user as an owner of a record + + :param rec: A B2ShareRecord object + :param user_id: Users id which to add + :raise UserAlreadyOwner: When an user is already an owner of the record + :raise Exception: When something goes horribly wrong + """ + if user_id not in rec['_deposit']['owners']: + rec['_deposit']['owners'].append(user_id) + try: + rec = rec.commit() + db.session.commit() + except Exception as e: + db.session.rollback() + raise Exception(e) + + else: + raise UserAlreadyOwner(rec['_deposit']['id']) diff --git a/tests/b2share_unit_tests/management/test_ownership_receivers.py b/tests/b2share_unit_tests/management/test_ownership_receivers.py new file mode 100644 index 000000000..2f3bb55c8 --- /dev/null +++ b/tests/b2share_unit_tests/management/test_ownership_receivers.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# This file is part of EUDAT B2Share. +# Copyright (C) 2023 CSC, EUDAT ltd. +# +# B2Share is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# B2Share is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with B2Share; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. + +"""Test B2Share Ownership receivers module.""" + +from b2share_unit_tests.helpers import create_user, create_deposit + +from b2share.modules.management.ownership.receivers import add_ownership_automatically + +def test_record_ownership_added(app, test_records_data): + with app.app_context(): + curator = create_user('curator') + app.config['DEFAULT_OWNERSHIP'] = ['curator@example.org'] + r1 = test_records_data[0] + rec = create_deposit(r1) + assert len(rec['_deposit']['owners']) == 1 + assert rec['_deposit']['owners'][0] == curator.id + +def test_record_ownership_faulty_emails(app, test_records_data): + with app.app_context(): + app.config['DEFAULT_OWNERSHIP'] = ['curator@organization.org'] + r1 = test_records_data[0] + rec = create_deposit(r1) + assert len(rec['_deposit']['owners']) == 0 From 8a2f6b0e3fa78864d6ea4da9e37afab5a9776ddd Mon Sep 17 00:00:00 2001 From: Johannes Lares Date: Thu, 6 Jul 2023 13:56:06 +0300 Subject: [PATCH 2/6] Fix case sensitivity on user query --- b2share/modules/management/ownership/receivers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/b2share/modules/management/ownership/receivers.py b/b2share/modules/management/ownership/receivers.py index 75219ae3e..2012cc80d 100644 --- a/b2share/modules/management/ownership/receivers.py +++ b/b2share/modules/management/ownership/receivers.py @@ -26,11 +26,12 @@ from invenio_records.signals import before_record_insert from invenio_accounts.models import User from flask import current_app +from sqlalchemy import func @before_record_insert.connect def add_ownership_automatically(sender, **kwargs): for email in current_app.config.get('DEFAULT_OWNERSHIP', []): - user = User.query.filter(User.email == email).one_or_none() + user = User.query.filter(func.lower(User.email) == email.lower()).one_or_none() if user == None: current_app.logger.error(f"User not found: {email}") continue From 6d208015915b9f38734298eba6c684c9766ca025 Mon Sep 17 00:00:00 2001 From: Johannes Lares Date: Wed, 24 May 2023 13:10:47 +0300 Subject: [PATCH 3/6] Add ownership modification feature * Backend with GET, PUT and DELETE * Frontend modal * Modified record bottom buttons for better mobile UX --- b2share/config.py | 2 + b2share/modules/management/ownership/cli.py | 88 ++-- b2share/modules/management/ownership/utils.py | 46 +- b2share/modules/management/ownership/views.py | 106 +++-- ci-docker/docker-test.sh | 45 ++ .../management/test_ownership_api.py | 396 +++++++++++------- webui/app/css/app.css | 25 +- webui/src/components/ownership.jsx | 130 ++++++ webui/src/components/record.jsx | 30 +- webui/src/data/server.js | 257 +++++++----- webui/webpack.config.devel.js | 1 - 11 files changed, 762 insertions(+), 364 deletions(-) create mode 100755 ci-docker/docker-test.sh create mode 100644 webui/src/components/ownership.jsx diff --git a/b2share/config.py b/b2share/config.py index 601b91a0f..74598c9c4 100644 --- a/b2share/config.py +++ b/b2share/config.py @@ -495,3 +495,5 @@ #: There is no password so don't send password change emails SECURITY_SEND_PASSWORD_CHANGE_EMAIL=False SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL=False + +DEFAULT_OWNERSHIP=['johannes.lares@csc.fi'] diff --git a/b2share/modules/management/ownership/cli.py b/b2share/modules/management/ownership/cli.py index 31ae222de..8ffeb2c02 100644 --- a/b2share/modules/management/ownership/cli.py +++ b/b2share/modules/management/ownership/cli.py @@ -43,19 +43,17 @@ from b2share.utils import ESSearch, to_tabulate from b2share.modules.users.cli import get_user +from .errors import UserAlreadyOwner +from .utils import get_record_by_pid, add_ownership_to_record, find_version_master, check_user + def render_error(error, type, pid, action): raise click.ClickException(click.style(error, fg="red") + "\t\t{}: ".format(type) + click.style(pid, fg="blue") + "\t\tAction: {}".format(action)) -def pid2record(pid): - pid = PersistentIdentifier.get('b2rec', pid) - return B2ShareRecord.get_record(pid.object_uuid) - - def replace_ownership(pid, user_id: int, obj_type='record'): - record = pid2record(pid) + record = get_record_by_pid(pid) record['_deposit']['owners'] = [user_id] try: record = record.commit() @@ -63,25 +61,20 @@ def replace_ownership(pid, user_id: int, obj_type='record'): except Exception as e: db.session.rollback() click.secho( - "Error for object {}, skipping".format(obj.id), fg='red') + "Error for object {}, skipping".format(pid), fg='red') click.secho(e) def add_ownership(obj, user_id: int, obj_type='record'): - if user_id not in obj['_deposit']['owners']: - obj['_deposit']['owners'].append(user_id) - try: - obj = obj.commit() - db.session.commit() - except Exception as e: - db.session.rollback() - click.secho( - "Error for object {}, skipping".format(obj.id), fg='red') - click.secho(e) - - else: + try: + add_ownership_to_record(obj, user_id) + except UserAlreadyOwner as e: render_error("User is already owner of object", 'object id', obj['_deposit']['id'], "skipping") + except Exception as e: + click.secho( + "Error for object {}, skipping".format(obj.id), fg='red') + click.secho(e) def remove_ownership(obj, user_id: int, obj_type='record'): @@ -109,7 +102,7 @@ def list_ownership(record_pid): all_pids = [v.pid_value for v in version_master.children.all()] click.secho("PID\t\t\t\t\tOwners", fg='green') for single_pid in all_pids: - record = pid2record(single_pid) + record = get_record_by_pid(single_pid) owners = record['_deposit']['owners'] click.secho("%s\t%s" % ( single_pid, @@ -117,9 +110,6 @@ def list_ownership(record_pid): User.id.in_([w])).all()[0].email) for w in owners]))) -def check_user(user_email): - return User.query.filter(User.email == user_email).one_or_none() - # decorator to patch the current_app.config file temporarely def patch_current_app_config(vars): def decorator(function): @@ -128,7 +118,8 @@ def inner(*args, **kwargs): v, None) for v in vars.keys()} for v, val in vars.items(): if val is None: - raise Exception("Value for var {} is None.\nSet up the variable to run the command.".format(str(v))) + raise Exception( + "Value for var {} is None.\nSet up the variable to run the command.".format(str(v))) current_app.config[v] = val out = function(*args, **kwargs) for v, val in old_vars.items(): @@ -143,23 +134,6 @@ def ownership(): """ownership management commands.""" -def find_version_master(pid): - """Retrieve the PIDVersioning of a record PID. - - :params pid: record PID. - """ - from b2share.modules.deposit.errors import RecordNotFoundVersioningError - from b2share.modules.records.providers import RecordUUIDProvider - try: - child_pid = RecordUUIDProvider.get(pid).pid - if child_pid.status == PIDStatus.DELETED: - raise RecordNotFoundVersioningError() - except PIDDoesNotExistError as e: - raise RecordNotFoundVersioningError() from e - - return PIDVersioning(child=child_pid) - - @ownership.command() @with_appcontext @click.argument('record-pid', required=True, type=str) @@ -207,7 +181,7 @@ def reset(record_pid, user_email, yes_i_know, quiet): @click.option('-q', '--quiet', is_flag=True, default=False) def add(record_pid, user_email, quiet): """ Add user as owner for all the version of the record. - + :params record-pid: B2rec record PID user-email: user email """ @@ -218,7 +192,7 @@ def add(record_pid, user_email, quiet): all_pids = [v.pid_value for v in version_master.children.all()] user = User.query.filter(User.email == user_email).one_or_none() for single_pid in all_pids: - record = pid2record(single_pid) + record = get_record_by_pid(single_pid) add_ownership(record, user.id) if not quiet: click.secho("Ownership Updated!", fg='red') @@ -232,7 +206,7 @@ def add(record_pid, user_email, quiet): @click.option('-q', '--quiet', is_flag=True, default=False) def remove(record_pid, user_email, quiet): """ Remove user as an owner of the record. - + :params record-pid: B2rec record PID user-email: user email """ @@ -244,7 +218,7 @@ def remove(record_pid, user_email, quiet): all_pids = [v.pid_value for v in version_master.children.all()] user = User.query.filter(User.email == user_email).one_or_none() for single_pid in all_pids: - record = pid2record(single_pid) + record = get_record_by_pid(single_pid) remove_ownership(record, user.id) if not quiet: click.secho("Ownership Updated!", fg='red') @@ -257,7 +231,7 @@ def remove(record_pid, user_email, quiet): @click.option('-t', '--type', type=click.Choice(['deposit', 'record']), required=False) def find(user_email, type=None): """ Find all the records or/and deposits where user is one of the owners. - + :params user-email: user email type: record type (deposit, record) """ @@ -276,7 +250,8 @@ def find(user_email, type=None): click.secho(click.style( "No objs found with owner {}".format(str(user)), fg="red")) -@ownership.command('add-all') + +@ownership.command('transfer-add') @with_appcontext @click.option('-t', '--type', type=click.Choice(['deposit', 'record']), required=False) @click.argument('user-email', required=True, type=str) @@ -284,10 +259,10 @@ def find(user_email, type=None): @patch_current_app_config({'SERVER_NAME': os.environ.get('JSONSCHEMAS_HOST')}) def transfer_add(user_email, new_user_email, type=None): """ Add user to all the records or/and deposits if user is not of the owners already. - - :param user-email: user email of the owner of the records/deposits - :param new-user-email: user email of the new owner - :param type: record type (deposit, record) + + :params user-email: user email of the old owner + new-user-email: user email of the new owner + type: record type (deposit, record) """ if check_user(user_email) is None: raise click.ClickException( @@ -308,7 +283,7 @@ def transfer_add(user_email, new_user_email, type=None): click.secho(click.style("Initial state:", fg="blue")) print(to_tabulate(search)) - #update values for each record/deposit + # update values for each record/deposit for id in search.keys(): obj = Deposit.get_record(id) # try to update @@ -338,9 +313,9 @@ def transfer_add(user_email, new_user_email, type=None): @patch_current_app_config({'SERVER_NAME': os.environ.get('JSONSCHEMAS_HOST')}) def transfer_remove(user_email, type=None): """ remove user to all the records or/and deposits. - - :param user-email: user email - :param type: record type (deposit, record) + + :params user-email: user email + type: record type (deposit, record) """ if check_user(user_email) is None: raise click.ClickException( @@ -356,7 +331,7 @@ def transfer_remove(user_email, type=None): click.secho(click.style("Initial state:", fg="blue")) print(to_tabulate(search)) - #update values for each record/deposit + # update values for each record/deposit for id in search.keys(): obj = Deposit.get_record(id) # try to delete user @@ -402,4 +377,3 @@ def search_es(user, type): else: click.secho("The search has returned 0 maches.") return None - diff --git a/b2share/modules/management/ownership/utils.py b/b2share/modules/management/ownership/utils.py index 97d17a251..2354bd002 100644 --- a/b2share/modules/management/ownership/utils.py +++ b/b2share/modules/management/ownership/utils.py @@ -24,7 +24,10 @@ """Utility functions for B2SHARE ownership module""" from invenio_db import db -from invenio_pidstore.models import PersistentIdentifier +from invenio_pidstore.models import PersistentIdentifier, PIDStatus +from invenio_pidrelations.contrib.versioning import PIDVersioning +from invenio_pidstore.errors import PIDDoesNotExistError, PIDMissingObjectError +from invenio_accounts.models import User from b2share.modules.records.api import B2ShareRecord from .errors import UserAlreadyOwner @@ -36,6 +39,8 @@ def get_record_by_pid(pid): :return: B2ShareRecord object """ pid = PersistentIdentifier.get('b2rec', pid) + if not pid.object_uuid: + raise PIDMissingObjectError(pid) return B2ShareRecord.get_record(pid.object_uuid) def add_ownership_to_record(rec, user_id): @@ -57,3 +62,42 @@ def add_ownership_to_record(rec, user_id): else: raise UserAlreadyOwner(rec['_deposit']['id']) + +def find_version_master(pid): + """Retrieve the PIDVersioning of a record PID. + + :params pid: record b2rec PID. + """ + from b2share.modules.deposit.errors import RecordNotFoundVersioningError + from b2share.modules.records.providers import RecordUUIDProvider + try: + child_pid = RecordUUIDProvider.get(pid).pid + if child_pid.status == PIDStatus.DELETED: + raise RecordNotFoundVersioningError() + except PIDDoesNotExistError as e: + raise RecordNotFoundVersioningError() from e + + return PIDVersioning(child=child_pid) + +def check_user(user_email): + """Check if user exists + + :param user_email: Email of the user to be searched + :return: user object + """ + return User.query.filter(User.email == user_email).one_or_none() + +def remove_ownership(obj, user_id: int): + if user_id in obj['_deposit']['owners']: + if len(obj['_deposit']['owners']) > 1: + try: + obj['_deposit']['owners'].remove(user_id) + obj = obj.commit() + db.session.commit() + except ValueError as e: + db.session.rollback() + raise ValueError() from e + else: + raise Exception("Record has to have at least one owner") + else: + raise Exception("User is not owner of the record") diff --git a/b2share/modules/management/ownership/views.py b/b2share/modules/management/ownership/views.py index f1951ede3..5db57da32 100644 --- a/b2share/modules/management/ownership/views.py +++ b/b2share/modules/management/ownership/views.py @@ -40,20 +40,17 @@ from invenio_pidstore.errors import PIDDoesNotExistError from b2share.utils import get_base_url -from b2share.modules.management.ownership.cli import find_version_master, pid2record +from .utils import get_record_by_pid, find_version_master, remove_ownership blueprint = Blueprint('b2share_ownership', __name__) -#@beartype - - def add_ownership(pid, user_id: int): """ Set user_id as a new owner for the record (given the record pid) :params pid : record pid user_id : id of the user to add as a owner """ - record = pid2record(pid) + record = get_record_by_pid(pid) if user_id in record['_deposit']['owners']: current_app.logger.warning( "OWN-API: User is already an owner of the record, skipping..", exc_info=True) @@ -73,25 +70,39 @@ def add_ownership(pid, user_id: int): def pass_user_email(f): """Decorator to retrieve a user.""" @wraps(f) - def inner(self, record, user_email, *args, **kwargs): + def inner(self, record, *args, **kwargs): + user_email = request.args.get('email') user = User.query.filter(User.email == user_email).one_or_none() if user is None: - abort(400, description="User not found in the db.") + abort(400, description="Email not found or not added.") return f(self, record, user, *args, **kwargs) return inner +def pass_user_emails(f): + """Decorator to retrieve user emails.""" + @wraps(f) + def inner(self, record, *args, **kwargs): + emails = [] + for uid in record['_deposit']['owners']: + user = User.query.filter(User.id == uid).one_or_none() + if user is None: + # ERROR + continue + emails.append(user.email) + return f(self, record, emails, *args, **kwargs) + return inner def pass_record(f): """Decorator to retrieve a record.""" @wraps(f) - def inner(self, record_pid, user_email, *args, **kwargs): + def inner(self, record_pid, *args, **kwargs): try: - record = pid2record(record_pid) + record = get_record_by_pid(record_pid) except PIDDoesNotExistError as e: abort(404) if record is None: abort(404) - return f(self, record, user_email, *args, **kwargs) + return f(self, record, *args, **kwargs) return inner @@ -126,43 +137,62 @@ def __init__(self, *args, **kwargs): }, default_media_type='application/json', *args, **kwargs) - # @auth_required('token', 'session') - # @pass_record - # @pass_user_email - # @pass_record_ownership - # def put(self, record, user, **kwargs): - # """Set user_id as a new owner of the record - # The function will retrive all the version of the - # record and will update the ownership for all of them - # """ - # record_pid = record['_deposit']['pid']['value'] - # user_id = user.id - # version_master = find_version_master(record_pid) - # all_pids = [v.pid_value for v in version_master.children.all()] - # for single_pid in all_pids: - # add_ownership(single_pid, user_id) - # return make_response(("User {} is now owner of the records {}".format(user.email, " - ".join(all_pids)), 200)) - @auth_required('token', 'session') @pass_record @pass_user_email @pass_record_ownership - def get(self, record, user, **kwargs): + def put(self, record, user, **kwargs): + """Set user_id as a new owner of the record + The function will retrive all versions of the + record and will update the ownership for all of them + """ + record_pid = record['_deposit']['pid']['value'] + user_id = user.id + version_master = find_version_master(record_pid) + all_pids = [v.pid_value for v in version_master.children.all()] + for single_pid in all_pids: + add_ownership(single_pid, user_id) + return make_response(jsonify({ 'success': True }), 200) + + @auth_required('token', 'session') + @pass_record + @pass_user_emails + @pass_record_ownership + def get(self, record, users, **kwargs): """ Test function to check if authn is working. """ """ Record and User are resolved using the decorators """ - # return make_response(("You can modify the file!
PID: {}
new owner: {}
previous owners: {}".format( - # str(record['_deposit']['pid']['value']), - # str(user.email), - # ", ".join([str(User.query.filter( - # User.id.in_([i])).all()[0].email) for i in record['_deposit']['owners']]) - - # ), 200)) - abort(501, description="Ownership API not available at the moment") - #return make_response(("API not available at the moment"), 501) + ret = [] + presets = current_app.config.get('DEFAULT_OWNERSHIP', []) + for e in users: + o = {'email': e} + if e in presets: + o['preset'] = True + ret.append(o) + return make_response(jsonify(ret), 200) + + @auth_required('token', 'session') + @pass_record + @pass_user_email + @pass_record_ownership + def delete(self, record, user, **kwargs): + """Remove owner from a record""" + user_id = user.id + try: + remove_ownership(record, user_id) + # TODO: What should be returned + return make_response(jsonify({'success': True}), 200) + except ValueError as e: + print(e) + # TODO + return make_response(f'{e}', 400) + except Exception as e: + print(e) + # TODO + return make_response(f'{e}', 400) blueprint.add_url_rule( - '/records//ownership/', + '/records//ownership', view_func=OwnershipRecord.as_view( OwnershipRecord.view_name ) diff --git a/ci-docker/docker-test.sh b/ci-docker/docker-test.sh new file mode 100755 index 000000000..92437e80b --- /dev/null +++ b/ci-docker/docker-test.sh @@ -0,0 +1,45 @@ +#!/bin/bash +mkdir -p logs +docker-compose -f docker-compose.test.yml up -d +sleep 60 + +docker ps -a +ID=$(docker container ls -qf "name=tests") + +while ! docker logs --tail 1 "$ID" | grep 'TESTS READY' +do + printf "\n" + echo "Waiting for tests to complete, please be patient. Status:" + docker-compose -f docker-compose.test.yml logs --tail=2 b2share-test + sleep 60 +done + +echo "Tests has completed" + +docker logs -t tests > ../logs.log + +docker cp "$ID":/eudat/b2share/coverage.xml ../coverage.xml +docker cp "$ID":/eudat/b2share/junit.xml ../junit.xml + +docker-compose -f docker-compose.test.yml down + +cat ../logs.log + +if grep -q "[1-9]\d* failed" ../logs.log || grep -q "[1-9]\d* error" ../logs.log +then + echo "We have fails or errors!" + grep "[1-9]\d* failed" ../logs.log + grep "[1-9]\d* error" ../logs.log + exit 1 +elif grep -q "[1-9]\d* xfailed" ../logs.log +then + echo "We have accepted fails (xfailed)!" + grep "[1-9]\d* xfailed" ../logs.log + exit 22 +elif grep -q "[1-9]\d* passed" ../logs.log +then + echo "No failures!" +else + echo "ERROR" + exit 1 +fi diff --git a/tests/b2share_unit_tests/management/test_ownership_api.py b/tests/b2share_unit_tests/management/test_ownership_api.py index 931289eba..dccfc541f 100644 --- a/tests/b2share_unit_tests/management/test_ownership_api.py +++ b/tests/b2share_unit_tests/management/test_ownership_api.py @@ -1,192 +1,270 @@ # -*- coding: utf-8 -*- -# + # This file is part of EUDAT B2Share. # Copyright (C) 2016 CERN. -# + # B2Share is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of the # License, or (at your option) any later version. -# + # B2Share is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# + # You should have received a copy of the GNU General Public License # along with B2Share; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. -# + # In applying this license, CERN does not # waive the privileges and immunities granted to it by virtue of its status # as an Intergovernmental Organization or submit itself to any jurisdiction. """Test B2Share Ownership """ +import json from flask import url_for from urllib import parse from b2share_unit_tests.helpers import create_record from b2share_unit_tests.helpers import create_user -from b2share.modules.management.ownership.cli import find_version_master, pid2record +from b2share.modules.management.ownership.utils import find_version_master from invenio_records_files.api import Record - -# COMMENTED OUT: API NOT USED AT THE MOMENT - -# def test_ownership_is_user_allowed_to_modify(app, login_user, test_records_data): -# """Test if an authorized user is allowed to change the ownership.""" - -# with app.app_context(): -# creator = create_user('creator') -# _deposit, pid, _record = create_record(test_records_data[0], creator) - -# new_owner = create_user('new_owner') - -# with app.test_client() as client: -# if creator is not None: -# login_user(creator, client) -# url = url_for('b2share_ownership.record_ownership', -# record_pid=pid.pid_value, user_email=new_owner.email, _external=True) -# headers = [('Accept', '*/*'), ('Content-Length', '0'), -# ('Accept-Encoding', 'gzip, deflate, br')] -# url = parse.unquote(url) -# res = client.get(url, headers=headers) -# # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. -# assert 200 == res.status_code - - -# def test_ownership_modify(app, login_user, test_records_data): -# """Test if the owner of a record can add another existing user as owner of the record. -# -# Test if owner can add an user that is already owner for that record. -# """ - -# with app.app_context(): -# creator = create_user('creator') -# _deposit, pid, _record = create_record(test_records_data[0], creator) -# # create a new version of the same record -# _deposit_v2, pid_v2, _record_v2 = create_record(test_records_data[0], creator, version_of=pid.pid_value) - -# new_owner = create_user('new_owner') - -# with app.test_client() as client: -# if creator is not None: -# login_user(creator, client) -# url = url_for('b2share_ownership.record_ownership', -# record_pid=pid.pid_value, user_email=new_owner.email, _external=True) -# url = parse.unquote(url) -# headers = [('Accept', '*/*'), ('Content-Length', '0'), -# ('Accept-Encoding', 'gzip, deflate, br')] -# res = client.put(url, headers=headers) -# # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. -# assert 200 == res.status_code -# record = Record.get_record(pid.object_uuid) -# assert new_owner.id in record['_deposit']['owners'] -# assert creator.id in record['_deposit']['owners'] - -# # now we try to check if we can add the user again -# res = client.put(url, headers=headers) -# assert 400 == res.status_code -# record = Record.get_record(pid.object_uuid) -# assert new_owner.id in record['_deposit']['owners'] -# assert creator.id in record['_deposit']['owners'] -# assert len(record['_deposit']['owners']) == 2 +def test_ownership_is_user_allowed_to_modify(app, login_user, test_records_data): + """Test if an authorized user is allowed to change the ownership.""" + + with app.app_context(): + creator = create_user('creator') + _deposit, pid, _record = create_record(test_records_data[0], creator) + + new_owner = create_user('new_owner') + + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, user_email=new_owner.email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.get(url, headers=headers) + # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. + assert 200 == res.status_code + + +def test_ownership_modify(app, login_user, test_records_data): + """Test if the owner of a record can add another existing user as owner of the record. + + Test if owner can add an user that is already owner for that record. + """ + + with app.app_context(): + creator = create_user('creator') + _deposit, pid, _record = create_record(test_records_data[0], creator) + # create a new version of the same record + _deposit_v2, pid_v2, _record_v2 = create_record(test_records_data[0], creator, version_of=pid.pid_value) + + new_owner = create_user('new_owner') + + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, email=new_owner.email, _external=True) + url = parse.unquote(url) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + res = client.put(url, headers=headers) + # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. + assert 200 == res.status_code + record = Record.get_record(pid.object_uuid) + assert new_owner.id in record['_deposit']['owners'] + assert creator.id in record['_deposit']['owners'] + + # now we try to check if we can add the user again + res = client.put(url, headers=headers) + assert 400 == res.status_code + record = Record.get_record(pid.object_uuid) + assert new_owner.id in record['_deposit']['owners'] + assert creator.id in record['_deposit']['owners'] + assert len(record['_deposit']['owners']) == 2 -# # check if the changes are applied to all the record versions -# version_master = find_version_master(pid.pid_value) -# all_record_versions = version_master.children.all() -# all_records=[Record.get_record(single_pid.object_uuid) for single_pid in all_record_versions] -# for record_v in all_records: -# assert new_owner.id in record_v['_deposit']['owners'] -# assert creator.id in record_v['_deposit']['owners'] -# assert len(record_v['_deposit']['owners']) == 2 + # check if the changes are applied to all the record versions + version_master = find_version_master(pid.pid_value) + all_record_versions = version_master.children.all() + all_records=[Record.get_record(single_pid.object_uuid) for single_pid in all_record_versions] + for record_v in all_records: + assert new_owner.id in record_v['_deposit']['owners'] + assert creator.id in record_v['_deposit']['owners'] + assert len(record_v['_deposit']['owners']) == 2 -# def test_ownership_modify_not_existing_pid(app, login_user, test_records_data): -# """Test if owner try to get an not existing record.""" - -# with app.app_context(): -# creator = create_user('creator') -# fake_pid = "00000000000000000000000000000000" -# new_owner = create_user('new_owner') - -# with app.test_client() as client: -# if creator is not None: -# login_user(creator, client) -# url = url_for('b2share_ownership.record_ownership', -# record_pid=fake_pid, user_email=new_owner.email, _external=True) -# headers = [('Accept', '*/*'), ('Content-Length', '0'), -# ('Accept-Encoding', 'gzip, deflate, br')] -# url = parse.unquote(url) -# res = client.put(url, headers=headers) -# assert 404 == res.status_code - - -# def test_ownership_modify_not_existing_owner(app, login_user, test_records_data): -# """Test if owner try to add a not existing user.""" - -# with app.app_context(): -# creator = create_user('creator') -# _deposit, pid, _record = create_record(test_records_data[0], creator) - -# fake_email = "fake_email@testing.csc" - -# with app.test_client() as client: -# if creator is not None: -# login_user(creator, client) -# url = url_for('b2share_ownership.record_ownership', -# record_pid=pid.pid_value, user_email=fake_email, _external=True) -# headers = [('Accept', '*/*'), ('Content-Length', '0'), -# ('Accept-Encoding', 'gzip, deflate, br')] -# url = parse.unquote(url) -# res = client.put(url, headers=headers) -# assert 400 == res.status_code - - -# def test_ownership_modify_unauthorized_user(app, login_user, test_records_data): -# """Test if a logged in user that is not the owner try to add an existing user as owner of the record.""" - -# with app.app_context(): -# creator = create_user('creator') -# _deposit, pid, _record = create_record(test_records_data[0], creator) - -# new_owner = create_user('new_owner') -# not_the_owner = create_user('not_the_owner') - -# with app.test_client() as client: -# if not_the_owner is not None: -# login_user(not_the_owner, client) -# url = url_for('b2share_ownership.record_ownership', -# record_pid=pid.pid_value, user_email=new_owner.email, _external=True) -# headers = [('Accept', '*/*'), ('Content-Length', '0'), -# ('Accept-Encoding', 'gzip, deflate, br')] -# url = parse.unquote(url) -# res = client.put(url, headers=headers) -# assert 403 == res.status_code -# record = Record.get_record(pid.object_uuid) -# assert new_owner.id not in record['_deposit']['owners'] -# assert not_the_owner.id not in record['_deposit']['owners'] -# assert creator.id in record['_deposit']['owners'] - - -# def test_ownership_modify_not_logged_in_user(app, login_user, test_records_data): -# """Test if a not logged in user can modify the record ownership.""" - -# with app.app_context(): -# creator = create_user('creator') -# _deposit, pid, _record = create_record(test_records_data[0], creator) - -# new_owner = create_user('new_owner') - -# with app.test_client() as client: -# url = url_for('b2share_ownership.record_ownership', -# record_pid=pid.pid_value, user_email=new_owner.email, _external=True) -# headers = [('Accept', '*/*'), ('Content-Length', '0'), -# ('Accept-Encoding', 'gzip, deflate, br')] -# url = parse.unquote(url) -# res = client.put(url, headers=headers) -# assert 401 == res.status_code +def test_ownership_modify_not_existing_pid(app, login_user, test_records_data): + """Test if owner try to get an not existing record.""" + + with app.app_context(): + creator = create_user('creator') + fake_pid = "00000000000000000000000000000000" + new_owner = create_user('new_owner') + + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=fake_pid, email=new_owner.email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.put(url, headers=headers) + assert 404 == res.status_code + + +def test_ownership_modify_not_existing_owner(app, login_user, test_records_data): + """Test if owner try to add a not existing user.""" + + with app.app_context(): + creator = create_user('creator') + _deposit, pid, _record = create_record(test_records_data[0], creator) + + fake_email = "fake_email@testing.csc" + + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, email=fake_email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.put(url, headers=headers) + assert 400 == res.status_code + + +def test_ownership_modify_unauthorized_user(app, login_user, test_records_data): + """Test if a logged in user that is not the owner try to add an existing user as owner of the record.""" + + with app.app_context(): + creator = create_user('creator') + _deposit, pid, _record = create_record(test_records_data[0], creator) + + new_owner = create_user('new_owner') + not_the_owner = create_user('not_the_owner') + + with app.test_client() as client: + if not_the_owner is not None: + login_user(not_the_owner, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, email=new_owner.email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.put(url, headers=headers) + assert 403 == res.status_code + record = Record.get_record(pid.object_uuid) + assert new_owner.id not in record['_deposit']['owners'] + assert not_the_owner.id not in record['_deposit']['owners'] + assert creator.id in record['_deposit']['owners'] + + +def test_ownership_modify_not_logged_in_user(app, login_user, test_records_data): + """Test if a not logged in user can modify the record ownership.""" + + with app.app_context(): + creator = create_user('creator') + _deposit, pid, _record = create_record(test_records_data[0], creator) + + new_owner = create_user('new_owner') + + with app.test_client() as client: + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, email=new_owner.email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.put(url, headers=headers) + assert 401 == res.status_code + +def test_ownership_with_preset(app, login_user, test_records_data): + with app.app_context(): + creator = create_user('creator') + app.config['DEFAULT_OWNERSHIP'] = [creator.email] + new_owner = create_user('new_owner') + _deposit, pid, _record = create_record(test_records_data[0], new_owner) + + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.get(url, headers=headers) + # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. + data = json.loads(res.data) + assert 200 == res.status_code + assert 2 == len(data) + found = False + for x in data: + if x['email'] == creator.email: + if x['preset']: + found = True + assert found + +def test_ownership_delete(app, login_user, test_records_data): + with app.app_context(): + creator = create_user('creator') + app.config['DEFAULT_OWNERSHIP'] = [creator.email] + new_owner = create_user('new_owner') + _deposit, pid, _record = create_record(test_records_data[0], new_owner) + + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, email=new_owner.email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.delete(url, headers=headers) + # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. + assert 200 == res.status_code + +def test_ownership_delete_no_owners_left(app, login_user, test_records_data): + with app.app_context(): + creator = create_user('creator') + _deposit, pid, _record = create_record(test_records_data[0], creator) + + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, email=creator.email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.delete(url, headers=headers) + # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. + assert 400 == res.status_code + +def test_ownership_delete_not_found_owner(app, login_user, test_records_data): + with app.app_context(): + creator = create_user('creator') + _deposit, pid, _record = create_record(test_records_data[0], creator) + new_owner = create_user('new_owner') + with app.test_client() as client: + if creator is not None: + login_user(creator, client) + url = url_for('b2share_ownership.record_ownership', + record_pid=pid.pid_value, email=new_owner.email, _external=True) + headers = [('Accept', '*/*'), ('Content-Length', '0'), + ('Accept-Encoding', 'gzip, deflate, br')] + url = parse.unquote(url) + res = client.delete(url, headers=headers) + # 200 = 3 conditions verified :logged in user is allowed to change ownership, record exists and also new user exists. + assert 400 == res.status_code diff --git a/webui/app/css/app.css b/webui/app/css/app.css index 42cdde403..982a562ff 100644 --- a/webui/app/css/app.css +++ b/webui/app/css/app.css @@ -1058,8 +1058,16 @@ a.navbar-brand { border: 1px solid #ddd; } -.large-record .bottom-buttons > *:not(:first-child) { - margin: 0em 0.5em; +@media (min-width: 580px) { + .large-record .bottom-buttons > *:not(:first-child) { + margin: 0em 0em 0em 1em; + } +} + +@media (max-width: 580px) { + .large-record .bottom-buttons > * { + margin: 0.5em 1em 0.5em 0em; + } } .large-record .bottom-buttons a.abuse { @@ -1432,3 +1440,16 @@ ul.rw-list>li.rw-list-option.rw-state-focus,.rw-selectlist>li.rw-list-option.rw- .stat>span:last-child { text-align: center; } + +/* OWNERSHIP */ + +.ownership-modal { + padding-bottom: 30px; + margin-top: 10%; + background-color: white; + overflow-y: auto; + box-shadow: 0px 0px 4px #000; + border-radius: 4px; + max-height: 80%; + bottom: auto !important; +} diff --git a/webui/src/components/ownership.jsx b/webui/src/components/ownership.jsx new file mode 100644 index 000000000..598c975a4 --- /dev/null +++ b/webui/src/components/ownership.jsx @@ -0,0 +1,130 @@ +import React from 'react/lib/ReactWithAddons'; +import { serverCache, notifications, browser, Error, apiUrls } from '../data/server'; + +export const Ownership = React.createClass({ + getInitialState() { + this.setWrapperRef = this.setWrapperRef.bind(this); + this.handleClickOutside = this.handleClickOutside.bind(this); + var state = { + owners: [] + } + + return state + }, + + getOwners() { + const addOwners = r => { + this.setState({ owners: r }) + } + serverCache.getRecordOwners(this.props.record.get('id'), addOwners) + }, + + componentDidMount() { + this.getOwners() + document.addEventListener("mousedown", this.handleClickOutside); + }, + + componentWillUnmount() { + document.removeEventListener("mousedown", this.handleClickOutside); + }, + + addOwnership() { + const ownershipSuccess = () => { + this.setState({ ownershipAdded: true }); + this.getOwners(); + } + console.log(this.state.ownershipEmail) + serverCache.addOwnership(this.props.record.get('id'), this.state.ownershipEmail.toLowerCase(), ownershipSuccess, console.error) + }, + + removeOwnersip(email) { + console.log(email) + const ownerRemoved = () => { + this.getOwners() + this.setState({ ownershipRemoved: true }) + } + serverCache.removeRecordOwner(this.props.record.get('id'), email, ownerRemoved) + }, + + setWrapperRef(node) { + this.wrapperRef = node; + }, + + handleClickOutside(event) { + if (this.wrapperRef && !this.wrapperRef.contains(event.target)) { + this.props.hide(); + } + }, + + render() { + return ( +
+

Change ownership for this record

+
+ +
+
+ this.setState({ ownershipEmail: event.target.value })} value={this.state.ownershipEmail} /> +
+
+
+
+
+ You are modifying ownership of this record. Record owners have possibility to modify the record metadata, create new versions of this record and modify the ownership of this record. Clicking Save does not remove ownership from you. +
+
+
+
+ Owners: +
+
+ {this.state.owners.map(o => { + const ce_setter = this.state.owners.length <= 1 || o['preset'] + const ce = !ce_setter ? "" : " disabled" + let title = ce ? "Record has to have at least one owner" : "Remove users ownership for this record" + if (o['preset']) title = "Can not remove preset owner" + return( +
+
+ {o.email} +
+
+ +
+
) + })} +
+
+ If you have any questions, ask FMI data support by clicking "CONTACT" from the menu or send email to b2share-tuki@fmi.fi +
+
+
+
+ +
+
+ +
+
+
+ {this.state.ownershipAdded ? +
+ Ownership of the record has been added to {this.state.ownershipEmail} +
+ : false} +
+
+ {this.state.ownershipRemoved ? +
+ Ownership removed +
+ : false} +
+
+ ) + }, +}) diff --git a/webui/src/components/record.jsx b/webui/src/components/record.jsx index bbf71372d..82ada7cc3 100644 --- a/webui/src/components/record.jsx +++ b/webui/src/components/record.jsx @@ -16,7 +16,10 @@ import { getSchemaOrderedMajorAndMinorFields } from './schema.jsx'; import { Card } from 'react-bootstrap'; import PiwikTracker from 'piwik-react-router'; import { TwitterShareButton, TwitterIcon, FacebookShareButton, FacebookIcon} from 'react-share'; - +import { Card } from 'react-bootstrap'; +import { ExternalUrlsRec } from './externalurls.jsx'; +import FileToken from './filetoken.jsx' +import { Ownership } from './ownership.jsx'; const PT = React.PropTypes; @@ -128,7 +131,10 @@ const Record = React.createClass({ showB2NoteWindow: false, record_notes: [], files_notes: [], - b2noteUrl: this.props.b2noteUrl + b2noteUrl: this.props.b2noteUrl, + token: null, + buckets: [], + showOwnershipWindow: false } return state; @@ -845,6 +851,13 @@ const Record = React.createClass({ className="frame"/> : false } + {this.state.showOwnershipWindow ? +
+ {/* {this.renderOwnershipModal()} */} + this.setState({ showOwnershipWindow: false })} /> +
+ : false + }
@@ -867,9 +880,9 @@ const Record = React.createClass({ : false } - { canEditRecord(record) ? -   - { state == 'draft' ? 'Edit draft metadata' : 'Edit metadata' } + {canEditRecord(record) ? +   + {state == 'draft' ? 'Edit draft metadata' : 'Edit metadata'} : false } { isRecordOwner(record) && isLatestVersion ? @@ -877,7 +890,12 @@ const Record = React.createClass({ Create New Version : false } - Report Abuse + {isRecordOwner(record) ? + this.setState({ showOwnershipWindow: !this.state.showOwnershipWindow })} className="btn btn-warning">  + Modify ownership + : false + } + Report Abuse
diff --git a/webui/src/data/server.js b/webui/src/data/server.js index db0a71e36..85a48add3 100644 --- a/webui/src/data/server.js +++ b/webui/src/data/server.js @@ -1,8 +1,8 @@ -import {fromJS, OrderedMap, List} from 'immutable'; -import {ajaxGet, ajaxPost, ajaxPut, ajaxPatch, ajaxDelete, errorHandler} from './ajax' -import {Store} from './store' -import {objEquals, expect, pairs} from './misc' -import {browserHistory} from 'react-router' +import { fromJS, OrderedMap, List } from 'immutable'; +import { ajaxGet, ajaxPost, ajaxPut, ajaxPatch, ajaxDelete, errorHandler } from './ajax' +import { Store } from './store' +import { objEquals, expect, pairs } from './misc' +import { browserHistory } from 'react-router' import _ from 'lodash'; const urlRoot = ""; // window.location.origin; @@ -12,39 +12,41 @@ export const access_type = process.env.LOGIN_TYPE || 'b2access'; export const loginURL = `${urlRoot}/api/oauth/login/${access_type}`; const apiUrls = { - root() { return `${urlRoot}/api/` }, + root() { return `${urlRoot}/api/` }, - user() { return `${urlRoot}/api/user` }, - userTokens() { return `${urlRoot}/api/user/tokens` }, - manageToken(id) { return `${urlRoot}/api/user/tokens/${id}` }, + user() { return `${urlRoot}/api/user` }, + userTokens() { return `${urlRoot}/api/user/tokens` }, + manageToken(id) { return `${urlRoot}/api/user/tokens/${id}` }, - users(queryString) { return `${urlRoot}/api/users` + (queryString ? `?q=${queryString}` : ``) }, - userListWithRole(id) { return `${urlRoot}/api/roles/${id}/users` }, - userWithRole(roleid, userid) { return `${urlRoot}/api/roles/${roleid}/users/${userid}` }, + users(queryString) { return `${urlRoot}/api/users` + (queryString ? `?q=${queryString}` : ``) }, + userListWithRole(id) { return `${urlRoot}/api/roles/${id}/users` }, + userWithRole(roleid, userid) { return `${urlRoot}/api/roles/${roleid}/users/${userid}` }, - records() { return `${urlRoot}/api/records/` }, - recordsVersion(versionOf) { return `${urlRoot}/api/records/?version_of=${versionOf}` }, - record(id) { return `${urlRoot}/api/records/${id}` }, - draft(id) { return `${urlRoot}/api/records/${id}/draft` }, - fileBucket(bucket, key) { return `${urlRoot}/api/files/${bucket}/${key}` }, + records() { return `${urlRoot}/api/records/` }, + recordsVersion(versionOf) { return `${urlRoot}/api/records/?version_of=${versionOf}` }, + record(id) { return `${urlRoot}/api/records/${id}` }, + draft(id) { return `${urlRoot}/api/records/${id}/draft` }, + fileBucket(bucket, key) { return `${urlRoot}/api/files/${bucket}/${key}` }, - abuse(id) { return `${urlRoot}/api/records/${id}/abuse` }, - accessrequests(id) { return `${urlRoot}/api/records/${id}/accessrequests` }, + abuse(id) { return `${urlRoot}/api/records/${id}/abuse` }, + accessrequests(id) { return `${urlRoot}/api/records/${id}/accessrequests` }, - communities() { return `${urlRoot}/api/communities/` }, - community(id) { return `${urlRoot}/api/communities/${id}` }, - communitySchema(cid, version) { return `${urlRoot}/api/communities/${cid}/schemas/${version}` }, + communities() { return `${urlRoot}/api/communities/` }, + community(id) { return `${urlRoot}/api/communities/${id}` }, + communitySchema(cid, version) { return `${urlRoot}/api/communities/${cid}/schemas/${version}` }, - schema(id, version) { return `${urlRoot}/api/schemas/${id}/versions/${version}` }, + schema(id, version) { return `${urlRoot}/api/schemas/${id}/versions/${version}` }, - remotes(remote) { return `${urlRoot}/api/remotes` + (remote ? `/${remote}` : ``) }, - remotesJob() { return `${urlRoot}/api/remotes/jobs` }, - b2drop(path_) { return `${urlRoot}/api/remotes/b2drop` + (path_ ? `/${path_}` : ``) }, + remotes(remote) { return `${urlRoot}/api/remotes` + (remote ? `/${remote}` : ``) }, + remotesJob() { return `${urlRoot}/api/remotes/jobs` }, + b2drop(path_) { return `${urlRoot}/api/remotes/b2drop` + (path_ ? `/${path_}` : ``) }, - vocabularies(vid) { return `${urlRoot}/suggest/${vid}.json` }, + vocabularies(vid) { return `${urlRoot}/suggest/${vid}.json` }, - statistics() { return `${urlRoot}/api/stats` }, - b2handle_pid_info(file_pid) { return `${urlRoot}/api/handle/${file_pid}` }, + statistics() { return `${urlRoot}/api/stats` }, + b2handle_pid_info(file_pid) { return `${urlRoot}/api/handle/${file_pid}` }, + ownership(id) { return `${urlRoot}/api/records/${id}/ownership` }, + ownershipModify(id, email) { return `${urlRoot}/api/records/${id}/ownership?email=${email}` }, extractCommunitySchemaInfoFromUrl(communitySchemaURL) { if (!communitySchemaURL) { @@ -104,18 +106,18 @@ class Getter { this.fetchErrorFn = fetchErrorFn; } - autofetch(fetchCallbackFn = () => {}) { + autofetch(fetchCallbackFn = () => { }) { this.fetch(this.params, fetchCallbackFn); } - fetch(params, fetchCallbackFn = () => {}) { + fetch(params, fetchCallbackFn = () => { }) { if (this.timer.ticking() && this.equals(params, this.params)) { return; } this.forceFetch(params, fetchCallbackFn); } - forceFetch(params, fetchCallbackFn = () => {}) { + forceFetch(params, fetchCallbackFn = () => { }) { this.timer.restart(); this.params = params; ajaxGet({ @@ -154,7 +156,7 @@ class Pool { return r; } - clear(){ + clear() { this.pool = {}; } } @@ -215,7 +217,7 @@ class FilePoster { put(file, progressFn) { if (this.xhr) { console.error("already uploading file"); - return ; + return; } const xhr = new XMLHttpRequest(); @@ -247,7 +249,7 @@ class FilePoster { if (this.xhr && this.xhr.abort) { this.xhr.abort(); } - const completeFn = () => {this.xhr = null}; + const completeFn = () => { this.xhr = null }; ajaxDelete({ url: this.url, successFn: (data) => { completeFn(); successFn(data); }, @@ -316,56 +318,56 @@ class ServerCache { this.getters.tokens = new Getter( apiUrls.userTokens(), null, (data) => { - var tokens = data.hits.hits.map(c => { return c }) + var tokens = data.hits.hits.map(c => { return c }) this.store.setIn(['tokens'], tokens); }, - (xhr) => this.store.setIn(['tokens'], new Error(xhr)) ); + (xhr) => this.store.setIn(['tokens'], new Error(xhr))); this.getters.communityUsers = new Pool(roleid => new Getter( - apiUrls.userListWithRole(roleid), null, + apiUrls.userListWithRole(roleid), null, (users) => { - this.store.setIn(['communityUsers', roleid], fromJS(users.hits.hits)); + this.store.setIn(['communityUsers', roleid], fromJS(users.hits.hits)); }, - null )); + null)); this.getters.latestRecords = new Getter( - apiUrls.records(), {sort:"mostrecent"}, + apiUrls.records(), { sort: "mostrecent" }, (data) => this.store.setIn(['latestRecords'], fromJS(data.hits.hits)), - (xhr) => this.store.setIn(['latestRecords'], new Error(xhr)) ); + (xhr) => this.store.setIn(['latestRecords'], new Error(xhr))); this.getters.latestRecordsOfCommunity = new Getter( - apiUrls.records(), {sort:"mostrecent"}, + apiUrls.records(), { sort: "mostrecent" }, (data) => this.store.setIn(['latestRecordsOfCommunity'], fromJS(data.hits.hits)), - (xhr) => this.store.setIn(['latestRecordsOfCommunity'], new Error(xhr)) ); + (xhr) => this.store.setIn(['latestRecordsOfCommunity'], new Error(xhr))); this.getters.searchRecords = new Getter( apiUrls.records(), null, (data) => this.store.setIn(['searchRecords'], fromJS(data.hits)), - (xhr) => this.store.setIn(['searchRecords'], new Error(xhr)) ); + (xhr) => this.store.setIn(['searchRecords'], new Error(xhr))); this.getters.communities = new Getter( apiUrls.communities(), null, (data) => { let map = OrderedMap(); - data.hits.hits.forEach(c => { map = map.set(c.id, fromJS(c)); } ); + data.hits.hits.forEach(c => { map = map.set(c.id, fromJS(c)); }); this.store.setIn(['communities'], map); }, - (xhr) => this.store.setIn(['communities'], new Error(xhr)) ); + (xhr) => this.store.setIn(['communities'], new Error(xhr))); this.getters.vocabularies = new Pool(vocabularyID => new Getter( apiUrls.vocabularies(vocabularyID), null, (data) => { const transform = id => { - return {id, name:id}; + return { id, name: id }; } const res = Object.assign(data, { - items: (data.items[0] instanceof Array) ? data.items.map(([id, name]) => ({id, name})) : data.items.map(transform), + items: (data.items[0] instanceof Array) ? data.items.map(([id, name]) => ({ id, name })) : data.items.map(transform), }); this.store.setIn(['vocabularies', vocabularyID], res); return res; }, - (xhr) => this.store.setIn(['vocabularies', vocabularyID], new Error(xhr)) )); + (xhr) => this.store.setIn(['vocabularies', vocabularyID], new Error(xhr)))); function retrieveVersions(store, links, cachePath) { const versionsLink = links && links.versions; @@ -504,12 +506,12 @@ class ServerCache { url: data.links.files, successFn: (filedata) => { const files = filedata.contents.map(this.fixFile); - for(var file_idx=0; file_idx this.store.setIn(['recordCache', recordID], new Error(xhr)) )); + (xhr) => this.store.setIn(['recordCache', recordID], new Error(xhr)))); this.getters.community = new Pool((communityID) => new Getter(apiUrls.community(communityID), null, @@ -534,7 +536,7 @@ class ServerCache { this.store.setIn(['communityCache', communityID], res); return res; }, - (xhr) => this.store.setIn(['communityCache', communityID], new Error(xhr)) )); + (xhr) => this.store.setIn(['communityCache', communityID], new Error(xhr)))); this.getters.draft = new Pool(draftID => new Getter(apiUrls.draft(draftID), null, @@ -548,7 +550,7 @@ class ServerCache { this.getters.fileBucket.get(draftID).fetch(); retrieveVersions(this.store, data.links, ['draftCache', draftID, 'versions']); }, - (xhr) => this.store.setIn(['draftCache', draftID], new Error(xhr)) )); + (xhr) => this.store.setIn(['draftCache', draftID], new Error(xhr)))); this.getters.fileBucket = new Pool(draftID => { const fileBucketUrl = this.store.getIn(['draftCache', draftID, 'links', 'files']); @@ -564,8 +566,8 @@ class ServerCache { return new Getter(fileBucketUrl, null, placeDataFn, errorFn); }); - this.getters.communitySchema = new Pool( communityID => - new Pool ( version => { + this.getters.communitySchema = new Pool(communityID => + new Pool(version => { const placeDataFn = (data) => { expect(communityID == data.community); processSchema(data.json_schema); @@ -664,14 +666,14 @@ class ServerCache { return this.store.getIn(['latestRecords']); } - getLatestRecordsOfCommunity({community}) { + getLatestRecordsOfCommunity({ community }) { let q = ' community:' + community; let sort = 'mostrecent', page = 1, size = 10; - this.getters.latestRecordsOfCommunity.fetch({q, sort, page, size}); + this.getters.latestRecordsOfCommunity.fetch({ q, sort, page, size }); return this.store.getIn(['latestRecordsOfCommunity']); } - searchRecords({q, community, sort, page, size, drafts}) { + searchRecords({ q, community, sort, page, size, drafts }) { if (community) { q = (q ? '(' + q + ') && ' : '') + ' community:' + community; } @@ -681,7 +683,7 @@ class ServerCache { // q = (q ? '(' + q + ') && ' : '') + 'publication_state:draft'; q = 'publication_state:draft'; } - (drafts == 1) ? this.getters.searchRecords.fetch({q, sort, page, size, drafts}) : this.getters.searchRecords.fetch({q, sort, page, size}); + (drafts == 1) ? this.getters.searchRecords.fetch({ q, sort, page, size, drafts }) : this.getters.searchRecords.fetch({ q, sort, page, size }); return this.store.getIn(['searchRecords']); } @@ -694,7 +696,7 @@ class ServerCache { return List(communities.valueSeq()); } - getCommunity(communityIDorName, callbackFn = ()=>{}) { + getCommunity(communityIDorName, callbackFn = () => { }) { this.getters.community.get(communityIDorName).fetch({}, callbackFn); return this.store.getIn(['communityCache', communityIDorName]); } @@ -749,7 +751,7 @@ class ServerCache { if (blockRefs) { blockRefs.entrySeq().forEach( - ([id,ref]) => { + ([id, ref]) => { const ver = apiUrls.extractSchemaVersionFromUrl(ref.get('$ref')); blockSchemas.push([id, this.getBlockSchema(id, ver)]); } @@ -767,14 +769,14 @@ class ServerCache { } newUserToken(tokenName, successFn) { - var param = {token_name:tokenName}; - this.posters.tokens.get().post(param, (token)=>{ + var param = { token_name: tokenName }; + this.posters.tokens.get().post(param, (token) => { this.getters.tokens.forceFetch(null); successFn(token); }); } - getUserTokens(){ + getUserTokens() { this.getters.tokens.autofetch(); return this.store.getIn(['tokens']); } @@ -782,7 +784,7 @@ class ServerCache { removeUserToken(tokenID) { ajaxDelete({ url: apiUrls.manageToken(tokenID), - successFn: ()=>{ + successFn: () => { notifications.success("Token is successfully deleted"); this.getters.tokens.forceFetch(null); }, @@ -923,45 +925,45 @@ class ServerCache { // List users with a specific role - getCommunityUsers(roleid){ + getCommunityUsers(roleid) { this.getters.communityUsers.get(roleid).autofetch(); - if(this.store.getIn(['communityUsers']).size > 0){ + if (this.store.getIn(['communityUsers']).size > 0) { return this.store.getIn(['communityUsers', roleid]); } - return {}; + return {}; } // Assign a role to the user by email - registerUserRole(email, roleid, successFn, errorFn){ + registerUserRole(email, roleid, successFn, errorFn) { ajaxGet({ url: apiUrls.users(email), successFn: (user) => { - if(user.hits.hits[0]){ - ajaxPut({ - url: apiUrls.userWithRole( roleid , fromJS(user.hits.hits[0].id) ), - successFn: ()=> { - this.getters.communityUsers.get(roleid).autofetch(); - ajaxGet({ - url: apiUrls.user(), - successFn: data => this.store.setIn(['user'], fromJS(data)), - }); - notifications.success("The new role was assigned to the user"); - }, - errorFn: () => { - notifications.danger("User not found"); - }, - }); - } - else{ - notifications.danger("User not found"); - } - }, + if (user.hits.hits[0]) { + ajaxPut({ + url: apiUrls.userWithRole(roleid, fromJS(user.hits.hits[0].id)), + successFn: () => { + this.getters.communityUsers.get(roleid).autofetch(); + ajaxGet({ + url: apiUrls.user(), + successFn: data => this.store.setIn(['user'], fromJS(data)), + }); + notifications.success("The new role was assigned to the user"); + }, + errorFn: () => { + notifications.danger("User not found"); + }, + }); + } + else { + notifications.danger("User not found"); + } + }, errorFn: errorFn, }); } // Unassign a role from a user - deleteRoleOfUser(roleid, userid){ + deleteRoleOfUser(roleid, userid) { ajaxDelete({ url: apiUrls.userWithRole(roleid, userid), successFn: () => { @@ -978,7 +980,7 @@ class ServerCache { }); } - getB2HandlePidInfo(file_pid, successFn){ + getB2HandlePidInfo(file_pid, successFn) { ajaxGet({ url: apiUrls.b2handle_pid_info(file_pid), successFn: response => { @@ -988,7 +990,7 @@ class ServerCache { }); } - addB2SafePid(file_pid, successFn){ + addB2SafePid(file_pid, successFn) { ajaxPatch({ url: apiUrls.addB2SafeFile(file_pid), successFn: response => { @@ -997,6 +999,60 @@ class ServerCache { }, }); } + + getAccessToken(record_id, successFn) { + ajaxGet({ + url: apiUrls.tempFileAccess(record_id), + successFn: (response) => { + successFn(response) + }, + }); + } + + addOwnership(record_id, email, successFn, errorFn) { + ajaxPut({ + url: apiUrls.ownershipModify(record_id, email), + params: { email }, + successFn: response => { + console.log(response); + successFn(response); + }, + errorFn: e => { + console.error(e); + notifications.danger(JSON.parse(e.response).message); + setTimeout(() => { + notifications.clearAll() + }, 5000); + }, + }); + } + + getRecordOwners(record_id, successFn) { + ajaxGet({ + url: apiUrls.ownership(record_id), + successFn: response => { + console.log(response); + successFn(response); + }, + errorFn: e => { + console.error(e); + } + }) + } + + removeRecordOwner(record_id, email, successFn) { + ajaxDelete({ + url: apiUrls.ownershipModify(record_id, email), + params: { email }, + successFn: response => { + console.log(response); + successFn(response); + }, + errorFn: e => { + console.error(e); + } + }) + } }; @@ -1047,19 +1103,20 @@ class Notifications { export class Error { - constructor({status, statusText, responseText}) { + constructor({ status, statusText, responseText }) { this.code = status; this.text = statusText; this.data = null; try { - this.data = JSON.parse(responseText); } + this.data = JSON.parse(responseText); + } catch (err) { this.data = responseText; }; } } -errorHandler.fn = function(text) { +errorHandler.fn = function (text) { notifications.danger(text); } @@ -1093,8 +1150,8 @@ export const browser = { return `${window.location.origin}/records/${recordId}`; }, - gotoSearch({q, community, sort, page, size, drafts}) { - const queryString = encode({q, community, sort, page, size, drafts}); + gotoSearch({ q, community, sort, page, size, drafts }) { + const queryString = encode({ q, community, sort, page, size, drafts }); // trigger a route reload which will do the new search, see SearchRecordRoute browserHistory.push(`/records/?${queryString}`); }, diff --git a/webui/webpack.config.devel.js b/webui/webpack.config.devel.js index ac54c6612..29f7e03cb 100644 --- a/webui/webpack.config.devel.js +++ b/webui/webpack.config.devel.js @@ -46,4 +46,3 @@ module.exports = { ] } }; - From fa2ff12300072db35bcd23f86253244876081872 Mon Sep 17 00:00:00 2001 From: Johannes Lares Date: Wed, 9 Oct 2024 15:38:16 +0200 Subject: [PATCH 4/6] UI: Remove duplicate Card import --- webui/src/components/record.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webui/src/components/record.jsx b/webui/src/components/record.jsx index 82ada7cc3..b9150f735 100644 --- a/webui/src/components/record.jsx +++ b/webui/src/components/record.jsx @@ -16,7 +16,6 @@ import { getSchemaOrderedMajorAndMinorFields } from './schema.jsx'; import { Card } from 'react-bootstrap'; import PiwikTracker from 'piwik-react-router'; import { TwitterShareButton, TwitterIcon, FacebookShareButton, FacebookIcon} from 'react-share'; -import { Card } from 'react-bootstrap'; import { ExternalUrlsRec } from './externalurls.jsx'; import FileToken from './filetoken.jsx' import { Ownership } from './ownership.jsx'; From 2c426ab5fc86ed6a83a33eac787c46474fe8968e Mon Sep 17 00:00:00 2001 From: Marvin Winkens Date: Wed, 9 Oct 2024 17:23:29 +0200 Subject: [PATCH 5/6] add user ID to transfer-ownership CLI command Signed-off-by: Marvin Winkens --- b2share/modules/management/ownership/cli.py | 59 ++++++++++++++------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/b2share/modules/management/ownership/cli.py b/b2share/modules/management/ownership/cli.py index 8ffeb2c02..7a3a312e0 100644 --- a/b2share/modules/management/ownership/cli.py +++ b/b2share/modules/management/ownership/cli.py @@ -27,6 +27,7 @@ from __future__ import absolute_import, print_function import click import os +import uuid from flask.cli import with_appcontext from flask import app, current_app @@ -251,29 +252,52 @@ def find(user_email, type=None): "No objs found with owner {}".format(str(user)), fg="red")) +def _is_valid_uuid(input_string: str) -> bool: + """ + Checks if a string is a valid UUID + """ + try: + _ = uuid.UUID(input_string) + return True + except ValueError: + return False + + +def _get_user_by_email_or_id(user_email_or_id: str): + """ + returns a user by email or id + """ + # user that we want to replace + if not _is_valid_uuid(user_email_or_id): + user_email = user_email_or_id + if check_user(user_email) is None: + raise click.ClickException( + "User <{}> does not exist. Please check the email and try again".format(user_email)) + user = get_user(user_email=user_email) + else: + user_id = user_email_or_id + user = get_user(user_id=user_id) + if user is None: + raise click.ClickException( + "User <{}> does not exist. Please check the id and try again".format(user_id)) + return user + + @ownership.command('transfer-add') @with_appcontext @click.option('-t', '--type', type=click.Choice(['deposit', 'record']), required=False) -@click.argument('user-email', required=True, type=str) -@click.argument('new-user-email', required=True, type=str) +@click.argument('user-email-or-id', required=True, type=str) +@click.argument('new-user-email-or-id', required=True, type=str) @patch_current_app_config({'SERVER_NAME': os.environ.get('JSONSCHEMAS_HOST')}) -def transfer_add(user_email, new_user_email, type=None): +def transfer_add(user_email_or_id, new_user_email_or_id): """ Add user to all the records or/and deposits if user is not of the owners already. - :params user-email: user email of the old owner - new-user-email: user email of the new owner - type: record type (deposit, record) + :params user-email-or-id: user email or id of the old owner + new-user-email-or-id: user email or id of the new owner """ - if check_user(user_email) is None: - raise click.ClickException( - "User <{}> does not exist. Please check the email and try again".format(user_email)) - if check_user(new_user_email) is None: - raise click.ClickException( - "New User <{}> does not exist. Please check the email and try again".format(new_user_email)) - # user that we want to replace - user = get_user(user_email=user_email) - # new owner that we want to add - new_user = get_user(user_email=new_user_email) + user = _get_user_by_email_or_id(user_email_or_id) + new_user = _get_user_by_email_or_id(new_user_email_or_id) + changed = False if user is not None and new_user is not None: # search using ESSearch class and filtering by type @@ -305,7 +329,6 @@ def transfer_add(user_email, new_user_email, type=None): click.secho(click.style( "It was not possible to update the ownership", fg="red")) - @ownership.command('remove-all') @with_appcontext @click.argument('user-email', required=True, type=str) @@ -360,7 +383,7 @@ def search_es(user, type): use ESSearch to find obj where user is owner :params user: User obj - type: filter the query. Possible values (deposit, record or None) + type: filter the query. Possible values (deposit, record or None) ''' query = 'owners:{} || _deposit.owners:{}'.format(user.id, user.id) From b67c1a20b34e86be2749645c9cf2ebbf6650a53d Mon Sep 17 00:00:00 2001 From: Marvin Winkens Date: Thu, 10 Oct 2024 11:45:06 +0200 Subject: [PATCH 6/6] advance all CLI commands to be able to use b2share ID Signed-off-by: Marvin Winkens --- b2share/modules/management/ownership/cli.py | 130 +++++++++----------- 1 file changed, 59 insertions(+), 71 deletions(-) diff --git a/b2share/modules/management/ownership/cli.py b/b2share/modules/management/ownership/cli.py index 7a3a312e0..5493e9157 100644 --- a/b2share/modules/management/ownership/cli.py +++ b/b2share/modules/management/ownership/cli.py @@ -39,6 +39,8 @@ from invenio_accounts.models import User from invenio_pidrelations.contrib.versioning import PIDVersioning from invenio_pidstore.errors import PIDDoesNotExistError +from invenio_oauthclient.utils import oauth_get_user +from invenio_oauthclient.models import UserIdentity from b2share.modules.deposit.api import Deposit from b2share.utils import ESSearch, to_tabulate @@ -130,6 +132,43 @@ def inner(*args, **kwargs): return decorator +def _is_valid_uuid(input_string: str) -> bool: + """ + Checks if a string is a valid UUID + """ + try: + _ = uuid.UUID(input_string) + return True + except ValueError: + return False + + +def _get_user_by_email_or_id(user_email_or_id: str): + """ + returns a user by email or b2access id + """ + # user that we want to replace + if not _is_valid_uuid(user_email_or_id): + user_email = user_email_or_id + user = User.query.filter(User.email == user_email).one_or_none() + if user is None: + raise click.ClickException( + "User <{}> does not exist. Please check the email and try again".format(user_email)) + else: + user_id_external = uuid.UUID(user_email_or_id) + user_identity = UserIdentity.query.filter(UserIdentity.id == str(user_id_external)).one_or_none() + if user_identity is None: + raise click.ClickException( + "User <{}> does not exist. Please check the b2access id and try again".format(user_id_external)) + user_id = user_identity.id_user # get the local ID from the identity + user = User.query.filter(User.id == user_id).one_or_none() # find user by local ID + if user is None: + raise click.ClickException( + "User <{}> not found internally, but user_identity <{}> exists, please contact a system administrator" + .format(user_id_external, user_id)) + return user + + @click.group() def ownership(): """ownership management commands.""" @@ -149,25 +188,21 @@ def list(record_pid): @ownership.command() @with_appcontext @click.argument('record-pid', required=True, type=str) -@click.argument('user-email', required=True, type=str) +@click.argument('user-email-or-id', required=True, type=str) @click.option('-q', '--quiet', is_flag=True, default=False) @click.option('-y', '--yes-i-know', is_flag=True, default=False) -def reset(record_pid, user_email, yes_i_know, quiet): +def reset(record_pid, user_email_or_id, yes_i_know, quiet): """ Remove the previous ownership and set up the new user as a unique owner for all the version of the record. :params record-pid: B2rec record PID - user-email: user email + user-email-or-id: user email or b2access ID """ - if check_user(user_email) is None: - raise click.ClickException( - click.style( - """User does not exist. Please check the email and try again""", fg="red")) + user = _get_user_by_email_or_id(user_email_or_id) list_ownership(record_pid) if yes_i_know or click.confirm( "Are you sure you want to reset the owership? Previous owners will not be able to access the records anymore.", abort=True): version_master = find_version_master(record_pid) all_pids = [v.pid_value for v in version_master.children.all()] - user = User.query.filter(User.email == user_email).one_or_none() for single_pid in all_pids: replace_ownership(single_pid, user.id) if not quiet: @@ -178,20 +213,17 @@ def reset(record_pid, user_email, yes_i_know, quiet): @ownership.command() @with_appcontext @click.argument('record-pid', required=True, type=str) -@click.argument('user-email', required=True, type=str) +@click.argument('user-email-or-id', required=True, type=str) @click.option('-q', '--quiet', is_flag=True, default=False) -def add(record_pid, user_email, quiet): +def add(record_pid, user_email_or_id, quiet): """ Add user as owner for all the version of the record. :params record-pid: B2rec record PID - user-email: user email + user-email-or-id: user email or b2access ID """ - if check_user(user_email) is None: - raise click.ClickException( - """User does not exist. Please check the email and try again""") + user = _get_user_by_email_or_id(user_email_or_id) version_master = find_version_master(record_pid) all_pids = [v.pid_value for v in version_master.children.all()] - user = User.query.filter(User.email == user_email).one_or_none() for single_pid in all_pids: record = get_record_by_pid(single_pid) add_ownership(record, user.id) @@ -203,21 +235,17 @@ def add(record_pid, user_email, quiet): @ownership.command() @with_appcontext @click.argument('record-pid', required=True, type=str) -@click.argument('user-email', required=True, type=str) +@click.argument('user-email-or-id', required=True, type=str) @click.option('-q', '--quiet', is_flag=True, default=False) -def remove(record_pid, user_email, quiet): +def remove(record_pid, user_email_or_id, quiet): """ Remove user as an owner of the record. :params record-pid: B2rec record PID - user-email: user email + user-email-or-id: user email or b2access ID """ - if check_user(user_email) is None: - raise click.ClickException( - """User does not exist. Please check the email and try again""") - + user = _get_user_by_email_or_id(user_email_or_id) version_master = find_version_master(record_pid) all_pids = [v.pid_value for v in version_master.children.all()] - user = User.query.filter(User.email == user_email).one_or_none() for single_pid in all_pids: record = get_record_by_pid(single_pid) remove_ownership(record, user.id) @@ -228,19 +256,15 @@ def remove(record_pid, user_email, quiet): @ownership.command() @with_appcontext -@click.argument('user-email', required=True, type=str) +@click.argument('user-email-or-id', required=True, type=str) @click.option('-t', '--type', type=click.Choice(['deposit', 'record']), required=False) -def find(user_email, type=None): +def find(user_email_or_id, type=None): """ Find all the records or/and deposits where user is one of the owners. :params user-email: user email type: record type (deposit, record) """ - if check_user(user_email) is None: - raise click.ClickException( - "User <{}> does not exist. Please check the email and try again".format(user_email)) - # user that we want to find in the db - user = get_user(user_email=user_email) + user = _get_user_by_email_or_id(user_email_or_id) if user is not None: # search using ESSearch class and filtering by type search = search_es(user, type=type) @@ -251,38 +275,6 @@ def find(user_email, type=None): click.secho(click.style( "No objs found with owner {}".format(str(user)), fg="red")) - -def _is_valid_uuid(input_string: str) -> bool: - """ - Checks if a string is a valid UUID - """ - try: - _ = uuid.UUID(input_string) - return True - except ValueError: - return False - - -def _get_user_by_email_or_id(user_email_or_id: str): - """ - returns a user by email or id - """ - # user that we want to replace - if not _is_valid_uuid(user_email_or_id): - user_email = user_email_or_id - if check_user(user_email) is None: - raise click.ClickException( - "User <{}> does not exist. Please check the email and try again".format(user_email)) - user = get_user(user_email=user_email) - else: - user_id = user_email_or_id - user = get_user(user_id=user_id) - if user is None: - raise click.ClickException( - "User <{}> does not exist. Please check the id and try again".format(user_id)) - return user - - @ownership.command('transfer-add') @with_appcontext @click.option('-t', '--type', type=click.Choice(['deposit', 'record']), required=False) @@ -292,8 +284,8 @@ def _get_user_by_email_or_id(user_email_or_id: str): def transfer_add(user_email_or_id, new_user_email_or_id): """ Add user to all the records or/and deposits if user is not of the owners already. - :params user-email-or-id: user email or id of the old owner - new-user-email-or-id: user email or id of the new owner + :params user-email-or-id: user email or b2access id of the old owner + new-user-email-or-id: user email or b2access id of the new owner """ user = _get_user_by_email_or_id(user_email_or_id) new_user = _get_user_by_email_or_id(new_user_email_or_id) @@ -331,20 +323,16 @@ def transfer_add(user_email_or_id, new_user_email_or_id): @ownership.command('remove-all') @with_appcontext -@click.argument('user-email', required=True, type=str) +@click.argument('user-email-or-id', required=True, type=str) @click.option('-t', '--type', type=click.Choice(['deposit', 'record']), required=False) @patch_current_app_config({'SERVER_NAME': os.environ.get('JSONSCHEMAS_HOST')}) -def transfer_remove(user_email, type=None): +def transfer_remove(user_email_or_id, type=None): """ remove user to all the records or/and deposits. :params user-email: user email type: record type (deposit, record) """ - if check_user(user_email) is None: - raise click.ClickException( - "User <{}> does not exist. Please check the email and try again".format(user_email)) - # user that we want to remove - user = get_user(user_email=user_email) + user = _get_user_by_email_or_id(user_email_or_id) changed = False if user is not None: # search using ESSearch class and filtering by type