Skip to content

Commit

Permalink
monitoring: access token
Browse files Browse the repository at this point in the history
* Adds monitoring user and access token for setup.

Co-Authored-by: Peter Weber <peter.weber@rero.ch>
  • Loading branch information
rerowep authored and jma committed Jan 18, 2024
1 parent dd36822 commit eef7415
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 30 deletions.
9 changes: 9 additions & 0 deletions scripts/functions
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
7 changes: 7 additions & 0 deletions scripts/setup
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions sonar/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down
101 changes: 88 additions & 13 deletions sonar/modules/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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')

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)

Expand All @@ -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')
2 changes: 2 additions & 0 deletions sonar/modules/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})()
Expand Down
19 changes: 8 additions & 11 deletions sonar/monitoring/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions tests/ui/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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']

0 comments on commit eef7415

Please sign in to comment.