Commit 91ceb8e3 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Added a based 'mirror' endpoint to export upload metadata.

parent 80108942
......@@ -29,7 +29,7 @@ There is a separate documentation for the API endpoints from a client perspectiv
.. automodule:: nomad.api.admin
"""
from .app import app
from . import info, auth, admin, upload, repo, archive, raw
from . import info, auth, admin, upload, repo, archive, raw, mirror
@app.before_first_request
......
......@@ -12,13 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from flask import g, request
from flask import request
from flask_restplus import abort, Resource, fields
from nomad import infrastructure, config
from .app import api
from .auth import login_really_required
from .auth import admin_login_required
ns = api.namespace('admin', description='Administrative operations')
......@@ -29,7 +29,7 @@ class AdminResetResource(Resource):
@api.doc('exec_reset_command')
@api.response(200, 'Reset performed')
@api.response(400, 'Reset not available/disabled')
@login_really_required
@admin_login_required
def post(self):
"""
The ``reset`` command will attempt to clear the contents of all databased and
......@@ -37,9 +37,6 @@ class AdminResetResource(Resource):
Nomad can be configured to disable reset and the command might not be available.
"""
if not g.user.is_admin:
abort(401, message='Only the admin user can perform reset.')
if config.services.disable_reset:
abort(400, message='Operation is disabled')
......@@ -53,7 +50,7 @@ class AdminRemoveResource(Resource):
@api.doc('exec_remove_command')
@api.response(200, 'Remove performed')
@api.response(400, 'Remove not available/disabled')
@login_really_required
@admin_login_required
def post(self):
"""
The ``remove``command will attempt to remove all databases. Expect the
......@@ -61,8 +58,6 @@ class AdminRemoveResource(Resource):
Nomad can be configured to disable remove and the command might not be available.
"""
if not g.user.is_admin:
abort(401, message='Only the admin user can perform remove.')
if config.services.disable_reset:
abort(400, message='Operation is disabled')
......@@ -77,21 +72,20 @@ pidprefix_model = api.model('PidPrefix', {
})
# TODO remove after migration
@ns.route('/pidprefix')
class AdminPidPrefixResource(Resource):
@api.doc('exec_pidprefix_command')
@api.response(200, 'Pid prefix set')
@api.response(400, 'Bad pid prefix data')
@api.expect(pidprefix_model)
@login_really_required
@admin_login_required
def post(self):
"""
The ``pidprefix``command will set the pid counter to the given value.
This might be useful while migrating data with old pids.
"""
if not g.user.is_admin:
abort(401, message='Only the admin user can perform remove.')
infrastructure.set_pid_prefix(**request.get_json())
......
......@@ -128,6 +128,24 @@ def login_really_required(func):
return wrapper
def admin_login_required(func):
"""
A decorator for API endpoint implementations that should only work for the admin user.
"""
@api.response(401, 'Authentication required or not authorized as admin user. Only admin can access this endpoint.')
@api.doc(security=list(api.authorizations.keys()))
@login_really_required
def wrapper(*args, **kwargs):
if not g.user.is_admin:
abort(401, message='Only the admin user can perform reset.')
else:
return func(*args, **kwargs)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
ns = api.namespace(
'auth',
description='Authentication related endpoints.')
......
......@@ -52,3 +52,14 @@ def calc_route(ns, prefix: str = ''):
})(func)
)
return decorator
def upload_route(ns, prefix: str = ''):
""" A resource decorator for /<upload> based routes. """
def decorator(func):
ns.route('%s/<string:upload_id>' % prefix)(
api.doc(params={
'upload_id': 'The unique id for the requested upload.'
})(func)
)
return decorator
# Copyright 2018 Markus Scheidgen
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an"AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The mirror API of the nomad@FAIRDI APIs. Allows to export upload metadata.
"""
from flask_restplus import Resource, abort, fields
from nomad import processing
from .app import api
from .auth import admin_login_required
from .common import upload_route
ns = api.namespace('mirror', description='Export upload (and all calc) metadata.')
mirror_upload_model = api.model('MirrorUpload', {
'upload_id': fields.String(description='The id of the exported upload'),
'upload': fields.String(description='The upload metadata as mongoengine json string'),
'calcs': fields.List(fields.String, description='All upload calculation metadata as mongoengine json strings'),
'upload_files_path': fields.String(description='The path to the local uploads file folder')
})
@upload_route(ns)
class MirrorUploadResource(Resource):
@api.response(400, 'Not available for the given upload, e.g. upload not published.')
@api.response(404, 'The upload does not exist')
@api.marshal_with(mirror_upload_model, skip_none=True, code=200, description='Upload exported')
@api.doc('get_mirror_upload')
@admin_login_required
def get(self, upload_id):
"""
Export upload (and all calc) metadata for mirrors.
"""
try:
upload = processing.Upload.get(upload_id)
except KeyError:
abort(404, message='Upload with id %s does not exist.' % upload_id)
if not upload.published:
abort(400, message='Only published uploads can be exported')
return {
'upload_id': upload.upload_id,
'upload': upload.to_json(),
'calcs': [calc.to_json() for calc in upload.calcs],
'upload_files_path': upload.upload_files.os_path
}, 200
......@@ -32,7 +32,7 @@ from nomad.processing import ProcessAlreadyRunning
from .app import api, with_logger, RFC3339DateTime
from .auth import login_really_required
from .common import pagination_request_parser, pagination_model
from .common import pagination_request_parser, pagination_model, upload_route
ns = api.namespace(
......@@ -342,8 +342,7 @@ class ProxyUpload:
return self.upload.__getattribute__(name)
@ns.route('/<string:upload_id>')
@api.doc(params={'upload_id': 'The unique id for the requested upload.'})
@upload_route(ns)
class UploadResource(Resource):
@api.doc('get_upload')
@api.response(404, 'Upload does not exist')
......
......@@ -1022,6 +1022,21 @@ class TestRaw(UploadFilesBasedTests):
assert rv.status_code == 404
class TestMirror:
def test_upload(self, client, published, admin_user_auth, no_warn):
url = '/mirror/%s' % published.upload_id
rv = client.get(url, headers=admin_user_auth)
assert rv.status_code == 200
data = json.loads(rv.data)
assert data['upload_id'] == published.upload_id
assert json.loads(data['upload'])['_id'] == published.upload_id
assert Upload.from_json(data['upload']).upload_id == published.upload_id
assert len(data['calcs']) == len(published.calcs)
assert data['upload_files_path'] == published.upload_files.os_path
def test_docs(client):
rv = client.get('/docs/index.html')
rv = client.get('/docs/introduction.html')
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment