From ddba779a284144b063de7b137b06357e314997c7 Mon Sep 17 00:00:00 2001 From: Peter Weber Date: Mon, 8 Jan 2024 12:16:58 +0100 Subject: [PATCH] monitoring: access token * Add monitoring user and access token for setup. Co-Authored-by: Peter Weber --- scripts/setup | 25 +++++-- sonar/modules/cli/utils.py | 115 +++++++++++++++++++++++++---- sonar/modules/permissions.py | 2 + sonar/monitoring/views/__init__.py | 26 ++++--- 4 files changed, 139 insertions(+), 29 deletions(-) diff --git a/scripts/setup b/scripts/setup index 35f126ff..673dcbfe 100755 --- a/scripts/setup +++ b/scripts/setup @@ -97,11 +97,11 @@ invenio queues declare # Create admin role to restrict access section "Create roles for users" "info" -invenio roles create superuser -invenio roles create admin -invenio roles create moderator -invenio roles create submitter -invenio roles create user +invenio roles create superuser -d SuperUser +invenio roles create admin -d Admin +invenio roles create moderator -d Moderator +invenio roles create submitter -d Submitter +invenio roles create user -d User section "Initialize access for roles" "info" invenio access allow superuser-access role superuser @@ -111,6 +111,21 @@ 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 -a monitoring@rero.ch --password monitor +invenio users confirm monitoring@rero.ch +invenio roles create monitoring -d Monitoring +invenio roles add monitoring@rero.ch monitoring +# create token access for monitoring +# if the environement variable INVENIO_RERO_ACCESS_TOKEN_MONITORING is not set +# a new token will be generated +if [ -z ${INVENIO_RERO_ACCESS_TOKEN_MONITORING} ] +then + eval ${PREFIX} invenio utils tokens_create -n monitoring -u monitoring@rero.ch +else + eval ${PREFIX} invenio utils tokens_create -n monitoring -u monitoring@rero.ch -t ${INVENIO_RERO_ACCESS_TOKEN_MONITORING} +fi + # Create a default location for depositing files section "Create location for storing files" "info" invenio fixtures deposits create diff --git a/sonar/modules/cli/utils.py b/sonar/modules/cli/utils.py index cc64142d..c82d0fac 100644 --- a/sonar/modules/cli/utils.py +++ b/sonar/modules/cli/utils.py @@ -27,15 +27,24 @@ import jsonref from flask import current_app from flask.cli import with_appcontext +from flask_security.confirmable import confirm_user +from invenio_accounts.cli import commit, users +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 +80,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 +129,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 +172,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 +185,89 @@ 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('tokens_create') +@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 tokens_create(name, user, scopes, internal, access_token): + """Create a personal OAuth token.""" + 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') + + +@users.command() +@click.argument('user') +@with_appcontext +@commit +def confirm(user): + """Confirm a user.""" + user_obj = _datastore.get_user(user) + if user_obj is None: + raise click.UsageError('ERROR: User not found.') + if confirm_user(user_obj): + click.secho(f'User "{user}" has been confirmed.', fg='green') + else: + click.secho('User "{user}" was already confirmed.', fg='yellow') diff --git a/sonar/modules/permissions.py b/sonar/modules/permissions.py index 2eede9a2..f9bdeeed 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 30a0e569..6a8761be 100644 --- a/sonar/monitoring/views/__init__.py +++ b/sonar/monitoring/views/__init__.py @@ -26,33 +26,37 @@ 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 + role `superuser`, `monitoring`. + """ @wraps(func) def decorated_view(*args, **kwargs): + print('--->', current_user) 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, + with decorator. + """ @api_blueprint.route('/db_connection_counts') @@ -65,7 +69,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: