From eef74157d8641ea00b202b3b6e032629e2522cce Mon Sep 17 00:00:00 2001 From: Peter Weber Date: Mon, 8 Jan 2024 12:16:58 +0100 Subject: [PATCH] monitoring: access token * Adds monitoring user and access token for setup. Co-Authored-by: Peter Weber --- scripts/functions | 9 +++ scripts/setup | 7 ++ sonar/config.py | 9 +-- sonar/modules/cli/utils.py | 101 +++++++++++++++++++++++++---- sonar/modules/permissions.py | 2 + sonar/monitoring/views/__init__.py | 19 +++--- tests/ui/test_cli.py | 25 +++++++ 7 files changed, 142 insertions(+), 30 deletions(-) diff --git a/scripts/functions b/scripts/functions index d818de5fa..527994988 100644 --- a/scripts/functions +++ b/scripts/functions @@ -61,3 +61,12 @@ function section { color $2 echo -e "$(tput setaf 0; tput setab $color)\n $1 $(tput sgr0)" } + +token_create() { + if [ -z ${3} ] + then + invenio utils token-create -n ${1} -u ${2} + else + invenio utils token-create -n ${1} -u ${2} -t ${3} + fi +} \ No newline at end of file diff --git a/scripts/setup b/scripts/setup index 35f126ff0..da9ef3dfd 100755 --- a/scripts/setup +++ b/scripts/setup @@ -111,6 +111,13 @@ invenio access allow admin-access role moderator invenio access allow admin-access role submitter message "Done" "success" +section "Create user for monitoring" "info" +invenio users create --active --confirm --password monitor monitoring@rero.ch +invenio roles create monitoring +invenio roles add monitoring@rero.ch monitoring +# create token access for monitoring +token_create monitoring monitoring@rero.ch ${INVENIO_RERO_ACCESS_TOKEN_MONITORING} + # Create a default location for depositing files section "Create location for storing files" "info" invenio fixtures deposits create diff --git a/sonar/config.py b/sonar/config.py index 24435bc8d..eec5050db 100644 --- a/sonar/config.py +++ b/sonar/config.py @@ -166,17 +166,14 @@ def _(x): # Stats 'stats-process-events': { 'task': 'invenio_stats.tasks.process_events', - 'schedule': timedelta(minutes=30), + 'schedule': crontab(minute='5,35', hour='*'), 'args': [('record-view', 'file-download')], }, # Stats Agg events 'stats-aggregate-events': { 'task': 'invenio_stats.tasks.aggregate_events', - 'schedule': timedelta(minutes=35), - 'args': [( - 'record-view-agg', 'record-view-agg', - 'file-download-agg', 'file-download-agg', - )], + 'schedule': crontab(minute='10,40', hour='*'), + 'args': [('record-view-agg', 'file-download-agg')], }, # Documents stats 'documents-stats': { diff --git a/sonar/modules/cli/utils.py b/sonar/modules/cli/utils.py index cc64142d1..4d9375ae5 100644 --- a/sonar/modules/cli/utils.py +++ b/sonar/modules/cli/utils.py @@ -27,15 +27,22 @@ import jsonref from flask import current_app from flask.cli import with_appcontext +from invenio_db import db from invenio_files_rest.models import Location from invenio_jsonschemas import current_jsonschemas +from invenio_oauth2server.cli import process_scopes, process_user +from invenio_oauth2server.models import Client, Token from invenio_records_rest.utils import obj_or_import_string from invenio_search.cli import search_version_check from invenio_search.proxies import current_search from jsonref import jsonloader +from werkzeug.local import LocalProxy +from werkzeug.security import gen_salt from sonar.modules.api import SonarRecord +_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) + @click.group() def utils(): @@ -71,9 +78,10 @@ def clear_files(): try: shutil.rmtree(location.uri) except Exception: - click.secho('Directory {directory} cannot be cleaned'.format( - directory=location.uri), - fg='yellow') + click.secho( + f'Directory {location.uri} cannot be cleaned', + fg='yellow' + ) click.secho('Finished', fg='green') @@ -119,16 +127,14 @@ def export(pid_type, serializer_key, output_dir): :param pid_type: record type :param output_dir: Output directory """ - click.secho('Export "{pid_type}" records in {dir}'.format( - pid_type=pid_type, dir=output_dir.name)) + click.secho(f'Export "{pid_type}" records in {output_dir.name}') try: # Get the correct record class record_class = SonarRecord.get_record_class_by_pid_type(pid_type) if not record_class: - raise Exception('No record class found for type "{type}"'.format( - type=pid_type)) + raise Exception(f'No record class found for type "{pid_type}"') # Load the serializer serializer_class = current_app.config.get( @@ -164,8 +170,7 @@ def export(pid_type, serializer_key, output_dir): exist_ok=True) shutil.copyfile(file['uri'], target_path) file.pop('uri') - file['path'] = './{pid}/{key}'.format(pid=pid, - key=file['key']) + file['path'] = f'./{pid}/{file["key"]}' records.append(record) @@ -178,7 +183,77 @@ def export(pid_type, serializer_key, output_dir): click.secho('Finished', fg='green') - except Exception as exception: - click.secho('An error occured during export: {error}'.format( - error=str(exception)), - fg='red') + except Exception as err: + click.secho(f'An error occured during export: {err}', fg='red') + + +def create_personal( + name, user_id, scopes=None, is_internal=False, access_token=None): + """Create a personal access token. + + A token that is bound to a specific user and which doesn't expire, i.e. + similar to the concept of an API key. + + :param name: Client name. + :param user_id: User ID. + :param scopes: The list of permitted scopes. (Default: ``None``) + :param is_internal: If ``True`` it's a internal access token. + (Default: ``False``) + :param access_token: personalized access_token. + :returns: A new access token. + """ + with db.session.begin_nested(): + scopes = " ".join(scopes) if scopes else "" + + client = Client( + name=name, + user_id=user_id, + is_internal=True, + is_confidential=False, + _default_scopes=scopes + ) + client.gen_salt() + + if not access_token: + access_token = gen_salt( + current_app.config.get( + 'OAUTH2SERVER_TOKEN_PERSONAL_SALT_LEN') + ) + token = Token( + client_id=client.client_id, + user_id=user_id, + access_token=access_token, + expires=None, + _scopes=scopes, + is_personal=True, + is_internal=is_internal, + ) + db.session.add(client) + db.session.add(token) + + return token + + +@utils.command() +@click.option('-n', '--name', required=True) +@click.option( + '-u', '--user', required=True, callback=process_user, + help='User ID or email.') +@click.option( + '-s', '--scope', 'scopes', multiple=True, callback=process_scopes) +@click.option('-i', '--internal', is_flag=True) +@click.option( + '-t', '--access_token', 'access_token', required=False, + help='personalized access_token.') +@with_appcontext +def token_create(name, user, scopes, internal, access_token): + """Create a personal OAuth token.""" + if user: + token = create_personal( + name, user.id, scopes=scopes, is_internal=internal, + access_token=access_token + ) + db.session.commit() + click.secho(token.access_token, fg='blue') + else: + click.secho('No user found', fg='red') diff --git a/sonar/modules/permissions.py b/sonar/modules/permissions.py index 2eede9a2c..f9bdeeedb 100644 --- a/sonar/modules/permissions.py +++ b/sonar/modules/permissions.py @@ -37,6 +37,8 @@ moderator_access_permission = Permission(RoleNeed('moderator'), RoleNeed('admin'), RoleNeed('superuser')) +monitoring_access_permission = Permission(RoleNeed('superuser'), + RoleNeed('monitoring')) # Allow access without permission check allow_access = type('Allow', (), {'can': lambda self: True})() diff --git a/sonar/monitoring/views/__init__.py b/sonar/monitoring/views/__init__.py index 30a0e5690..493b3c81d 100644 --- a/sonar/monitoring/views/__init__.py +++ b/sonar/monitoring/views/__init__.py @@ -26,33 +26,30 @@ from redis import Redis from sonar.modules.documents.urn import Urn -from sonar.modules.permissions import superuser_access_permission +from sonar.modules.permissions import monitoring_access_permission from sonar.monitoring.api.data_integrity import DataIntegrityMonitoring from sonar.monitoring.api.database import DatabaseMonitoring api_blueprint = Blueprint('monitoring_api', __name__, url_prefix='/monitoring') -def is_superuser(func): - """Decorator checking if a user is logged and has role `superuser`.""" +def is_monitoring_user(func): + """Decorator checking if a user is logged and has `monitoring` rights.""" @wraps(func) def decorated_view(*args, **kwargs): if not current_user.is_authenticated: return jsonify({'error': 'Unauthorized'}), 401 - - if not superuser_access_permission.can(): + if not monitoring_access_permission.require().can(): return jsonify({'error': 'Forbidden'}), 403 - return func(*args, **kwargs) - return decorated_view @api_blueprint.before_request -@is_superuser -def check_for_superuser(): - """Check if user is superuser before each request, with decorator.""" +@is_monitoring_user +def check_for_monitoring_user(): + """Check if user is superuser or monitoring before each request.""" @api_blueprint.route('/db_connection_counts') @@ -65,7 +62,7 @@ def db_connection_count(): return jsonify({'error': str(exception)}), 500 -@api_blueprint.route('db_connections') +@api_blueprint.route('/db_connections') def db_activity(): """Current database activity.""" try: diff --git a/tests/ui/test_cli.py b/tests/ui/test_cli.py index 99746770c..aacc4e223 100644 --- a/tests/ui/test_cli.py +++ b/tests/ui/test_cli.py @@ -20,6 +20,7 @@ from io import BytesIO from os.path import isdir +import invenio_accounts.cli as CliUsers from click.testing import CliRunner from invenio_search.cli import destroy @@ -90,3 +91,27 @@ def test_export(app, script_info, document, organisation): ['--pid-type', 'org', '--output-dir', '/tmp/org'], obj=script_info) assert result.output.find('Export "org" records') != -1 + + +def test_cli_access_token(app, script_info): + """Test access token cli.""" + runner = CliRunner() + email = 'test@test.com' + res = runner.invoke( + CliUsers.users_create, + ['--active', '--confirm', '--password', 'PWD_TEST', email], + obj=script_info + ) + res = runner.invoke( + Cli.token_create, + ['-n', 'test_good', '-u', email, '-t', 'my_token'], + obj=script_info + ) + assert res.output.strip().split('\n') == ['my_token'] + + res = runner.invoke( + Cli.token_create, + ['-n', 'test_fail', '-u', 'fail@test.com', '-t', 'my_token'], + obj=script_info + ) + assert res.output.strip().split('\n') == ['No user found']