diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 15b43cdb..00000000 --- a/.pylintrc +++ /dev/null @@ -1,447 +0,0 @@ -# This Pylint rcfile contains a best-effort configuration to uphold the -# best-practices and style described in the Google Python style guide: -# https://google.github.io/styleguide/pyguide.html -# -# Its canonical open-source location is: -# https://google.github.io/styleguide/pylintrc - -[MASTER] - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=third_party - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Pickle collected data for later comparisons. -persistent=no - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=4 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=abstract-method, - apply-builtin, - arguments-differ, - attribute-defined-outside-init, - backtick, - bad-option-value, - basestring-builtin, - buffer-builtin, - c-extension-no-member, - consider-using-enumerate, - cmp-builtin, - cmp-method, - coerce-builtin, - coerce-method, - delslice-method, - div-method, - duplicate-code, - eq-without-hash, - execfile-builtin, - file-builtin, - filter-builtin-not-iterating, - fixme, - getslice-method, - global-statement, - hex-method, - idiv-method, - implicit-str-concat-in-sequence, - import-error, - import-self, - import-star-module-level, - inconsistent-return-statements, - input-builtin, - intern-builtin, - invalid-str-codec, - locally-disabled, - long-builtin, - long-suffix, - map-builtin-not-iterating, - misplaced-comparison-constant, - missing-function-docstring, - metaclass-assignment, - next-method-called, - next-method-defined, - no-absolute-import, - no-else-break, - no-else-continue, - no-else-raise, - no-else-return, - no-init, # added - no-member, - no-name-in-module, - no-self-use, - nonzero-method, - oct-method, - old-division, - old-ne-operator, - old-octal-literal, - old-raise-syntax, - parameter-unpacking, - print-statement, - raising-string, - range-builtin-not-iterating, - raw_input-builtin, - rdiv-method, - reduce-builtin, - relative-import, - reload-builtin, - round-builtin, - setslice-method, - signature-differs, - standarderror-builtin, - suppressed-message, - sys-max-int, - too-few-public-methods, - too-many-ancestors, - too-many-arguments, - too-many-boolean-expressions, - too-many-branches, - too-many-instance-attributes, - too-many-locals, - too-many-nested-blocks, - too-many-public-methods, - too-many-return-statements, - too-many-statements, - trailing-newlines, - unichr-builtin, - unicode-builtin, - unnecessary-pass, - unpacking-in-except, - useless-else-on-loop, - useless-object-inheritance, - useless-suppression, - using-cmp-argument, - wrong-import-order, - xrange-builtin, - zip-builtin-not-iterating, - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". This option is deprecated -# and it will be removed in Pylint 2.0. -files-output=no - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# Good variable names which should always be accepted, separated by a comma -good-names=main,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl - -# Regular expression matching correct function names -function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ - -# Regular expression matching correct variable names -variable-rgx=^[a-z][a-z0-9_]*$ - -# Regular expression matching correct constant names -const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ - -# Regular expression matching correct attribute names -attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ - -# Regular expression matching correct argument names -argument-rgx=^[a-z][a-z0-9_]*$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=^[a-z][a-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=^_?[A-Z][a-zA-Z0-9]*$ - -# Regular expression matching correct module names -module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ - -# Regular expression matching correct method names -method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=10 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt -# lines made too long by directives to pytype. - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=(?x)( - ^\s*(\#\ )??$| - ^\s*(from\s+\S+\s+)?import\s+.+$) - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=yes - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check= - -# Maximum number of lines in a module -max-module-lines=99999 - -# String used as indentation unit. The internal Google style guide mandates 2 -# spaces. Google's externaly-published style guide says 4, consistent with -# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google -# projects (like TensorFlow). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=TODO - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=yes - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging,absl.logging,tensorflow.io.logging - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec, - sets - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant, absl - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls, - class_ - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=StandardError, - Exception, - BaseException diff --git a/Dockerfile-backend b/Dockerfile-backend index e0b3265c..540ac09d 100644 --- a/Dockerfile-backend +++ b/Dockerfile-backend @@ -1,6 +1,6 @@ FROM python:3.8-alpine -RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev +RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev rust cargo COPY ./backend/requirements.txt /requirements.txt diff --git a/Dockerfile-frontend b/Dockerfile-frontend index 234a88fc..03ec18e6 100644 --- a/Dockerfile-frontend +++ b/Dockerfile-frontend @@ -1,4 +1,4 @@ -FROM node:alpine +FROM node:14-alpine RUN yarn global add @quasar/cli diff --git a/README.md b/README.md index cec84712..257a7b77 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ The backend requires a `config.yaml` file to be mounted to `/config.yaml`. The frontend assumes that the backend is available at `/api`. +## Code + +`backend`: python3, flask + +`frontend`: Quasar (Vue) + +`docs`: Sphinx + [travis-badge]: https://api.travis-ci.com/ScilifelabDataCentre/Data-Tracker.svg?branch=develop [travis-link]: https://travis-ci.com/ScilifelabDataCentre/Data-Tracker diff --git a/backend/app.py b/backend/app.py index 60fbe3a8..93aeb3ba 100644 --- a/backend/app.py +++ b/backend/app.py @@ -21,21 +21,19 @@ appconf = config.init() db_management.check_db(appconf) app.config.update(appconf) -app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=31) +if app.config["dev_mode"]["api"]: + app.register_blueprint(developer.blueprint, url_prefix="/api/v1/developer") -if app.config['dev_mode']['api']: - app.register_blueprint(developer.blueprint, url_prefix='/api/v1/developer') - -app.register_blueprint(dataset.blueprint, url_prefix='/api/v1/dataset') -app.register_blueprint(order.blueprint, url_prefix='/api/v1/order') -app.register_blueprint(collection.blueprint, url_prefix='/api/v1/collection') -app.register_blueprint(user.blueprint, url_prefix='/api/v1/user') +app.register_blueprint(dataset.blueprint, url_prefix="/api/v1/dataset") +app.register_blueprint(order.blueprint, url_prefix="/api/v1/order") +app.register_blueprint(collection.blueprint, url_prefix="/api/v1/collection") +app.register_blueprint(user.blueprint, url_prefix="/api/v1/user") oauth = OAuth(app) -for oidc_name in app.config.get('oidc_names'): - oauth.register(oidc_name, client_kwargs={'scope': 'openid profile email'}) +for oidc_name in app.config.get("oidc_names"): + oauth.register(oidc_name, client_kwargs={"scope": "openid profile email"}) @app.before_request @@ -43,95 +41,97 @@ def prepare(): """Open the database connection and get the current user.""" flask.g.dbclient = utils.get_dbclient(flask.current_app.config) flask.g.db = utils.get_db(flask.g.dbclient, flask.current_app.config) - if apikey := flask.request.headers.get('X-API-Key'): - if not (apiuser := flask.request.headers.get('X-API-User')): # pylint: disable=superfluous-parens + if apikey := flask.request.headers.get("X-API-Key"): + if not ( + apiuser := flask.request.headers.get("X-API-User") + ): # pylint: disable=superfluous-parens flask.abort(status=400) utils.verify_api_key(apiuser, apikey) - flask.g.current_user = flask.g.db['users'].find_one({'auth_ids': apiuser}) - flask.g.permissions = flask.g.current_user['permissions'] + flask.g.current_user = flask.g.db["users"].find_one({"auth_ids": apiuser}) + flask.g.permissions = flask.g.current_user["permissions"] else: - if flask.request.method != 'GET': + if flask.request.method != "GET": utils.verify_csrf_token() flask.g.current_user = user.get_current_user() - flask.g.permissions = flask.g.current_user['permissions'] if flask.g.current_user else None + flask.g.permissions = ( + flask.g.current_user["permissions"] if flask.g.current_user else None + ) @app.after_request def finalize(response): """Finalize the response and clean up.""" # close db connection - if hasattr(flask.g, 'dbserver'): + if hasattr(flask.g, "dbserver"): flask.g.dbserver.close() # set csrf cookie if not set - if not flask.request.cookies.get('_csrf_token'): - response.set_cookie('_csrf_token', utils.gen_csrf_token(), samesite='Lax') + if not flask.request.cookies.get("_csrf_token"): + response.set_cookie("_csrf_token", utils.gen_csrf_token(), samesite="Lax") # add some headers for protection - response.headers['X-Frame-Options'] = 'SAMEORIGIN' - response.headers['X-XSS-Protection'] = '1; mode=block' + response.headers["X-Frame-Options"] = "SAMEORIGIN" + response.headers["X-XSS-Protection"] = "1; mode=block" return response -@app.route('/api/v1/') +@app.route("/api/v1/") def api_base(): """List entities.""" - return flask.jsonify({'entities': ['dataset', 'order', 'collection', 'user', 'login']}) + return flask.jsonify( + {"entities": ["dataset", "order", "collection", "user", "login"]} + ) -@app.route('/api/v1/login/') +@app.route("/api/v1/login/") def login_types(): """List login types.""" - return flask.jsonify({'types': ['apikey', 'oidc']}) + return flask.jsonify({"types": ["apikey", "oidc"]}) -@app.route('/api/v1/login/oidc/') +@app.route("/api/v1/login/oidc/") def oidc_types(): """List OpenID Connect types.""" auth_types = {} - for auth_name in app.config.get('oidc_names'): - auth_types[auth_name] = flask.url_for('oidc_login', - auth_name=auth_name) + for auth_name in app.config.get("oidc_names"): + auth_types[auth_name] = flask.url_for("oidc_login", auth_name=auth_name) return flask.jsonify(auth_types) -@app.route('/api/v1/login/oidc//login/') +@app.route("/api/v1/login/oidc//login/") def oidc_login(auth_name): """Perform a login using OpenID Connect (e.g. Elixir AAI).""" client = oauth.create_client(auth_name) - redirect_uri = flask.url_for('oidc_authorize', - auth_name=auth_name, - _external=True) - flask.session['incoming_url'] = flask.request.args.get('origin') or '/' + redirect_uri = flask.url_for("oidc_authorize", auth_name=auth_name, _external=True) + flask.session["incoming_url"] = flask.request.args.get("origin") or "/" return client.authorize_redirect(redirect_uri) -@app.route('/api/v1/login/oidc//authorize/') +@app.route("/api/v1/login/oidc//authorize/") def oidc_authorize(auth_name): """Authorize a login using OpenID Connect (e.g. Elixir AAI).""" - if auth_name not in app.config.get('oidc_names'): + if auth_name not in app.config.get("oidc_names"): flask.abort(status=404) client = oauth.create_client(auth_name) token = client.authorize_access_token() - if 'id_token' in token: + if "id_token" in token: user_info = client.parse_id_token(token) else: user_info = client.userinfo() - if auth_name != 'elixir': - user_info['auth_id'] = f'{user_info["email"]}::{auth_name}' + if auth_name != "elixir": + user_info["auth_id"] = f'{user_info["email"]}::{auth_name}' else: - user_info['auth_id'] = token['sub'] - if not user.do_login(user_info['auth_id']): + user_info["auth_id"] = token["sub"] + if not user.do_login(user_info["auth_id"]): user.add_new_user(user_info) - user.do_login(user_info['auth_id']) + user.do_login(user_info["auth_id"]) - response = flask.redirect(flask.session['incoming_url']) - del flask.session['incoming_url'] - response.set_cookie('loggedIn', 'true', max_age=datetime.timedelta(days=31)) + response = flask.redirect(flask.session["incoming_url"]) + del flask.session["incoming_url"] return response # requests -@app.route('/api/v1/login/apikey/', methods=['POST']) +@app.route("/api/v1/login/apikey/", methods=["POST"]) def key_login(): """Log in using an apikey.""" try: @@ -139,23 +139,21 @@ def key_login(): except json.decoder.JSONDecodeError: flask.abort(status=400) - if 'api-user' not in indata or 'api-key' not in indata: - app.logger.debug('API key login - bad keys: %s', indata) + if "api-user" not in indata or "api-key" not in indata: + app.logger.debug("API key login - bad keys: %s", indata) return flask.Response(status=400) - utils.verify_api_key(indata['api-user'], indata['api-key']) - user.do_login(auth_id=indata['api-user']) + utils.verify_api_key(indata["api-user"], indata["api-key"]) + user.do_login(auth_id=indata["api-user"]) response = flask.Response(status=200) - response.set_cookie('loggedIn', 'true', max_age=datetime.timedelta(days=31)) return response -@app.route('/api/v1/logout/') +@app.route("/api/v1/logout/") def logout(): """Log out the current user.""" flask.session.clear() response = flask.Response(status=200) - response.set_cookie('_csrf_token', utils.gen_csrf_token(), 0) - response.set_cookie('loggedIn', 'false', 0) + response.set_cookie("_csrf_token", utils.gen_csrf_token(), 0) return response @@ -184,10 +182,10 @@ def error_not_found(_): # to allow coverage check for testing -if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000) +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) else: - # Assume this means it's handled by gunicorn - gunicorn_logger = logging.getLogger('gunicorn.error') - app.logger.handlers = gunicorn_logger.handlers - app.logger.setLevel(gunicorn_logger.level) + gunicorn_logger = logging.getLogger("gunicorn.error") + if gunicorn_logger: + app.logger.handlers = gunicorn_logger.handlers + app.logger.setLevel(gunicorn_logger.level) diff --git a/backend/collection.py b/backend/collection.py index 0b99c299..850a486b 100644 --- a/backend/collection.py +++ b/backend/collection.py @@ -7,21 +7,22 @@ import user import utils -blueprint = flask.Blueprint('collection', __name__) # pylint: disable=invalid-name +blueprint = flask.Blueprint("collection", __name__) # pylint: disable=invalid-name -@blueprint.route('/', methods=['GET']) +@blueprint.route("/", methods=["GET"]) def list_collection(): """Provide a simplified list of all available collections.""" - results = list(flask.g.db['collections'].find(projection={'title': 1, - '_id': 1, - 'tags': 1, - 'properties': 1})) - return utils.response_json({'collections': results}) + results = list( + flask.g.db["collections"].find( + projection={"title": 1, "_id": 1, "tags": 1, "properties": 1} + ) + ) + return utils.response_json({"collections": results}) -@blueprint.route('/random/', methods=['GET']) -@blueprint.route('/random/', methods=['GET']) +@blueprint.route("/random/", methods=["GET"]) +@blueprint.route("/random/", methods=["GET"]) def get_random(amount: int = 1): """ Retrieve random collection(s). @@ -33,25 +34,28 @@ def get_random(amount: int = 1): flask.Request: json structure for the collection(s) """ - results = list(flask.g.db['collections'].aggregate([{'$sample': {'size': amount}}])) + results = list(flask.g.db["collections"].aggregate([{"$sample": {"size": amount}}])) for result in results: # only show editors if editor/admin - if not flask.g.current_user or\ - (not user.has_permission('DATA_MANAGEMENT') or - flask.g.current_user['_id'] not in result['editors']): - flask.current_app.logger.debug('Not allowed to access editors field %s', - flask.g.current_user) - del result['editors'] + if not flask.g.current_user or ( + not user.has_permission("DATA_MANAGEMENT") + or flask.g.current_user["_id"] not in result["editors"] + ): + flask.current_app.logger.debug( + "Not allowed to access editors field %s", flask.g.current_user + ) + del result["editors"] # return {_id, _title} for datasets - result['datasets'] = [flask.g.db.datasets.find_one({'_id': dataset}, - {'title': 1}) - for dataset in result['datasets']] - return utils.response_json({'collections': results}) + result["datasets"] = [ + flask.g.db.datasets.find_one({"_id": dataset}, {"title": 1}) + for dataset in result["datasets"] + ] + return utils.response_json({"collections": results}) -@blueprint.route('//', methods=['GET']) +@blueprint.route("//", methods=["GET"]) def get_collection(identifier): """ Retrieve the collection with uuid . @@ -68,29 +72,32 @@ def get_collection(identifier): except ValueError: flask.abort(status=404) - result = flask.g.db['collections'].find_one({'_id': uuid}) + result = flask.g.db["collections"].find_one({"_id": uuid}) if not result: return flask.Response(status=404) # only show owner if owner/admin - if not flask.g.current_user or\ - (not user.has_permission('DATA_MANAGEMENT') and - flask.g.current_user['_id'] not in result['editors']): - flask.current_app.logger.debug('Not allowed to access editors field %s', - flask.g.current_user) - del result['editors'] + if not flask.g.current_user or ( + not user.has_permission("DATA_MANAGEMENT") + and flask.g.current_user["_id"] not in result["editors"] + ): + flask.current_app.logger.debug( + "Not allowed to access editors field %s", flask.g.current_user + ) + del result["editors"] else: - result['editors'] = utils.user_uuid_data(result['editors'], flask.g.db) + result["editors"] = utils.user_uuid_data(result["editors"], flask.g.db) # return {_id, _title} for datasets - result['datasets'] = [flask.g.db.datasets.find_one({'_id': dataset}, - {'title': 1}) - for dataset in result['datasets']] + result["datasets"] = [ + flask.g.db.datasets.find_one({"_id": dataset}, {"title": 1}) + for dataset in result["datasets"] + ] - return utils.response_json({'collection': result}) + return utils.response_json({"collection": result}) -@blueprint.route('/structure/', methods=['GET']) +@blueprint.route("/structure/", methods=["GET"]) def get_collection_data_structure(): """ Get an empty collection entry. @@ -99,11 +106,11 @@ def get_collection_data_structure(): flask.Response: JSON structure with a list of collections. """ empty_collection = structure.collection() - empty_collection['_id'] = '' - return utils.response_json({'collection': empty_collection}) + empty_collection["_id"] = "" + return utils.response_json({"collection": empty_collection}) -@blueprint.route('/', methods=['POST']) +@blueprint.route("/", methods=["POST"]) @user.login_required def add_collection(): # pylint: disable=too-many-branches """ @@ -121,36 +128,32 @@ def add_collection(): # pylint: disable=too-many-branches flask.abort(status=400) # indata validation - validation = utils.basic_check_indata(indata, collection, prohibited=['_id']) + validation = utils.basic_check_indata(indata, collection, prohibited=["_id"]) if not validation[0]: flask.abort(status=validation[1]) - # properties may only be set by users with DATA_MANAGEMENT - if 'properties' in indata: - if not user.has_permission('DATA_MANAGEMENT'): - flask.abort(403) - - if 'title' not in indata: + if "title" not in indata: flask.abort(status=400) - if not indata.get('editors'): - indata['editors'] = [flask.g.current_user['_id']] + if not indata.get("editors"): + indata["editors"] = [flask.g.current_user["_id"]] - if 'datasets' in indata: - indata['datasets'] = [utils.str_to_uuid(value) for value in indata['datasets']] + if "datasets" in indata: + indata["datasets"] = [utils.str_to_uuid(value) for value in indata["datasets"]] collection.update(indata) + collection["description"] = utils.secure_description(collection["description"]) # add to db - result = flask.g.db['collections'].insert_one(collection) + result = flask.g.db["collections"].insert_one(collection) if not result.acknowledged: - flask.current_app.logger.error('Collection insert failed: %s', collection) + flask.current_app.logger.error("Collection insert failed: %s", collection) else: - utils.make_log('collection', 'add', 'Collection added', collection) + utils.make_log("collection", "add", "Collection added", collection) - return utils.response_json({'_id': result.inserted_id}) + return utils.response_json({"_id": result.inserted_id}) -@blueprint.route('//', methods=['DELETE']) +@blueprint.route("//", methods=["DELETE"]) @user.login_required def delete_collection(identifier: str): """ @@ -165,25 +168,27 @@ def delete_collection(identifier: str): ds_uuid = utils.str_to_uuid(identifier) except ValueError: return flask.abort(status=404) - collection = flask.g.db['collections'].find_one({'_id': ds_uuid}) + collection = flask.g.db["collections"].find_one({"_id": ds_uuid}) if not collection: flask.abort(status=404) # permission check - if not user.has_permission('DATA_MANAGEMENT') and \ - flask.g.current_user['_id'] not in collection['editors']: + if ( + not user.has_permission("DATA_MANAGEMENT") + and flask.g.current_user["_id"] not in collection["editors"] + ): flask.abort(status=403) - result = flask.g.db['collections'].delete_one({'_id': ds_uuid}) + result = flask.g.db["collections"].delete_one({"_id": ds_uuid}) if not result.acknowledged: - flask.current_app.logger.error('Failed to delete collection %s', ds_uuid) + flask.current_app.logger.error("Failed to delete collection %s", ds_uuid) return flask.Response(status=500) - utils.make_log('collection', 'delete', 'Deleted collection', data={'_id': ds_uuid}) + utils.make_log("collection", "delete", "Deleted collection", data={"_id": ds_uuid}) return flask.Response(status=200) -@blueprint.route('//', methods=['PATCH']) +@blueprint.route("//", methods=["PATCH"]) @user.login_required def update_collection(identifier): # pylint: disable=too-many-branches """ @@ -199,7 +204,7 @@ def update_collection(identifier): # pylint: disable=too-many-branches collection_uuid = utils.str_to_uuid(identifier) except ValueError: return flask.abort(status=404) - collection = flask.g.db['collections'].find_one({'_id': collection_uuid}) + collection = flask.g.db["collections"].find_one({"_id": collection_uuid}) if not collection: flask.abort(status=404) @@ -209,29 +214,31 @@ def update_collection(identifier): # pylint: disable=too-many-branches flask.abort(status=400) # permission check - if not user.has_permission('DATA_MANAGEMENT') and \ - flask.g.current_user['_id'] not in collection['editors'] and\ - flask.g.current_user['email'] not in collection['editors']: - flask.current_app.logger.debug('Unauthorized update attempt (collection %s, user %s)', - collection_uuid, - flask.g.current_user['_id']) + if ( + not user.has_permission("DATA_MANAGEMENT") + and flask.g.current_user["_id"] not in collection["editors"] + and flask.g.current_user["email"] not in collection["editors"] + ): + flask.current_app.logger.debug( + "Unauthorized update attempt (collection %s, user %s)", + collection_uuid, + flask.g.current_user["_id"], + ) flask.abort(status=403) # indata validation - validation = utils.basic_check_indata(indata, collection, prohibited=('_id')) + validation = utils.basic_check_indata(indata, collection, prohibited=("_id")) if not validation[0]: flask.abort(status=validation[1]) - # properties may only be set by users with DATA_MANAGEMENT - if 'properties' in indata: - if not user.has_permission('DATA_MANAGEMENT'): - flask.abort(403) + if "datasets" in indata: + indata["datasets"] = [utils.str_to_uuid(value) for value in indata["datasets"]] - if 'datasets' in indata: - indata['datasets'] = [utils.str_to_uuid(value) for value in indata['datasets']] + if "editors" in indata and not indata["editors"]: + indata["editors"] = [flask.g.current_user["_id"]] - if 'editors' in indata and not indata['editors']: - indata['editors'] = [flask.g.current_user['_id']] + if "description" in indata: + indata["description"] = utils.secure_description(indata["description"]) is_different = False for field in indata: @@ -240,17 +247,19 @@ def update_collection(identifier): # pylint: disable=too-many-branches break if indata and is_different: - result = flask.g.db['collections'].update_one({'_id': collection['_id']}, {'$set': indata}) + result = flask.g.db["collections"].update_one( + {"_id": collection["_id"]}, {"$set": indata} + ) if not result.acknowledged: - flask.current_app.logger.error('Collection update failed: %s', indata) + flask.current_app.logger.error("Collection update failed: %s", indata) else: collection.update(indata) - utils.make_log('collection', 'edit', 'Collection updated', collection) + utils.make_log("collection", "edit", "Collection updated", collection) return flask.Response(status=200) -@blueprint.route('/user/', methods=['GET']) +@blueprint.route("/user/", methods=["GET"]) @user.login_required def list_user_collections(): # pylint: disable=too-many-branches """ @@ -259,12 +268,13 @@ def list_user_collections(): # pylint: disable=too-many-branches Returns: flask.Response: JSON structure. """ - results = list(flask.g.db['collections'] - .find({'editors': flask.g.current_user['_id']})) - return utils.response_json({'collections': results}) + results = list( + flask.g.db["collections"].find({"editors": flask.g.current_user["_id"]}) + ) + return utils.response_json({"collections": results}) -@blueprint.route('//log/', methods=['GET']) +@blueprint.route("//log/", methods=["GET"]) @user.login_required def get_collection_log(identifier: str = None): """ @@ -283,21 +293,28 @@ def get_collection_log(identifier: str = None): except ValueError: flask.abort(status=404) - if not user.has_permission('DATA_MANAGEMENT'): - collection_data = flask.g.db['collections'].find_one({'_id': collection_uuid}) + if not user.has_permission("DATA_MANAGEMENT"): + collection_data = flask.g.db["collections"].find_one({"_id": collection_uuid}) if not collection_data: flask.abort(403) - if flask.g.current_user['_id'] not in collection_data['editors']: + if flask.g.current_user["_id"] not in collection_data["editors"]: flask.abort(403) - collection_logs = list(flask.g.db['logs'].find({'data_type': 'collection', - 'data._id': collection_uuid})) + collection_logs = list( + flask.g.db["logs"].find( + {"data_type": "collection", "data._id": collection_uuid} + ) + ) for log in collection_logs: - del log['data_type'] + del log["data_type"] utils.incremental_logs(collection_logs) - return utils.response_json({'entry_id': collection_uuid, - 'data_type': 'collection', - 'logs': collection_logs}) + return utils.response_json( + { + "entry_id": collection_uuid, + "data_type": "collection", + "logs": collection_logs, + } + ) diff --git a/backend/config.py b/backend/config.py index bb84268f..09e93f5c 100755 --- a/backend/config.py +++ b/backend/config.py @@ -11,7 +11,7 @@ import yaml -def read_config(path: str = ''): +def read_config(path: str = ""): """ Look for settings.yaml and parse the settings from there. @@ -27,16 +27,15 @@ def read_config(path: str = ''): FileNotFoundError: No settings file found """ - file_locations = [os.getcwd(), - os.pardir] + file_locations = [os.getcwd(), os.pardir] if not path: for location in file_locations: - fpath = os.path.join(location, 'config.yaml') + fpath = os.path.join(location, "config.yaml") if os.path.exists(fpath): path = fpath break - with open(path, 'r') as in_file: + with open(path, "r") as in_file: return yaml.load(in_file, Loader=yaml.FullLoader) @@ -47,31 +46,33 @@ def init() -> dict: Returns: dict: The config. """ - config_file = '' - arg = '--config_file' + config_file = "" + arg = "--config_file" if arg in sys.argv: try: - config_file = sys.argv[sys.argv.index(arg)+1] + config_file = sys.argv[sys.argv.index(arg) + 1] except IndexError: - logging.error('No argument for --config_file') + logging.error("No argument for --config_file") sys.exit(1) config = read_config(config_file) - if config['dev_mode']['testing']: + if config["dev_mode"]["testing"]: logging.getLogger().setLevel(logging.DEBUG) - config['DEBUG'] = True - config['TESTING'] = True - config['ENV'] = 'development' + config["DEBUG"] = True + config["TESTING"] = True + config["ENV"] = "development" - if config.get('oidc'): - for oidc_entry in config['oidc']: + if config.get("oidc"): + for oidc_entry in config["oidc"]: base_name = oidc_entry.upper() - for conf_part in config['oidc'][oidc_entry]: - config[f'{base_name}_{conf_part.upper()}'] = config['oidc'][oidc_entry][conf_part] - config['oidc_names'] = config['oidc'].keys() - del config['oidc'] - - config['SESSION_COOKIE_NAME'] = 'dt_session' - config['SECRET_KEY'] = config['flask']['secret'] - config['SESSION_COOKIE_SAMESITE'] = 'Lax' + for conf_part in config["oidc"][oidc_entry]: + config[f"{base_name}_{conf_part.upper()}"] = config["oidc"][oidc_entry][ + conf_part + ] + config["oidc_names"] = config["oidc"].keys() + del config["oidc"] + + config["SESSION_COOKIE_NAME"] = "dt_session" + config["SECRET_KEY"] = config["flask"]["secret"] + config["SESSION_COOKIE_SAMESITE"] = "Lax" return config diff --git a/backend/dataset.py b/backend/dataset.py index 8469e9ad..fb39e50e 100644 --- a/backend/dataset.py +++ b/backend/dataset.py @@ -7,34 +7,37 @@ import utils import user -blueprint = flask.Blueprint('dataset', __name__) # pylint: disable=invalid-name +blueprint = flask.Blueprint("dataset", __name__) # pylint: disable=invalid-name -@blueprint.route('/', methods=['GET']) +@blueprint.route("/", methods=["GET"]) def list_datasets(): """Provide a simplified list of all available datasets.""" - results = list(flask.g.db['datasets'].find(projection={'title': 1, - '_id': 1, - 'tags': 1, - 'properties': 1})) - return utils.response_json({'datasets': results}) + results = list( + flask.g.db["datasets"].find( + projection={"title": 1, "_id": 1, "tags": 1, "properties": 1} + ) + ) + return utils.response_json({"datasets": results}) -@blueprint.route('/user/', methods=['GET']) +@blueprint.route("/user/", methods=["GET"]) @user.login_required def list_user_data(): """List all datasets belonging to current user.""" - user_orders = list(flask.g.db['orders'].find({'$or': [{'receivers': flask.session['user_id']}, - {'editors': flask.session['user_id']}]}, - {'datasets': 1})) - uuids = list(ds for entry in user_orders for ds in entry['datasets']) - user_datasets = list(flask.g.db['datasets'].find({'_id': {'$in': uuids}})) + user_orders = list( + flask.g.db["orders"].find( + {"editors": flask.session["user_id"]}, {"datasets": 1} + ) + ) + uuids = list(ds for entry in user_orders for ds in entry["datasets"]) + user_datasets = list(flask.g.db["datasets"].find({"_id": {"$in": uuids}})) - return utils.response_json({'datasets': user_datasets}) + return utils.response_json({"datasets": user_datasets}) -@blueprint.route('/random/', methods=['GET']) -@blueprint.route('/random//', methods=['GET']) +@blueprint.route("/random/", methods=["GET"]) +@blueprint.route("/random//", methods=["GET"]) def get_random_ds(amount: int = 1): """ Retrieve random dataset(s). @@ -46,14 +49,17 @@ def get_random_ds(amount: int = 1): flask.Response: json structure for the dataset(s) """ - results = list(flask.g.db['datasets'].aggregate([{'$sample': {'size': amount}}, - {'$project': {'_id': 1}}])) + results = list( + flask.g.db["datasets"].aggregate( + [{"$sample": {"size": amount}}, {"$project": {"_id": 1}}] + ) + ) for i, result in enumerate(results): - results[i] = build_dataset_info(result['_id'].hex) - return utils.response_json({'datasets': results}) + results[i] = build_dataset_info(result["_id"].hex) + return utils.response_json({"datasets": results}) -@blueprint.route('/structure/', methods=['GET']) +@blueprint.route("/structure/", methods=["GET"]) def get_dataset_data_structure(): """ Get an empty dataset entry. @@ -62,11 +68,11 @@ def get_dataset_data_structure(): flask.Response: JSON structure with a list of datasets. """ empty_dataset = structure.dataset() - empty_dataset['_id'] = '' - return utils.response_json({'dataset': empty_dataset}) + empty_dataset["_id"] = "" + return utils.response_json({"dataset": empty_dataset}) -@blueprint.route('//', methods=['GET']) +@blueprint.route("//", methods=["GET"]) def get_dataset(identifier): """ Retrieve the dataset with uuid . @@ -81,9 +87,10 @@ def get_dataset(identifier): result = build_dataset_info(identifier) if not result: return flask.Response(status=404) - return utils.response_json({'dataset': result}) + return utils.response_json({"dataset": result}) -@blueprint.route('/', methods=['POST']) + +@blueprint.route("/", methods=["POST"]) @user.login_required def add_dataset(): # pylint: disable=too-many-branches """ @@ -97,62 +104,64 @@ def add_dataset(): # pylint: disable=too-many-branches indata = flask.json.loads(flask.request.data) except json.decoder.JSONDecodeError: flask.abort(status=400) - if not 'order' in indata: - flask.current_app.logger.debug('Order field missing') + if not "order" in indata: + flask.current_app.logger.debug("Order field missing") flask.abort(status=400) try: - order_uuid = utils.str_to_uuid(indata['order']) + order_uuid = utils.str_to_uuid(indata["order"]) except ValueError: - flask.current_app.logger.debug('Incorrect order UUID (%s)', indata['order']) + flask.current_app.logger.debug("Incorrect order UUID (%s)", indata["order"]) flask.abort(status=400) - order = flask.g.db['orders'].find_one({'_id': order_uuid}) + order = flask.g.db["orders"].find_one({"_id": order_uuid}) if not order: - flask.current_app.logger.debug('Order (%s) not in db', indata['order']) + flask.current_app.logger.debug("Order (%s) not in db", indata["order"]) flask.abort(status=400) - if not (user.has_permission('DATA_MANAGEMENT') or - flask.g.current_user['_id'] in order['editors']): + if not ( + user.has_permission("DATA_MANAGEMENT") + or flask.g.current_user["_id"] in order["editors"] + ): return flask.abort(status=403) - del indata['order'] - - # properties may only be set by users with DATA_MANAGEMENT - if 'properties' in indata: - if not user.has_permission('DATA_MANAGEMENT'): - flask.abort(403) + del indata["order"] # create new dataset dataset = structure.dataset() - validation = utils.basic_check_indata(indata, dataset, ['_id']) + validation = utils.basic_check_indata(indata, dataset, ["_id"]) if not validation.result: flask.abort(status=validation.status) dataset.update(indata) + dataset["description"] = utils.secure_description(dataset["description"]) + # add to db - result_ds = flask.g.db['datasets'].insert_one(dataset) + result_ds = flask.g.db["datasets"].insert_one(dataset) if not result_ds.acknowledged: - flask.current_app.logger.error('Dataset insert failed: %s', dataset) + flask.current_app.logger.error("Dataset insert failed: %s", dataset) else: - utils.make_log('dataset', - 'add', - f'Dataset added for order {order_uuid}', - dataset) + utils.make_log( + "dataset", "add", f"Dataset added for order {order_uuid}", dataset + ) - result_o = flask.g.db['orders'].update_one({'_id': order_uuid}, - {'$push': {'datasets': dataset['_id']}}) + result_o = flask.g.db["orders"].update_one( + {"_id": order_uuid}, {"$push": {"datasets": dataset["_id"]}} + ) if not result_o.acknowledged: - flask.current_app.logger.error('Order %s insert failed: ADD dataset %s', - order_uuid, dataset['_id']) + flask.current_app.logger.error( + "Order %s insert failed: ADD dataset %s", order_uuid, dataset["_id"] + ) else: - order = flask.g.db['orders'].find_one({'_id': order_uuid}) + order = flask.g.db["orders"].find_one({"_id": order_uuid}) - utils.make_log('order', - 'edit', - f'Dataset {result_ds.inserted_id} added for order', - order) + utils.make_log( + "order", + "edit", + f"Dataset {result_ds.inserted_id} added for order", + order, + ) - return utils.response_json({'_id': result_ds.inserted_id}) + return utils.response_json({"_id": result_ds.inserted_id}) -@blueprint.route('//', methods=['DELETE']) +@blueprint.route("//", methods=["DELETE"]) @user.login_required def delete_dataset(identifier: str): """ @@ -167,46 +176,52 @@ def delete_dataset(identifier: str): ds_uuid = utils.str_to_uuid(identifier) except ValueError: return flask.abort(status=404) - dataset = flask.g.db['datasets'].find_one({'_id': ds_uuid}) + dataset = flask.g.db["datasets"].find_one({"_id": ds_uuid}) if not dataset: flask.abort(status=404) # permission check - order = flask.g.db['orders'].find_one({'datasets': ds_uuid}) - if not user.has_permission('DATA_MANAGEMENT') and \ - flask.g.current_user['_id'] not in order['editors']: + order = flask.g.db["orders"].find_one({"datasets": ds_uuid}) + if ( + not user.has_permission("DATA_MANAGEMENT") + and flask.g.current_user["_id"] not in order["editors"] + ): flask.abort(status=403) - result = flask.g.db['datasets'].delete_one({'_id': ds_uuid}) + result = flask.g.db["datasets"].delete_one({"_id": ds_uuid}) if not result.acknowledged: - flask.current_app.logger.error('Failed to delete dataset %s', ds_uuid) + flask.current_app.logger.error("Failed to delete dataset %s", ds_uuid) return flask.Response(status=500) - utils.make_log('dataset', 'delete', 'Deleted dataset', data={'_id': ds_uuid}) + utils.make_log("dataset", "delete", "Deleted dataset", data={"_id": ds_uuid}) - for entry in flask.g.db['orders'].find({'datasets': ds_uuid}): - result = flask.g.db['orders'].update_one({'_id': entry['_id']}, - {'$pull': {'datasets': ds_uuid}}) + for entry in flask.g.db["orders"].find({"datasets": ds_uuid}): + result = flask.g.db["orders"].update_one( + {"_id": entry["_id"]}, {"$pull": {"datasets": ds_uuid}} + ) if not result.acknowledged: - flask.current_app.logger.error('Failed to delete dataset %s in order %s', - ds_uuid, entry['_id']) + flask.current_app.logger.error( + "Failed to delete dataset %s in order %s", ds_uuid, entry["_id"] + ) return flask.Response(status=500) - new_data = flask.g.db['orders'].find_one({'_id': entry['_id']}) - utils.make_log('order', 'edit', f'Deleted dataset {ds_uuid}', new_data) + new_data = flask.g.db["orders"].find_one({"_id": entry["_id"]}) + utils.make_log("order", "edit", f"Deleted dataset {ds_uuid}", new_data) - for entry in flask.g.db['collections'].find({'datasets': ds_uuid}): - flask.g.db['collections'].update_one({'_id': entry['_id']}, - {'$pull': {'datasets': ds_uuid}}) + for entry in flask.g.db["collections"].find({"datasets": ds_uuid}): + flask.g.db["collections"].update_one( + {"_id": entry["_id"]}, {"$pull": {"datasets": ds_uuid}} + ) if not result.acknowledged: - flask.current_app.logger.error('Failed to delete dataset %s in project %s', - ds_uuid, entry['_id']) + flask.current_app.logger.error( + "Failed to delete dataset %s in project %s", ds_uuid, entry["_id"] + ) return flask.Response(status=500) - new_data = flask.g.db['collections'].find_one({'_id': entry['_id']}) - utils.make_log('collection', 'edit', f'Deleted dataset {ds_uuid}', new_data) + new_data = flask.g.db["collections"].find_one({"_id": entry["_id"]}) + utils.make_log("collection", "edit", f"Deleted dataset {ds_uuid}", new_data) return flask.Response(status=200) -@blueprint.route('//', methods=['PATCH']) +@blueprint.route("//", methods=["PATCH"]) @user.login_required def update_dataset(identifier): """ @@ -217,33 +232,32 @@ def update_dataset(identifier): Returns: flask.Response: success: 200, failure: 400 - """ try: ds_uuid = utils.str_to_uuid(identifier) except ValueError: return flask.abort(status=404) - dataset = flask.g.db['datasets'].find_one({'_id': ds_uuid}) + dataset = flask.g.db["datasets"].find_one({"_id": ds_uuid}) if not dataset: flask.abort(status=404) # permissions - order = flask.g.db['orders'].find_one({'datasets': ds_uuid}) - if not user.has_permission('DATA_MANAGEMENT') and \ - flask.g.current_user['_id'] not in order['editors']: + order = flask.g.db["orders"].find_one({"datasets": ds_uuid}) + if ( + not user.has_permission("DATA_MANAGEMENT") + and flask.g.current_user["_id"] not in order["editors"] + ): flask.abort(status=403) try: indata = flask.json.loads(flask.request.data) except json.decoder.JSONDecodeError: flask.abort(status=400) - validation = utils.basic_check_indata(indata, dataset, prohibited=('_id')) + validation = utils.basic_check_indata(indata, dataset, prohibited=("_id")) if not validation[0]: flask.abort(status=validation[1]) - # properties may only be set by users with DATA_MANAGEMENT - if 'properties' in indata: - if not user.has_permission('DATA_MANAGEMENT'): - flask.abort(403) + if "description" in indata: + indata["description"] = utils.secure_description(indata["description"]) is_different = False for field in indata: @@ -252,18 +266,20 @@ def update_dataset(identifier): break if is_different: - result = flask.g.db['datasets'].update_one({'_id': dataset['_id']}, {'$set': indata}) + result = flask.g.db["datasets"].update_one( + {"_id": dataset["_id"]}, {"$set": indata} + ) if not result.acknowledged: - flask.current_app.logger.error('Dataset update failed: %s', dataset) + flask.current_app.logger.error("Dataset update failed: %s", dataset) flask.abort(status=500) else: dataset.update(indata) - utils.make_log('dataset', 'edit', 'Dataset updated', dataset) + utils.make_log("dataset", "edit", "Dataset updated", dataset) return flask.Response(status=200) -@blueprint.route('//log/', methods=['GET']) +@blueprint.route("//log/", methods=["GET"]) @user.login_required def get_dataset_log(identifier: str = None): """ @@ -282,22 +298,24 @@ def get_dataset_log(identifier: str = None): except ValueError: flask.abort(status=404) - if not user.has_permission('DATA_MANAGEMENT'): - order_data = flask.g.db['orders'].find_one({'datasets': dataset_uuid}) + if not user.has_permission("DATA_MANAGEMENT"): + order_data = flask.g.db["orders"].find_one({"datasets": dataset_uuid}) if not order_data: flask.abort(403) - if flask.g.current_user['_id'] not in order_data['editors']: + if flask.g.current_user["_id"] not in order_data["editors"]: flask.abort(403) - dataset_logs = list(flask.g.db['logs'].find({'data_type': 'dataset', 'data._id': dataset_uuid})) + dataset_logs = list( + flask.g.db["logs"].find({"data_type": "dataset", "data._id": dataset_uuid}) + ) for log in dataset_logs: - del log['data_type'] + del log["data_type"] utils.incremental_logs(dataset_logs) - return utils.response_json({'entry_id': dataset_uuid, - 'data_type': 'dataset', - 'logs': dataset_logs}) + return utils.response_json( + {"entry_id": dataset_uuid, "data_type": "dataset", "logs": dataset_logs} + ) # helper functions @@ -315,26 +333,33 @@ def build_dataset_info(identifier: str): dataset_uuid = utils.str_to_uuid(identifier) except ValueError: return None - dataset = flask.g.db['datasets'].find_one({'_id': dataset_uuid}) + dataset = flask.g.db["datasets"].find_one({"_id": dataset_uuid}) if not dataset: return None - order = flask.g.db['orders'].find_one({'datasets': dataset_uuid}) - - if (user.has_permission('DATA_MANAGEMENT') or\ - flask.g.db.current_user['id'] in order['editors']): - dataset['order'] = order['_id'] - dataset['related'] = list(flask.g.db['datasets'].find({'_id': {'$in': order['datasets']}}, - {'title': 1})) - dataset['related'].remove({'_id': dataset['_id'], 'title': dataset['title']}) - dataset['collections'] = list(flask.g.db['projects'].find({'datasets': dataset_uuid}, - {'title': 1})) - for field in ('editors', 'generators', 'authors'): - if field == 'editors' and\ - (not user.has_permission('DATA_MANAGEMENT') and\ - flask.g.db.current_user['id'] not in order[field]): + order = flask.g.db["orders"].find_one({"datasets": dataset_uuid}) + + if ( + user.has_permission("DATA_MANAGEMENT") + or flask.g.db.current_user["id"] in order["editors"] + ): + dataset["order"] = order["_id"] + dataset["related"] = list( + flask.g.db["datasets"].find({"_id": {"$in": order["datasets"]}}, {"title": 1}) + ) + dataset["related"].remove({"_id": dataset["_id"], "title": dataset["title"]}) + dataset["collections"] = list( + flask.g.db["projects"].find({"datasets": dataset_uuid}, {"title": 1}) + ) + for field in ("editors", "generators", "authors"): + if field == "editors" and ( + not user.has_permission("DATA_MANAGEMENT") + and flask.g.db.current_user["id"] not in order[field] + ): continue dataset[field] = utils.user_uuid_data(order[field], flask.g.db) - dataset['organisation'] = utils.user_uuid_data(order[field], flask.g.db) - dataset['organisation'] = dataset['organisation'][0] if dataset['organisation'] else '' + dataset["organisation"] = utils.user_uuid_data(order[field], flask.g.db) + dataset["organisation"] = ( + dataset["organisation"][0] if dataset["organisation"] else "" + ) return dataset diff --git a/backend/db_management.py b/backend/db_management.py index d9efc1e1..5d5c1a6b 100644 --- a/backend/db_management.py +++ b/backend/db_management.py @@ -8,6 +8,7 @@ DB_VERSION = 1 + def check_db(config: dict): """ Perform database checks. @@ -19,7 +20,7 @@ def check_db(config: dict): config (dict): Configuration for the data tracker """ db = utils.get_db(utils.get_dbclient(config), config) - db_initialised = db['db_status'].find_one({'_id': 'init_db'}) + db_initialised = db["db_status"].find_one({"_id": "init_db"}) if not db_initialised: init_db(db) else: @@ -33,18 +34,14 @@ def init_db(db): - create a default user - set current db_version """ - db['db_status'].insert_one({'_id': 'init_db', - 'started': True, - 'user_added': False, - 'finished': False}) + db["db_status"].insert_one( + {"_id": "init_db", "started": True, "user_added": False, "finished": False} + ) add_default_user(db) # Set DB version - db['db_status'].insert_one({'_id': 'db_version', - 'version': DB_VERSION}) - db['db_status'].update_one({'_id': 'init_db'}, - {'$set': {'finished': True}}) - + db["db_status"].insert_one({"_id": "db_version", "version": DB_VERSION}) + db["db_status"].update_one({"_id": "init_db"}, {"$set": {"finished": True}}) def add_default_user(db): @@ -62,21 +59,26 @@ def add_default_user(db): Api_key: 1234 Auth_id: default::default """ - logging.info('Attempting to add default user') + logging.info("Attempting to add default user") new_user = structure.user() - api_salt = 'fedcba09' - new_user.update({'name': 'Default User', - 'email': 'default_user@example.com', - 'permissions': ['USER_MANAGEMENT'], - 'api_key': utils.gen_api_key_hash('1234', api_salt), - 'api_salt': api_salt, - 'auth_ids': ['default::default']}) + api_salt = "fedcba09" + new_user.update( + { + "name": "Default User", + "email": "default_user@example.com", + "permissions": ["USER_MANAGEMENT"], + "api_key": utils.gen_api_key_hash("1234", api_salt), + "api_salt": api_salt, + "auth_ids": ["default::default"], + } + ) result = db.users.insert_one(new_user) - print(result) - db['db_status'].update_one({'_id': 'init_db'}, - {'$set': {'user_added': True}}) - logging.info('Default user added') + db["db_status"].update_one({"_id": "init_db"}, {"$set": {"user_added": True}}) + if result.acknowledged: + logging.info("Default user added") + else: + logging.error("Failed to add default user") def check_migrations(db): @@ -86,13 +88,13 @@ def check_migrations(db): Args: config (dict): Configuration for the data tracker """ - db_version = db['db_status'].find_one({'_id': 'db_version'}) - if db_version['version'] > DB_VERSION: - logging.critical('The database is newer than the software') + db_version = db["db_status"].find_one({"_id": "db_version"}) + if db_version["version"] > DB_VERSION: + logging.critical("The database is newer than the software") sys.exit(1) - elif db_version['version'] == DB_VERSION: - logging.info('The database is up-to-date') + elif db_version["version"] == DB_VERSION: + logging.info("The database is up-to-date") - for i in range(db_version['version'], DB_VERSION): - logging.info('Database migration for version %d to %d starting', i, i+1) + for i in range(db_version["version"], DB_VERSION): + logging.info("Database migration for version %d to %d starting", i, i + 1) MIGRATIONS[i](db) diff --git a/backend/developer.py b/backend/developer.py index 86014873..3c9d1792 100644 --- a/backend/developer.py +++ b/backend/developer.py @@ -5,10 +5,10 @@ import user -blueprint = flask.Blueprint('developer', __name__) # pylint: disable=invalid-name +blueprint = flask.Blueprint("developer", __name__) # pylint: disable=invalid-name -@blueprint.route('/login/') +@blueprint.route("/login/") def login(identifier: str): """ Log in without password. @@ -19,25 +19,24 @@ def login(identifier: str): res = user.do_login(auth_id=identifier) if res: response = flask.Response(status=200) - response.set_cookie('loggedIn', 'true') return response return flask.Response(status=500) -@blueprint.route('/hello') +@blueprint.route("/hello") def api_hello(): """Test request.""" - return flask.jsonify({'test': 'success'}) + return flask.jsonify({"test": "success"}) -@blueprint.route('/loginhello') +@blueprint.route("/loginhello") @user.login_required def login_hello(): """Test request requiring login.""" - return flask.jsonify({'test': 'success'}) + return flask.jsonify({"test": "success"}) -@blueprint.route('/hello/') +@blueprint.route("/hello/") def permission_hello(permission: str): """ Test request requiring the given permission. @@ -48,24 +47,25 @@ def permission_hello(permission: str): if not user.has_permission(permission): flask.abort(status=403) - return flask.jsonify({'test': 'success'}) + return flask.jsonify({"test": "success"}) -@blueprint.route('/csrftest', methods=['POST', 'PATCH', 'POST', 'DELETE']) +@blueprint.route("/csrftest", methods=["POST", "PATCH", "POST", "DELETE"]) def csrf_test(): """Test csrf tokens.""" - return flask.jsonify({'test': 'success'}) + return flask.jsonify({"test": "success"}) -@blueprint.route('/test_datasets') +@blueprint.route("/test_datasets") def get_added_ds(): """Get datasets added during testing.""" - added = list(flask.g.db['datasets'].find({'description': 'Test dataset'}, - {'_id': 1})) - return flask.jsonify({'datasets': added}) + added = list( + flask.g.db["datasets"].find({"description": "Test dataset"}, {"_id": 1}) + ) + return flask.jsonify({"datasets": added}) -@blueprint.route('/session') +@blueprint.route("/session") def list_session(): """List all session variables.""" session = copy.deepcopy(flask.session) @@ -74,7 +74,7 @@ def list_session(): return flask.jsonify(session) -@blueprint.route('/user/me') +@blueprint.route("/user/me") def list_current_user(): """List all session variables.""" current_user = flask.g.current_user @@ -83,7 +83,7 @@ def list_current_user(): return flask.jsonify(current_user) -@blueprint.route('/config') +@blueprint.route("/config") def list_config(): """List all session variables.""" config = copy.deepcopy(flask.current_app.config) @@ -92,8 +92,8 @@ def list_config(): return flask.jsonify(config) -@blueprint.route('/quit') +@blueprint.route("/quit") def stop_server(): """Shutdown the flask server.""" - flask.request.environ.get('werkzeug.server.shutdown')() + flask.request.environ.get("werkzeug.server.shutdown")() return flask.Response(status=200) diff --git a/backend/exceptions.py b/backend/exceptions.py index abaa751b..d8b7a4a6 100644 --- a/backend/exceptions.py +++ b/backend/exceptions.py @@ -1,4 +1,5 @@ """Custom exception definitions.""" + class AuthError(Exception): """Raised if a permission check fails.""" diff --git a/backend/order.py b/backend/order.py index 09cdab24..01e2ecee 100644 --- a/backend/order.py +++ b/backend/order.py @@ -14,7 +14,7 @@ import user import utils -blueprint = flask.Blueprint('order', __name__) # pylint: disable=invalid-name +blueprint = flask.Blueprint("order", __name__) # pylint: disable=invalid-name @blueprint.before_request @@ -26,11 +26,11 @@ def prepare(): """ if not flask.g.current_user: flask.abort(status=401) - if not user.has_permission('ORDERS'): + if not user.has_permission("ORDERS"): flask.abort(status=403) -@blueprint.route('/', methods=['GET']) +@blueprint.route("/", methods=["GET"]) def list_orders(): """ List all orders visible to the current user. @@ -38,21 +38,24 @@ def list_orders(): Returns: flask.Response: JSON structure with a list of orders. """ - if user.has_permission('DATA_MANAGEMENT'): - orders = list(flask.g.db['orders'].find(projection={'_id': 1, - 'title': 1, - 'tags': 1, - 'properties': 1})) + if user.has_permission("DATA_MANAGEMENT"): + orders = list( + flask.g.db["orders"].find( + projection={"_id": 1, "title": 1, "tags": 1, "properties": 1} + ) + ) else: - orders = list(flask.g.db['orders'] - .find({'editors': flask.g.current_user['_id']}, - projection={'_id': 1, - 'title': 1})) + orders = list( + flask.g.db["orders"].find( + {"editors": flask.g.current_user["_id"]}, + projection={"_id": 1, "title": 1}, + ) + ) - return utils.response_json({'orders': orders}) + return utils.response_json({"orders": orders}) -@blueprint.route('/structure/', methods=['GET']) +@blueprint.route("/structure/", methods=["GET"]) def get_order_data_structure(): """ Get an empty order entry. @@ -61,12 +64,12 @@ def get_order_data_structure(): flask.Response: JSON structure with a list of orders. """ empty_order = structure.order() - empty_order['_id'] = '' - return utils.response_json({'order': empty_order}) + empty_order["_id"] = "" + return utils.response_json({"order": empty_order}) -@blueprint.route('/user/', defaults={'user_id': None}, methods=['GET']) -@blueprint.route('/user//', methods=['GET']) +@blueprint.route("/user/", defaults={"user_id": None}, methods=["GET"]) +@blueprint.route("/user//", methods=["GET"]) def list_orders_user(user_id: str): """ List all orders belonging to the provided user. @@ -78,24 +81,26 @@ def list_orders_user(user_id: str): flask.Response: Json structure with a list of orders. """ if user_id: - if not user.has_permission('OWNERS_READ'): + if not user.has_permission("OWNERS_READ"): flask.abort(status=403) try: user_uuid = utils.str_to_uuid(user_id) except ValueError: return flask.abort(status=404) - if not flask.g.db['users'].find_one({'_id': user_uuid}): + if not flask.g.db["users"].find_one({"_id": user_uuid}): return flask.abort(status=404) else: # current user - user_uuid = flask.session['user_id'] - orders = list(flask.g.db['orders'].find({'editors': user_uuid}, - projection={'_id': 1, - 'title': 1})) + user_uuid = flask.session["user_id"] + orders = list( + flask.g.db["orders"].find( + {"editors": user_uuid}, projection={"_id": 1, "title": 1} + ) + ) - return utils.response_json({'orders': orders}) + return utils.response_json({"orders": orders}) -@blueprint.route('//', methods=['GET']) +@blueprint.route("//", methods=["GET"]) def get_order(identifier): """ Retrieve the order with the provided uuid. @@ -112,19 +117,21 @@ def get_order(identifier): uuid = utils.str_to_uuid(identifier) except ValueError: flask.abort(status=404) - order_data = flask.g.db['orders'].find_one({'_id': uuid}) + order_data = flask.g.db["orders"].find_one({"_id": uuid}) if not order_data: flask.abort(status=404) - if not (user.has_permission('DATA_MANAGEMENT') or - flask.session['user_id'] in order_data['editors']): + if not ( + user.has_permission("DATA_MANAGEMENT") + or flask.session["user_id"] in order_data["editors"] + ): flask.abort(status=403) prepare_order_response(order_data, flask.g.db) - return utils.response_json({'order': order_data}) + return utils.response_json({"order": order_data}) -@blueprint.route('//log/', methods=['GET']) +@blueprint.route("//log/", methods=["GET"]) def get_order_logs(identifier): """ List changes to the dataset. @@ -144,23 +151,25 @@ def get_order_logs(identifier): except ValueError: flask.abort(status=404) - if not user.has_permission('DATA_MANAGEMENT'): - order_data = flask.g.db['orders'].find_one({'_id': order_uuid}) - if not order_data or flask.g.current_user['_id'] not in order_data['editors']: + if not user.has_permission("DATA_MANAGEMENT"): + order_data = flask.g.db["orders"].find_one({"_id": order_uuid}) + if not order_data or flask.g.current_user["_id"] not in order_data["editors"]: flask.abort(403) - order_logs = list(flask.g.db['logs'].find({'data_type': 'order', 'data._id': order_uuid})) + order_logs = list( + flask.g.db["logs"].find({"data_type": "order", "data._id": order_uuid}) + ) for log in order_logs: - del log['data_type'] + del log["data_type"] utils.incremental_logs(order_logs) - return utils.response_json({'entry_id': order_uuid, - 'data_type': 'order', - 'logs': order_logs}) + return utils.response_json( + {"entry_id": order_uuid, "data_type": "order", "logs": order_logs} + ) -@blueprint.route('/base/', methods=['GET']) +@blueprint.route("/base/", methods=["GET"]) def get_empty_order(): """ Provide the basic data structure for an empty order. @@ -170,12 +179,12 @@ def get_empty_order(): """ # create new order order = structure.order() - order['_id'] = '' + order["_id"] = "" - return utils.response_json({'order': order}) + return utils.response_json({"order": order}) -@blueprint.route('/', methods=['POST']) +@blueprint.route("/", methods=["POST"]) def add_order(): """ Add an order. @@ -188,42 +197,38 @@ def add_order(): try: indata = flask.json.loads(flask.request.data) except json.decoder.JSONDecodeError: - flask.current_app.logger.debug('Bad json') + flask.current_app.logger.debug("Bad json") flask.abort(status=400) - validation = utils.basic_check_indata(indata, new_order, ['_id', 'datasets']) + validation = utils.basic_check_indata(indata, new_order, ["_id", "datasets"]) if not validation.result: flask.abort(status=validation.status) - # properties may only be set by users with DATA_MANAGEMENT - if 'properties' in indata: - if not user.has_permission('DATA_MANAGEMENT'): - flask.abort(403) - # convert all incoming uuids to uuid.UUID - for field in ('editors', 'authors', 'generators'): + for field in ("editors", "authors", "generators"): if field in indata: indata[field] = [utils.str_to_uuid(entry) for entry in indata[field]] - if 'organisation' in indata: - if indata['organisation']: - indata['organisation'] = utils.str_to_uuid(indata['organisation']) + if "organisation" in indata: + if indata["organisation"]: + indata["organisation"] = utils.str_to_uuid(indata["organisation"]) new_order.update(indata) + new_order["description"] = utils.secure_description(new_order["description"]) - if not new_order['editors']: - new_order['editors'].append(flask.g.current_user['_id']) + if not new_order["editors"]: + new_order["editors"].append(flask.g.current_user["_id"]) # add to db - result = flask.g.db['orders'].insert_one(new_order) + result = flask.g.db["orders"].insert_one(new_order) if not result.acknowledged: - flask.current_app.logger.error('Order insert failed: %s', new_order) + flask.current_app.logger.error("Order insert failed: %s", new_order) else: - utils.make_log('order', 'add', 'Order added', new_order) + utils.make_log("order", "add", "Order added", new_order) - return utils.response_json({'_id': result.inserted_id}) + return utils.response_json({"_id": result.inserted_id}) -@blueprint.route('//', methods=['DELETE']) +@blueprint.route("//", methods=["DELETE"]) def delete_order(identifier: str): """ Delete the order with the given identifier. @@ -235,32 +240,37 @@ def delete_order(identifier: str): order_uuid = utils.str_to_uuid(identifier) except ValueError: flask.abort(status=404) - order = flask.g.db['orders'].find_one({'_id': order_uuid}) + order = flask.g.db["orders"].find_one({"_id": order_uuid}) if not order: flask.abort(status=404) - if not user.has_permission('DATA_MANAGEMENT') and \ - flask.g.current_user['_id'] not in order['editors']: + if ( + not user.has_permission("DATA_MANAGEMENT") + and flask.g.current_user["_id"] not in order["editors"] + ): flask.abort(status=403) - for dataset_uuid in order['datasets']: - result = flask.g.db['datasets'].delete_one({'_id': dataset_uuid}) + for dataset_uuid in order["datasets"]: + result = flask.g.db["datasets"].delete_one({"_id": dataset_uuid}) if not result.acknowledged: - flask.current_app.logger.error('Dataset %s delete failed (order %s deletion):', - dataset_uuid, order_uuid) + flask.current_app.logger.error( + "Dataset %s delete failed (order %s deletion):", + dataset_uuid, + order_uuid, + ) flask.abort(status=500) else: - utils.make_log('dataset', 'delete', 'Deleting order', {'_id': dataset_uuid}) - result = flask.g.db['orders'].delete_one(order) + utils.make_log("dataset", "delete", "Deleting order", {"_id": dataset_uuid}) + result = flask.g.db["orders"].delete_one(order) if not result.acknowledged: - flask.current_app.logger.error('Order deletion failed: %s', order_uuid) + flask.current_app.logger.error("Order deletion failed: %s", order_uuid) flask.abort(status=500) else: - utils.make_log('order', 'delete', 'Order deleted', {'_id': order_uuid}) + utils.make_log("order", "delete", "Order deleted", {"_id": order_uuid}) return flask.Response(status=200) -@blueprint.route('//', methods=['PATCH']) +@blueprint.route("//", methods=["PATCH"]) def update_order(identifier: str): # pylint: disable=too-many-branches """ Update an existing order. @@ -276,32 +286,32 @@ def update_order(identifier: str): # pylint: disable=too-many-branches except ValueError: return flask.abort(status=404) - order = flask.g.db['orders'].find_one({'_id': order_uuid}) + order = flask.g.db["orders"].find_one({"_id": order_uuid}) if not order: return flask.abort(status=404) - if not (user.has_permission('DATA_MANAGEMENT') or - flask.g.current_user['_id'] in order['editors']): + if not ( + user.has_permission("DATA_MANAGEMENT") + or flask.g.current_user["_id"] in order["editors"] + ): return flask.abort(status=403) try: indata = flask.json.loads(flask.request.data) except json.decoder.JSONDecodeError: flask.abort(status=400) - validation = utils.basic_check_indata(indata, order, ['_id', 'datasets']) + validation = utils.basic_check_indata(indata, order, ["_id", "datasets"]) if not validation.result: flask.abort(status=validation.status) - # properties may only be set by users with DATA_MANAGEMENT - if 'properties' in indata: - if not user.has_permission('DATA_MANAGEMENT'): - flask.abort(403) - - for field in ('editors', 'authors', 'generators'): + for field in ("editors", "authors", "generators"): if field in indata: indata[field] = [utils.str_to_uuid(entry) for entry in indata[field]] - if 'organisation' in indata: - if indata['organisation']: - indata['organisation'] = utils.str_to_uuid(indata['organisation']) + if "organisation" in indata: + if indata["organisation"]: + indata["organisation"] = utils.str_to_uuid(indata["organisation"]) + + if "description" in indata: + indata["description"] = utils.secure_description(indata["description"]) is_different = False for field in indata: @@ -311,15 +321,15 @@ def update_order(identifier: str): # pylint: disable=too-many-branches order.update(indata) - if not order['editors']: - order['editors'] = [flask.g.current_user['_id']] + if not order["editors"]: + order["editors"] = [flask.g.current_user["_id"]] if is_different: - result = flask.g.db['orders'].update_one({'_id': order['_id']}, {'$set': order}) + result = flask.g.db["orders"].update_one({"_id": order["_id"]}, {"$set": order}) if not result.acknowledged: - flask.current_app.logger.error('Order update failed: %s', order) + flask.current_app.logger.error("Order update failed: %s", order) else: - utils.make_log('order', 'edit', 'Order updated', order) + utils.make_log("order", "edit", "Order updated", order) return flask.Response(status=200) @@ -334,18 +344,22 @@ def prepare_order_response(order_data: dict, mongodb): order_data (dict): The order entry from the db. mongodb: The mongo database to use. """ - order_data['authors'] = utils.user_uuid_data(order_data['authors'], mongodb) - order_data['generators'] = utils.user_uuid_data(order_data['generators'], mongodb) - order_data['editors'] = utils.user_uuid_data(order_data['editors'], mongodb) - if order_data['organisation']: - if org_entry := utils.user_uuid_data(order_data['organisation'], mongodb): - order_data['organisation'] = org_entry[0] + order_data["authors"] = utils.user_uuid_data(order_data["authors"], mongodb) + order_data["generators"] = utils.user_uuid_data(order_data["generators"], mongodb) + order_data["editors"] = utils.user_uuid_data(order_data["editors"], mongodb) + if order_data["organisation"]: + if org_entry := utils.user_uuid_data(order_data["organisation"], mongodb): + order_data["organisation"] = org_entry[0] else: - flask.current_app.logger.error('Reference to non-existing organisation: %s', - order_data['organisation']) + flask.current_app.logger.error( + "Reference to non-existing organisation: %s", order_data["organisation"] + ) else: - order_data['organisation'] = {} + order_data["organisation"] = {} # convert dataset list into {title, _id} - order_data['datasets'] = list(mongodb['datasets'].find({'_id': {'$in': order_data['datasets']}}, - {'_id': 1, 'title': 1})) + order_data["datasets"] = list( + mongodb["datasets"].find( + {"_id": {"$in": order_data["datasets"]}}, {"_id": 1, "title": 1} + ) + ) diff --git a/backend/requirements.txt b/backend/requirements.txt index 36e00ab5..b89f256f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,10 +1,11 @@ Click==7.1.2 Flask==1.1.2 itsdangerous==1.1.0 -Jinja2==2.11.2 +Jinja2==2.11.3 MarkupSafe==1.1.1 pymongo==3.11.0 -PyYAML==5.3.1 +PyYAML==5.4 Werkzeug==1.0.1 authlib==0.14.3 requests==2.24.0 +argon2-cffi==20.1.0 diff --git a/backend/structure.py b/backend/structure.py index f7db25e0..240dc154 100644 --- a/backend/structure.py +++ b/backend/structure.py @@ -14,12 +14,14 @@ def dataset(): Returns: dict: The data structure for a dataset. """ - return {'_id': utils.new_uuid(), - 'description': '', - 'cross_references': [], - 'title': '', - 'properties': {}, - 'tags': []} + return { + "_id": utils.new_uuid(), + "description": "", + "cross_references": [], + "title": "", + "properties": {}, + "tags": [], + } def order(): @@ -29,16 +31,18 @@ def order(): Returns: dict: The data structure for an order. """ - return {'_id': utils.new_uuid(), - 'title': '', - 'description': '', - 'authors': [], - 'generators': [], - 'organisation': '', - 'editors': [], - 'datasets': [], - 'properties': {}, - 'tags': []} + return { + "_id": utils.new_uuid(), + "title": "", + "description": "", + "authors": [], + "generators": [], + "organisation": "", + "editors": [], + "datasets": [], + "properties": {}, + "tags": [], + } def collection(): @@ -48,14 +52,16 @@ def collection(): Returns: dict: The data structure for a project. """ - return {'_id': utils.new_uuid(), - 'cross_references': [], - 'datasets': [], - 'description': '', - 'properties': {}, - 'tags': [], - 'editors': [], - 'title': ''} + return { + "_id": utils.new_uuid(), + "cross_references": [], + "datasets": [], + "description": "", + "properties": {}, + "tags": [], + "editors": [], + "title": "", + } def user(): @@ -65,17 +71,19 @@ def user(): Returns: dict: The data structure for a user. """ - return {'_id': utils.new_uuid(), - 'affiliation': '', - 'api_key': '', - 'api_salt': '', - 'auth_ids': [], - 'email': '', - 'contact': '', - 'name': '', - 'orcid': '', - 'permissions': [], - 'url': ''} + return { + "_id": utils.new_uuid(), + "affiliation": "", + "api_key": "", + "api_salt": "", + "auth_ids": [], + "email": "", + "contact": "", + "name": "", + "orcid": "", + "permissions": [], + "url": "", + } def log(): @@ -85,10 +93,12 @@ def log(): Returns: dict: The data structure for a log. """ - return {'_id': utils.new_uuid(), - 'action': '', - 'comment': '', - 'data_type': '', - 'data': '', - 'timestamp': utils.make_timestamp(), - 'user': ''} + return { + "_id": utils.new_uuid(), + "action": "", + "comment": "", + "data_type": "", + "data": "", + "timestamp": utils.make_timestamp(), + "user": "", + } diff --git a/backend/tests/helpers.py b/backend/tests/helpers.py index 603db764..1bb71f0c 100644 --- a/backend/tests/helpers.py +++ b/backend/tests/helpers.py @@ -17,26 +17,28 @@ CURR_DIR = os.path.realpath(__file__) -SETTINGS = json.loads(open(f'{os.path.dirname(CURR_DIR)}/settings_tests.json').read()) +SETTINGS = json.loads(open(f"{os.path.dirname(CURR_DIR)}/settings_tests.json").read()) BASE_URL = f'{SETTINGS["host"]}:{SETTINGS["port"]}' -TEST_LABEL = {'tags': ['testing']} +TEST_LABEL = {"tags": ["testing"]} -USERS = {'no-login': None, - 'base': 'base::testers', - 'orders': 'orders::testers', - 'owners': 'owners::testers', - 'users': 'users::testers', - 'data': 'data::testers', - 'root': 'root::testers'} +USERS = { + "no-login": None, + "base": "base::testers", + "orders": "orders::testers", + "owners": "owners::testers", + "users": "users::testers", + "data": "data::testers", + "root": "root::testers", +} -Response = collections.namedtuple('Response', - ['data', 'code', 'role'], - defaults=[None, None, None]) +Response = collections.namedtuple( + "Response", ["data", "code", "role"], defaults=[None, None, None] +) -FACILITY_RE = re.compile('facility[0-9]*::local') -ORGANISATION_RE = re.compile('organisation[0-9]*::local') -USER_RE = re.compile('.*::elixir') +FACILITY_RE = re.compile("facility[0-9]*::local") +ORGANISATION_RE = re.compile("organisation[0-9]*::local") +USER_RE = re.compile(".*::elixir") def db_connection(): @@ -66,13 +68,13 @@ def as_user(session: requests.Session, auth_id: str, set_csrf: bool = True) -> i int: Status code. """ if auth_id: - code = session.get(f'{BASE_URL}/api/v1/developer/login/{auth_id}').status_code + code = session.get(f"{BASE_URL}/api/v1/developer/login/{auth_id}").status_code assert code == 200 else: - code = session.get(f'{BASE_URL}/api/v1/logout/').status_code - session.get(f'{BASE_URL}/api/v1/developer/hello') # reset cookies + code = session.get(f"{BASE_URL}/api/v1/logout/").status_code + session.get(f"{BASE_URL}/api/v1/developer/hello") # reset cookies if set_csrf: - session.headers['X-CSRFToken'] = session.cookies.get('_csrf_token') + session.headers["X-CSRFToken"] = session.cookies.get("_csrf_token") return code @@ -100,34 +102,40 @@ def add_dataset(): mongo_db = db_connection() # prepare order_indata = structure.order() - order_indata.update({'description': 'Added by fixture.', - 'title': 'Test title from fixture'}) + order_indata.update( + {"description": "Added by fixture.", "title": "Test title from fixture"} + ) order_indata.update(TEST_LABEL) - orders_user = mongo_db['users'].find_one({'auth_ids': USERS['orders']}) - base_user = mongo_db['users'].find_one({'auth_ids': USERS['base']}) - order_indata['authors'] = [orders_user['_id']] - order_indata['editors'] = [orders_user['_id']] - order_indata['generators'] = [orders_user['_id']] - order_indata['organisation'] = orders_user['_id'] - order_indata['receivers'] = [base_user['_id']] + orders_user = mongo_db["users"].find_one({"auth_ids": USERS["orders"]}) + base_user = mongo_db["users"].find_one({"auth_ids": USERS["base"]}) + order_indata["authors"] = [orders_user["_id"]] + order_indata["editors"] = [orders_user["_id"]] + order_indata["generators"] = [orders_user["_id"]] + order_indata["organisation"] = orders_user["_id"] + order_indata["receivers"] = [base_user["_id"]] dataset_indata = structure.dataset() - dataset_indata.update({'description': 'Added by fixture.', - 'title': 'Test title from fixture'}) + dataset_indata.update( + {"description": "Added by fixture.", "title": "Test title from fixture"} + ) dataset_indata.update(TEST_LABEL) collection_indata = structure.collection() - collection_indata.update({'description': 'Added by fixture.', - 'title': 'Test title from fixture', - 'editors': [base_user['_id']]}) + collection_indata.update( + { + "description": "Added by fixture.", + "title": "Test title from fixture", + "editors": [base_user["_id"]], + } + ) collection_indata.update(TEST_LABEL) - mongo_db['datasets'].insert_one(dataset_indata) - order_indata['datasets'].append(dataset_indata['_id']) - collection_indata['datasets'].append(dataset_indata['_id']) - mongo_db['orders'].insert_one(order_indata) - mongo_db['collections'].insert_one(collection_indata) - return (order_indata['_id'], dataset_indata['_id'], collection_indata['_id']) + mongo_db["datasets"].insert_one(dataset_indata) + order_indata["datasets"].append(dataset_indata["_id"]) + collection_indata["datasets"].append(dataset_indata["_id"]) + mongo_db["orders"].insert_one(order_indata) + mongo_db["collections"].insert_one(collection_indata) + return (order_indata["_id"], dataset_indata["_id"], collection_indata["_id"]) def delete_dataset(order_uuid, dataset_uuid, project_uuid): @@ -135,12 +143,14 @@ def delete_dataset(order_uuid, dataset_uuid, project_uuid): Delete an order and a dataset added by ``add_dataset()``. """ mongo_db = db_connection() - mongo_db['orders'].delete_one({'_id': order_uuid}) - mongo_db['datasets'].delete_one({'_id': dataset_uuid}) - mongo_db['projects'].delete_one({'_id': project_uuid}) + mongo_db["orders"].delete_one({"_id": order_uuid}) + mongo_db["datasets"].delete_one({"_id": dataset_uuid}) + mongo_db["projects"].delete_one({"_id": project_uuid}) -def make_request(session, url: str, data: dict = None, method='GET', ret_json: bool = True) -> dict: +def make_request( + session, url: str, data: dict = None, method="GET", ret_json: bool = True +) -> dict: """ Helper method for using get/post to a url. Args: @@ -153,21 +163,18 @@ def make_request(session, url: str, data: dict = None, method='GET', ret_json: b Returns: tuple: (data: dict, status_code: int) """ - if method == 'GET': - response = session.get(f'{BASE_URL}{url}') - elif method == 'POST': - response = session.post(f'{BASE_URL}{url}', - data=json.dumps(data)) - elif method == 'PATCH': - response = session.patch(f'{BASE_URL}{url}', - data=json.dumps(data)) - elif method == 'PUT': - response = session.put(f'{BASE_URL}{url}', - data=json.dumps(data)) - elif method == 'DELETE': - response = session.delete(f'{BASE_URL}{url}') + if method == "GET": + response = session.get(f"{BASE_URL}{url}") + elif method == "POST": + response = session.post(f"{BASE_URL}{url}", data=json.dumps(data)) + elif method == "PATCH": + response = session.patch(f"{BASE_URL}{url}", data=json.dumps(data)) + elif method == "PUT": + response = session.put(f"{BASE_URL}{url}", data=json.dumps(data)) + elif method == "DELETE": + response = session.delete(f"{BASE_URL}{url}") else: - raise ValueError(f'Unsupported http method ({method})') + raise ValueError(f"Unsupported http method ({method})") if response.text and ret_json: data = json.loads(response.text) @@ -178,8 +185,13 @@ def make_request(session, url: str, data: dict = None, method='GET', ret_json: b return Response(data=data, code=response.status_code) -def make_request_all_roles(url: str, method: str = 'GET', data=None, - set_csrf: bool = True, ret_json: bool = False) -> list: +def make_request_all_roles( + url: str, + method: str = "GET", + data=None, + set_csrf: bool = True, + ret_json: bool = False, +) -> list: """ Perform a query for all roles (anonymous, User, Steward, Admin). @@ -208,18 +220,22 @@ def collection_for_tests(): # prepare mongo_db = db_connection() session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) collection_indata = structure.collection() - base_user = mongo_db['users'].find_one({'auth_ids': USERS['base']}) - collection_indata.update({'description': 'Added by fixture.', - 'title': 'Test title from fixture', - 'editors': [base_user['_id']]}) + base_user = mongo_db["users"].find_one({"auth_ids": USERS["base"]}) + collection_indata.update( + { + "description": "Added by fixture.", + "title": "Test title from fixture", + "editors": [base_user["_id"]], + } + ) collection_indata.update(TEST_LABEL) - mongo_db['collections'].insert_one(collection_indata) + mongo_db["collections"].insert_one(collection_indata) - yield collection_indata['_id'] + yield collection_indata["_id"] - mongo_db['collections'].delete_one({'_id': collection_indata['_id']}) + mongo_db["collections"].delete_one({"_id": collection_indata["_id"]}) def random_string(min_length: int = 1, max_length: int = 150): @@ -234,9 +250,9 @@ def random_string(min_length: int = 1, max_length: int = 150): str: a string of random characters """ - char_source = string.ascii_letters + string.digits + '-' + char_source = string.ascii_letters + string.digits + "-" length = random.randint(min_length, max_length) - return ''.join(random.choice(char_source) for _ in range(length)) + return "".join(random.choice(char_source) for _ in range(length)) def parse_time(datetime_str: str): @@ -246,5 +262,5 @@ def parse_time(datetime_str: str): Args: datetime_str (str): timestamp string (Wed, 22 Jan 2020 21:07:35 GMT) """ - str_format = '%a, %d %b %Y %H:%M:%S %Z' + str_format = "%a, %d %b %Y %H:%M:%S %Z" return datetime.datetime.strptime(datetime_str, str_format) diff --git a/backend/tests/test_collections.py b/backend/tests/test_collections.py index db3a0f77..b512def2 100644 --- a/backend/tests/test_collections.py +++ b/backend/tests/test_collections.py @@ -5,38 +5,54 @@ import utils # pylint: disable=unused-import -from helpers import make_request, as_user, make_request_all_roles,\ - USERS, random_string, mdb, TEST_LABEL, collection_for_tests, add_dataset, delete_dataset +from helpers import ( + make_request, + as_user, + make_request_all_roles, + USERS, + random_string, + mdb, + TEST_LABEL, + collection_for_tests, + add_dataset, + delete_dataset, +) + # pylint: enable=unused-import # pylint: disable=redefined-outer-name + def test_random_collection(): """Request a random collection.""" - responses = make_request_all_roles('/api/v1/collection/random', ret_json=True) + responses = make_request_all_roles("/api/v1/collection/random", ret_json=True) for response in responses: assert response.code == 200 - assert len(response.data['collections']) == 1 + assert len(response.data["collections"]) == 1 def test_random_collections(): """Request random collections.""" session = requests.Session() - as_user(session, USERS['base']) + as_user(session, USERS["base"]) for i in (1, 5, 0): - response = make_request(session, f'/api/v1/collection/random/{i}', ret_json=True) + response = make_request( + session, f"/api/v1/collection/random/{i}", ret_json=True + ) assert response.code == 200 - assert len(response.data['collections']) == i + assert len(response.data["collections"]) == i - response = make_request(session, '/api/v1/collection/random/-1') + response = make_request(session, "/api/v1/collection/random/-1") assert response[1] == 404 assert not response[0] def test_get_collection_permissions(mdb): """Test permissions for requesting a collection.""" - collection = list(mdb['collections'].aggregate([{'$sample': {'size': 1}}]))[0] + collection = list(mdb["collections"].aggregate([{"$sample": {"size": 1}}]))[0] - responses = make_request_all_roles(f'/api/v1/collection/{collection["_id"]}', ret_json=True) + responses = make_request_all_roles( + f'/api/v1/collection/{collection["_id"]}', ret_json=True + ) for response in responses: assert response.code == 200 @@ -49,48 +65,48 @@ def test_get_collection(mdb): """ session = requests.Session() for _ in range(3): - collection = list(mdb['collections'].aggregate([{'$sample': {'size': 1}}]))[0] - collection['_id'] = str(collection['_id']) - proj_owner = mdb['users'].find_one({'_id': {'$in': collection['editors']}}) - collection['editors'] = [str(entry) for entry in collection['editors']] - collection['datasets'] = [str(entry) for entry in collection['datasets']] + collection = list(mdb["collections"].aggregate([{"$sample": {"size": 1}}]))[0] + collection["_id"] = str(collection["_id"]) + proj_owner = mdb["users"].find_one({"_id": {"$in": collection["editors"]}}) + collection["editors"] = [str(entry) for entry in collection["editors"]] + collection["datasets"] = [str(entry) for entry in collection["datasets"]] collection = utils.convert_keys_to_camel(collection) - as_user(session, USERS['base']) + as_user(session, USERS["base"]) response = make_request(session, f'/api/v1/collection/{collection["_id"]}') assert response.code == 200 for field in collection: - if field == 'datasets': + if field == "datasets": for i, ds_uuid in enumerate(collection[field]): - assert ds_uuid == response.data['collection'][field][i]['_id'] - elif field == 'editors': + assert ds_uuid == response.data["collection"][field][i]["_id"] + elif field == "editors": continue else: - assert collection[field] == response.data['collection'][field] + assert collection[field] == response.data["collection"][field] - as_user(session, proj_owner['auth_ids'][0]) + as_user(session, proj_owner["auth_ids"][0]) response = make_request(session, f'/api/v1/collection/{collection["_id"]}') assert response.code == 200 print(collection) for field in collection: - if field in ('datasets', 'editors'): - entries = [entry['_id'] for entry in response.data['collection'][field]] + if field in ("datasets", "editors"): + entries = [entry["_id"] for entry in response.data["collection"][field]] assert len(collection[field]) == len(entries) for i, ds_uuid in enumerate(collection[field]): assert ds_uuid in entries else: - assert collection[field] == response.data['collection'][field] + assert collection[field] == response.data["collection"][field] - as_user(session, USERS['root']) + as_user(session, USERS["root"]) response = make_request(session, f'/api/v1/collection/{collection["_id"]}') assert response.code == 200 for field in collection: - if field in ('datasets', 'editors'): - entries = [entry['_id'] for entry in response.data['collection'][field]] + if field in ("datasets", "editors"): + entries = [entry["_id"] for entry in response.data["collection"][field]] assert len(collection[field]) == len(entries) for i, ds_uuid in enumerate(collection[field]): assert ds_uuid in entries else: - assert collection[field] == response.data['collection'][field] + assert collection[field] == response.data["collection"][field] def test_get_collection_bad(): @@ -101,12 +117,12 @@ def test_get_collection_bad(): """ session = requests.Session() for _ in range(2): - response = make_request(session, f'/api/v1/collection/{uuid.uuid4().hex}') + response = make_request(session, f"/api/v1/collection/{uuid.uuid4().hex}") assert response.code == 404 assert not response.data for _ in range(2): - response = make_request(session, f'/api/v1/collection/{random_string()}') + response = make_request(session, f"/api/v1/collection/{random_string()}") assert response.code == 404 assert not response.data @@ -117,37 +133,35 @@ def test_add_collection_permissions(mdb): Test permissions. """ - indata = {'title': 'Test title'} + indata = {"title": "Test title"} indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 + assert "_id" in response.data + assert len(response.data["_id"]) == 36 - user_info = mdb['users'].find_one({'auth_ids': USERS['base']}) - indata.update({'editors': [str(user_info['_id'])]}) + user_info = mdb["users"].find_one({"auth_ids": USERS["base"]}) + indata.update({"editors": [str(user_info["_id"])]}) - responses = make_request_all_roles('/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 + assert "_id" in response.data + assert len(response.data["_id"]) == 36 def test_add_collection(mdb): @@ -158,62 +172,68 @@ def test_add_collection(mdb): * fields are set correctly * logs are created """ - dataset_info = next(mdb['datasets'].aggregate([{'$sample': {'size': 1}}])) - order_info = mdb['orders'].find_one({'datasets': dataset_info['_id']}) + dataset_info = next(mdb["datasets"].aggregate([{"$sample": {"size": 1}}])) + order_info = mdb["orders"].find_one({"datasets": dataset_info["_id"]}) session = requests.Session() - user_info = mdb['users'].find_one({'_id': {'$in': order_info['editors']}}) + user_info = mdb["users"].find_one({"_id": {"$in": order_info["editors"]}}) - as_user(session, user_info['auth_ids'][0]) + as_user(session, user_info["auth_ids"][0]) - indata = {'description': 'Test description', - 'editors': [str(user_info['_id'])], - 'title': 'Test title', - 'datasets': [str(dataset_info['_id'])]} + indata = { + "description": "Test description", + "editors": [str(user_info["_id"])], + "title": "Test title", + "datasets": [str(dataset_info["_id"])], + } indata.update(TEST_LABEL) - response = make_request(session, - '/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + response = make_request( + session, "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - collection = mdb['collections'].find_one({'_id': uuid.UUID(response.data['_id'])}) - assert collection['description'] == indata['description'] - assert str(collection['editors'][0]) == indata['editors'][0] - assert collection['title'] == indata['title'] - assert str(collection['datasets'][0]) == indata['datasets'][0] + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + collection = mdb["collections"].find_one({"_id": uuid.UUID(response.data["_id"])}) + assert collection["description"] == indata["description"] + assert str(collection["editors"][0]) == indata["editors"][0] + assert collection["title"] == indata["title"] + assert str(collection["datasets"][0]) == indata["datasets"][0] # log - assert mdb['logs'].find_one({'data._id': uuid.UUID(response.data['_id']), - 'data_type': 'collection', - 'user': user_info['_id'], - 'action': 'add'}) - - as_user(session, USERS['data']) - - response = make_request(session, - '/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + assert mdb["logs"].find_one( + { + "data._id": uuid.UUID(response.data["_id"]), + "data_type": "collection", + "user": user_info["_id"], + "action": "add", + } + ) + + as_user(session, USERS["data"]) + + response = make_request( + session, "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - collection = mdb['collections'].find_one({'_id': uuid.UUID(response.data['_id'])}) - assert collection['description'] == indata['description'] - assert str(collection['editors'][0]) == indata['editors'][0] - assert collection['title'] == indata['title'] - assert str(collection['datasets'][0]) == indata['datasets'][0] + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + collection = mdb["collections"].find_one({"_id": uuid.UUID(response.data["_id"])}) + assert collection["description"] == indata["description"] + assert str(collection["editors"][0]) == indata["editors"][0] + assert collection["title"] == indata["title"] + assert str(collection["datasets"][0]) == indata["datasets"][0] - data_user = mdb['users'].find_one({'auth_ids': USERS['data']}) + data_user = mdb["users"].find_one({"auth_ids": USERS["data"]}) # log - assert mdb['logs'].find_one({'data._id': uuid.UUID(response.data['_id']), - 'data_type': 'collection', - 'user': data_user['_id'], - 'action': 'add'}) + assert mdb["logs"].find_one( + { + "data._id": uuid.UUID(response.data["_id"]), + "data_type": "collection", + "user": data_user["_id"], + "action": "add", + } + ) def test_add_collection_bad(): @@ -222,66 +242,61 @@ def test_add_collection_bad(): Bad requests. """ - indata = {'title': ''} + indata = {"title": ""} indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 400 assert not response.data - indata = {} indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 400 assert not response.data - - indata = {'bad_tag': 'content', - 'title': 'title'} + indata = {"bad_tag": "content", "title": "title"} indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 400 assert not response.data - indata = {'description': 'Test description', - 'owners': [str(uuid.uuid4())], - 'title': 'Test title'} + indata = { + "description": "Test description", + "owners": [str(uuid.uuid4())], + "title": "Test title", + } indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -289,27 +304,24 @@ def test_add_collection_bad(): assert not response.data session = requests.Session() - as_user(session, USERS['data']) - indata = {'_id': str(uuid.uuid4()), - 'owners': [str(uuid.uuid4())], - 'title': 'Test title'} + as_user(session, USERS["data"]) + indata = { + "_id": str(uuid.uuid4()), + "owners": [str(uuid.uuid4())], + "title": "Test title", + } indata.update(TEST_LABEL) - response = make_request(session, - '/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + response = make_request( + session, "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) assert response.code == 403 assert not response.data - indata = {'datasets': [str(uuid.uuid4())], - 'title': 'Test title'} + indata = {"datasets": [str(uuid.uuid4())], "title": "Test title"} indata.update(TEST_LABEL) - response = make_request(session, - '/api/v1/collection/', - method='POST', - data=indata, - ret_json=True) + response = make_request( + session, "/api/v1/collection/", method="POST", data=indata, ret_json=True + ) assert response.code == 400 @@ -322,22 +334,24 @@ def test_update_collection_permissions(mdb, collection_for_tests): session = requests.Session() collection_uuid = collection_for_tests - print(mdb['collections'].find_one({'_id': collection_uuid})) + print(mdb["collections"].find_one({"_id": collection_uuid})) for role in USERS: as_user(session, USERS[role]) - indata = {'title': f'Test title - updated by {role}'} - response = make_request(session, - f'/api/v1/collection/{collection_uuid}/', - method='PATCH', - data=indata, - ret_json=True) - if role in ('base', 'data', 'root'): + indata = {"title": f"Test title - updated by {role}"} + response = make_request( + session, + f"/api/v1/collection/{collection_uuid}/", + method="PATCH", + data=indata, + ret_json=True, + ) + if role in ("base", "data", "root"): assert response.code == 200 assert not response.data - new_collection = mdb['collections'].find_one({'_id': collection_uuid}) - assert new_collection['title'] == f'Test title - updated by {role}' - elif role == 'no-login': + new_collection = mdb["collections"].find_one({"_id": collection_uuid}) + assert new_collection["title"] == f"Test title - updated by {role}" + elif role == "no-login": assert response.code == 401 assert not response.data else: @@ -353,62 +367,78 @@ def test_update_collection(mdb): Confirm that logs are created. """ uuids = add_dataset() - collection_info = mdb['collections'].find_one({'_id': uuids[2]}) - user_info = mdb['users'].find_one({'auth_ids': USERS['base']}) - - indata = {'description': 'Test description updated', - 'editors': [str(collection_info['editors'][0])], - 'title': 'Test title updated', - 'datasets': [str(uuids[1])]} + collection_info = mdb["collections"].find_one({"_id": uuids[2]}) + user_info = mdb["users"].find_one({"auth_ids": USERS["base"]}) + + indata = { + "description": "Test description updated", + "editors": [str(collection_info["editors"][0])], + "title": "Test title updated", + "datasets": [str(uuids[1])], + } indata.update(TEST_LABEL) session = requests.Session() - as_user(session, USERS['base']) - - response = make_request(session, - f'/api/v1/collection/{collection_info["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + as_user(session, USERS["base"]) + + response = make_request( + session, + f'/api/v1/collection/{collection_info["_id"]}/', + method="PATCH", + data=indata, + ret_json=True, + ) assert response.code == 200 - collection = mdb['collections'].find_one({'_id': collection_info['_id']}) - assert collection['description'] == indata['description'] - assert str(collection['editors'][0]) == indata['editors'][0] - assert collection['title'] == indata['title'] - assert str(collection['datasets'][0]) == indata['datasets'][0] + collection = mdb["collections"].find_one({"_id": collection_info["_id"]}) + assert collection["description"] == indata["description"] + assert str(collection["editors"][0]) == indata["editors"][0] + assert collection["title"] == indata["title"] + assert str(collection["datasets"][0]) == indata["datasets"][0] # log - assert mdb['logs'].find_one({'data._id': collection_info['_id'], - 'data_type': 'collection', - 'user': user_info['_id'], - 'action': 'edit'}) - - as_user(session, USERS['data']) - user_info = mdb['users'].find_one({'auth_ids': USERS['data']}) - - indata = {'description': 'Test description updated2', - 'editors': [str(user_info['_id'])], - 'title': 'Test title updated', - 'datasets': [str(uuids[1]), str(uuids[1])]} + assert mdb["logs"].find_one( + { + "data._id": collection_info["_id"], + "data_type": "collection", + "user": user_info["_id"], + "action": "edit", + } + ) + + as_user(session, USERS["data"]) + user_info = mdb["users"].find_one({"auth_ids": USERS["data"]}) + + indata = { + "description": "Test description updated2", + "editors": [str(user_info["_id"])], + "title": "Test title updated", + "datasets": [str(uuids[1]), str(uuids[1])], + } indata.update(TEST_LABEL) - response = make_request(session, - f'/api/v1/collection/{collection_info["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + response = make_request( + session, + f'/api/v1/collection/{collection_info["_id"]}/', + method="PATCH", + data=indata, + ret_json=True, + ) assert response.code == 200 - collection = mdb['collections'].find_one({'_id': collection_info['_id']}) - assert collection['description'] == indata['description'] - assert str(collection['editors'][0]) == indata['editors'][0] - assert collection['title'] == indata['title'] - assert str(collection['datasets'][0]) == indata['datasets'][0] + collection = mdb["collections"].find_one({"_id": collection_info["_id"]}) + assert collection["description"] == indata["description"] + assert str(collection["editors"][0]) == indata["editors"][0] + assert collection["title"] == indata["title"] + assert str(collection["datasets"][0]) == indata["datasets"][0] # log - assert mdb['logs'].find_one({'data._id': collection_info['_id'], - 'data_type': 'collection', - 'user': user_info['_id'], - 'action': 'edit'}) + assert mdb["logs"].find_one( + { + "data._id": collection_info["_id"], + "data_type": "collection", + "user": user_info["_id"], + "action": "edit", + } + ) delete_dataset(*uuids) @@ -419,38 +449,44 @@ def test_update_collection_bad(mdb): Bad requests. """ uuids = add_dataset() - collection_info = mdb['collections'].find_one({'_id': uuids[2]}) + collection_info = mdb["collections"].find_one({"_id": uuids[2]}) - indata = {'bad_tag': 'value'} + indata = {"bad_tag": "value"} - responses = make_request_all_roles(f'/api/v1/collection/{collection_info["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + responses = make_request_all_roles( + f'/api/v1/collection/{collection_info["_id"]}/', + method="PATCH", + data=indata, + ret_json=True, + ) for response in responses: - if response.role in ('base', 'data', 'root'): + if response.role in ("base", "data", "root"): assert response.code == 400 assert not response.data - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - indata = {'description': 'Test description', - 'owners': [str(uuid.uuid4())], - 'title': 'Test title'} - - responses = make_request_all_roles(f'/api/v1/collection/{collection_info["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + indata = { + "description": "Test description", + "owners": [str(uuid.uuid4())], + "title": "Test title", + } + + responses = make_request_all_roles( + f'/api/v1/collection/{collection_info["_id"]}/', + method="PATCH", + data=indata, + ret_json=True, + ) for response in responses: - if response.role in ('base', 'data', 'root'): + if response.role in ("base", "data", "root"): assert response.code == 400 assert not response.data - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -458,26 +494,30 @@ def test_update_collection_bad(mdb): assert not response.data for _ in range(2): - indata = {'title': 'Test title'} - responses = make_request_all_roles(f'/api/v1/collection/{uuid.uuid4()}/', - method='PATCH', - data=indata, - ret_json=True) + indata = {"title": "Test title"} + responses = make_request_all_roles( + f"/api/v1/collection/{uuid.uuid4()}/", + method="PATCH", + data=indata, + ret_json=True, + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 404 assert not response.data - indata = {'title': 'Test title'} - responses = make_request_all_roles(f'/api/v1/collection/{random_string()}/', - method='PATCH', - data=indata, - ret_json=True) + indata = {"title": "Test title"} + responses = make_request_all_roles( + f"/api/v1/collection/{random_string()}/", + method="PATCH", + data=indata, + ret_json=True, + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -499,36 +539,46 @@ def test_delete_collection(mdb): session = requests.Session() # must be updated if TEST_LABEL is modified - collections = list(mdb['collections'].find({'extra.testing': 'yes'})) + collections = list(mdb["collections"].find({"extra.testing": "yes"})) i = 0 while i < len(collections): for role in USERS: as_user(session, USERS[role]) - response = make_request(session, - f'/api/v1/collection/{collections[i]["_id"]}/', - method='DELETE') - if role in ('data', 'root'): + response = make_request( + session, f'/api/v1/collection/{collections[i]["_id"]}/', method="DELETE" + ) + if role in ("data", "root"): assert response.code == 200 assert not response.data - assert not mdb['collections'].find_one({'_id': collections[i]['_id']}) - assert mdb['logs'].find_one({'data._id': collections[i]['_id'], - 'action': 'delete', - 'data_type': 'collection'}) + assert not mdb["collections"].find_one({"_id": collections[i]["_id"]}) + assert mdb["logs"].find_one( + { + "data._id": collections[i]["_id"], + "action": "delete", + "data_type": "collection", + } + ) i += 1 if i >= len(collections): break - elif role == 'no-login': + elif role == "no-login": assert response.code == 401 assert not response.data else: - current_user = mdb['users'].find_one({'auth_id': USERS[role]}) - if current_user['_id'] in collections[i]['owners']: + current_user = mdb["users"].find_one({"auth_id": USERS[role]}) + if current_user["_id"] in collections[i]["owners"]: assert response.code == 200 assert not response.data - assert not mdb['collections'].find_one({'_id': collections[i]['_id']}) - assert mdb['logs'].find_one({'data._id': collections[i]['_id'], - 'action': 'delete', - 'data_type': 'collection'}) + assert not mdb["collections"].find_one( + {"_id": collections[i]["_id"]} + ) + assert mdb["logs"].find_one( + { + "data._id": collections[i]["_id"], + "action": "delete", + "data_type": "collection", + } + ) i += 1 if i >= len(collections): break @@ -537,15 +587,14 @@ def test_delete_collection(mdb): assert response.code == 403 assert not response.data - as_user(session, USERS['base']) - response = make_request(session, - '/api/v1/collection/', - data={'title': 'tmp'}, - method='POST') + as_user(session, USERS["base"]) + response = make_request( + session, "/api/v1/collection/", data={"title": "tmp"}, method="POST" + ) assert response.code == 200 - response = make_request(session, - f'/api/v1/collection/{response.data["_id"]}/', - method='DELETE') + response = make_request( + session, f'/api/v1/collection/{response.data["_id"]}/', method="DELETE" + ) assert response.code == 200 assert not response.data @@ -554,18 +603,18 @@ def test_delete_collection_bad(): """Attempt bad collection delete requests.""" session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) for _ in range(2): - response = make_request(session, - f'/api/v1/collection/{random_string()}/', - method='DELETE') + response = make_request( + session, f"/api/v1/collection/{random_string()}/", method="DELETE" + ) assert response.code == 404 assert not response.data for _ in range(2): - response = make_request(session, - f'/api/v1/collection/{uuid.uuid4()}/', - method='DELETE') + response = make_request( + session, f"/api/v1/collection/{uuid.uuid4()}/", method="DELETE" + ) assert response.code == 404 assert not response.data @@ -576,10 +625,12 @@ def test_list_collections(mdb): Should also test e.g. pagination once implemented. """ - responses = make_request_all_roles('/api/v1/collection/', ret_json=True) + responses = make_request_all_roles("/api/v1/collection/", ret_json=True) for response in responses: assert response.code == 200 - assert len(response.data['collections']) == mdb['collections'].count_documents({}) + assert len(response.data["collections"]) == mdb["collections"].count_documents( + {} + ) def test_get_collection_logs_permissions(mdb): @@ -588,15 +639,16 @@ def test_get_collection_logs_permissions(mdb): Assert that DATA_MANAGEMENT or user in owners is required. """ - collection_data = mdb['collections'].aggregate([{'$sample': {'size': 1}}]).next() - user_data = mdb['users'].find_one({'_id': {'$in': collection_data['editors']}}) - responses = make_request_all_roles(f'/api/v1/collection/{collection_data["_id"]}/log/', - ret_json=True) + collection_data = mdb["collections"].aggregate([{"$sample": {"size": 1}}]).next() + user_data = mdb["users"].find_one({"_id": {"$in": collection_data["editors"]}}) + responses = make_request_all_roles( + f'/api/v1/collection/{collection_data["_id"]}/log/', ret_json=True + ) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 200 - assert 'logs' in response.data - elif response.role == 'no-login': + assert "logs" in response.data + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -605,13 +657,13 @@ def test_get_collection_logs_permissions(mdb): session = requests.Session() - as_user(session, user_data['auth_ids'][0]) - response = make_request(session, - f'/api/v1/collection/{collection_data["_id"]}/log/', - ret_json=True) + as_user(session, user_data["auth_ids"][0]) + response = make_request( + session, f'/api/v1/collection/{collection_data["_id"]}/log/', ret_json=True + ) assert response.code == 200 - assert 'logs' in response.data + assert "logs" in response.data def test_get_collection_logs(mdb): @@ -621,12 +673,16 @@ def test_get_collection_logs(mdb): Confirm that the logs contain only the intended fields. """ session = requests.session() - collections = mdb['collections'].aggregate([{'$sample': {'size': 2}}]) + collections = mdb["collections"].aggregate([{"$sample": {"size": 2}}]) for collection in collections: - logs = list(mdb['logs'].find({'data_type': 'collection', 'data._id': collection['_id']})) - as_user(session, USERS['data']) - response = make_request(session, f'/api/v1/collection/{collection["_id"]}/log/', ret_json=True) - assert response.data['dataType'] == 'collection' - assert response.data['entryId'] == str(collection['_id']) - assert len(response.data['logs']) == len(logs) + logs = list( + mdb["logs"].find({"data_type": "collection", "data._id": collection["_id"]}) + ) + as_user(session, USERS["data"]) + response = make_request( + session, f'/api/v1/collection/{collection["_id"]}/log/', ret_json=True + ) + assert response.data["dataType"] == "collection" + assert response.data["entryId"] == str(collection["_id"]) + assert len(response.data["logs"]) == len(logs) assert response.code == 200 diff --git a/backend/tests/test_datasets.py b/backend/tests/test_datasets.py index 858b82af..30cccf51 100644 --- a/backend/tests/test_datasets.py +++ b/backend/tests/test_datasets.py @@ -6,66 +6,136 @@ # avoid pylint errors because of fixtures # pylint: disable = redefined-outer-name, unused-import -from helpers import make_request, as_user, make_request_all_roles,\ - dataset_for_tests, USERS, random_string, parse_time, TEST_LABEL, mdb,\ - add_dataset, delete_dataset, USER_RE +from helpers import ( + make_request, + as_user, + make_request_all_roles, + dataset_for_tests, + USERS, + random_string, + parse_time, + TEST_LABEL, + mdb, + add_dataset, + delete_dataset, + USER_RE, +) -def test_list_user_datasets(mdb): +def test_list_datasets(mdb): + """ + Confirm that listing datasets work as intended. + + Tests: + + * Confirm all datasets in the database are listed. + * Confirm that the correct fields are included + """ + responses = make_request_all_roles("/api/v1/dataset/", ret_json=True) + expected_fields = {"title", "_id", "tags", "properties"} + for response in responses: + assert response.code == 200 + assert len(response.data["datasets"]) == mdb["datasets"].count_documents({}) + assert set(response.data["datasets"][0].keys()) == expected_fields + + +def test_list_user_datasets_permissions(): """ - Choose a few users. + Confirm that users get the correct status code response. - Compare the ids of datasets from the request to a db query. + Tests: + + * Confirm that non-logged in users get 401, logged in users 200 + """ + responses = make_request_all_roles("/api/v1/dataset/user/") + for response in responses: + if response.role == "no-login": + assert response.code == 401 + else: + assert response.code == 200 + + +def test_list_user_datasets_with_datasets(mdb): + """ + Confirm that users get the correct datasets. + + Tests: + + * Select a few users, confirm that the returned datasets are correct + * Confirm that the included fields are the intended ones """ session = requests.Session() - users = mdb['users'].aggregate([{'$sample': {'size': 5}}, - {'$match': {'auth_ids': USER_RE}}]) + orders = mdb["orders"].aggregate( + [{"$match": {"datasets": {"$not": {"$size": 0}}}}, {"$sample": {"size": 2}}] + ) + user_uuids = list( + itertools.chain.from_iterable(order["editors"] for order in orders) + ) + users = mdb["users"].find({"_id": {"$in": list(user_uuids)}}) for user in users: - user_orders = list(mdb['orders'].find({'$or': [{'editors': user['_id']}, - {'receivers': user['_id']}], - 'datasets': {'$not': {'$size': 0}}}, - {'datasets': 1})) - user_datasets = list(itertools.chain.from_iterable(order['datasets'] - for order in user_orders)) + user_orders = list( + mdb["orders"].find({"editors": user["_id"]}, {"datasets": 1}) + ) + user_datasets = list( + itertools.chain.from_iterable(order["datasets"] for order in user_orders) + ) user_datasets = [str(uuid) for uuid in user_datasets] - as_user(session, user['auth_ids'][0]) - response = make_request(session, '/api/v1/dataset/user/') + as_user(session, user["auth_ids"][0]) + response = make_request(session, "/api/v1/dataset/user/") assert response.code == 200 - assert len(user_datasets) == len(response.data['datasets']) - for dset in response.data['datasets']: - assert dset['_id'] in user_datasets + assert len(user_datasets) == len(response.data["datasets"]) + assert set(entry["_id"] for entry in response.data["datasets"]) == set( + user_datasets + ) + + +def test_list_user_datasets_no_datasets(): + """ + Confirm that users with no datasets get the correct response. + + Tests: + + * Select a few users, confirm that no datasets are returned as intended + """ + # *::testers should have no datasets + responses = make_request_all_roles("/api/v1/dataset/user/", ret_json=True) + for response in responses: + if response.role != "no-login": + assert len(response.data["datasets"]) == 0 def test_random_dataset(): """Request a random dataset.""" - responses = make_request_all_roles('/api/v1/dataset/random/', ret_json=True) + responses = make_request_all_roles("/api/v1/dataset/random/", ret_json=True) for response in responses: assert response.code == 200 - assert len(response.data['datasets']) == 1 + assert len(response.data["datasets"]) == 1 def test_random_datasets(): """Request random datasets.""" session = requests.Session() - as_user(session, USERS['base']) + as_user(session, USERS["base"]) for i in (1, 5, 0): - response = make_request(session, f'/api/v1/dataset/random/{i}/') + response = make_request(session, f"/api/v1/dataset/random/{i}/") assert response.code == 200 - assert len(response.data['datasets']) == i + assert len(response.data["datasets"]) == i - response = make_request(session, '/api/v1/dataset/random/-1') + response = make_request(session, "/api/v1/dataset/random/-1") assert response.code == 404 assert not response.data def test_get_dataset_get_permissions(mdb): """Test permissions for requesting a dataset.""" - orders = list(mdb['datasets'].aggregate([{'$sample': {'size': 2}}])) + orders = list(mdb["datasets"].aggregate([{"$sample": {"size": 2}}])) for order in orders: - responses = make_request_all_roles(f'/api/v1/dataset/{order["_id"]}/', ret_json=True) + responses = make_request_all_roles( + f'/api/v1/dataset/{order["_id"]}/', ret_json=True + ) for response in responses: - assert response.data['dataset'] + assert response.data["dataset"] assert response.code == 200 @@ -77,12 +147,12 @@ def test_get_dataset(): """ session = requests.Session() for _ in range(10): - orig = make_request(session, '/api/v1/dataset/random/')[0]['datasets'][0] + orig = make_request(session, "/api/v1/dataset/random/")[0]["datasets"][0] response = make_request(session, f'/api/v1/dataset/{orig["_id"]}/') assert response[1] == 200 - requested = response[0]['dataset'] + requested = response[0]["dataset"] assert orig == requested - assert requested['_id'] not in requested['related'] + assert requested["_id"] not in requested["related"] def test_get_dataset_bad(): @@ -93,12 +163,12 @@ def test_get_dataset_bad(): """ session = requests.Session() for _ in range(5): - response = make_request(session, f'/api/v1/dataset/{uuid.uuid4().hex}/') + response = make_request(session, f"/api/v1/dataset/{uuid.uuid4().hex}/") assert response.code == 404 assert not response.data for _ in range(5): - response = make_request(session, f'/api/v1/dataset/{random_string()}/') + response = make_request(session, f"/api/v1/dataset/{random_string()}/") assert response.code == 404 assert not response.data @@ -116,42 +186,60 @@ def test_delete_dataset(mdb): uuids = [add_dataset() for _ in range(5)] - datasets = list(mdb['datasets'].find(TEST_LABEL)) + datasets = list(mdb["datasets"].find(TEST_LABEL)) if not datasets: assert False i = 0 while i < len(datasets): for role in USERS: as_user(session, USERS[role]) - order = mdb['orders'].find_one({'datasets': datasets[i]['_id']}) - collections = list(mdb['collections'].find({'datasets': datasets[i]['_id']})) - response = make_request(session, - f'/api/v1/dataset/{datasets[i]["_id"]}/', - method='DELETE') - current_user = mdb['users'].find_one({'auth_ids': USERS[role]}) - if role == 'no-login': + order = mdb["orders"].find_one({"datasets": datasets[i]["_id"]}) + collections = list( + mdb["collections"].find({"datasets": datasets[i]["_id"]}) + ) + response = make_request( + session, f'/api/v1/dataset/{datasets[i]["_id"]}/', method="DELETE" + ) + current_user = mdb["users"].find_one({"auth_ids": USERS[role]}) + if role == "no-login": assert response.code == 401 assert not response.data # only data managers or owners may delete datasets - elif role in ('data', 'root') or current_user['_id'] in order['editors']: + elif role in ("data", "root") or current_user["_id"] in order["editors"]: assert response.code == 200 assert not response.data # confirm that dataset does not exist in mdb and that a log has been created - assert not mdb['datasets'].find_one({'_id': datasets[i]['_id']}) - assert mdb['logs'].find_one({'data._id': datasets[i]['_id'], - 'action': 'delete', - 'data_type': 'dataset'}) + assert not mdb["datasets"].find_one({"_id": datasets[i]["_id"]}) + assert mdb["logs"].find_one( + { + "data._id": datasets[i]["_id"], + "action": "delete", + "data_type": "dataset", + } + ) # confirm that no references to the dataset exist in orders or collection - assert not list(mdb['orders'].find({'datasets': datasets[i]['_id']})) - assert not list(mdb['collections'].find({'datasets': datasets[i]['_id']})) + assert not list(mdb["orders"].find({"datasets": datasets[i]["_id"]})) + assert not list( + mdb["collections"].find({"datasets": datasets[i]["_id"]}) + ) # confirm that the removal of the references are logged. - assert mdb['logs'].find_one({'data._id': order['_id'], - 'action': 'edit', - 'data_type': 'order', - 'comment': f'Deleted dataset {datasets[i]["_id"]}'}) - p_log = list(mdb['logs'].find({'action': 'edit', - 'data_type': 'collection', - 'comment': f'Deleted dataset {datasets[i]["_id"]}'})) + assert mdb["logs"].find_one( + { + "data._id": order["_id"], + "action": "edit", + "data_type": "order", + "comment": f'Deleted dataset {datasets[i]["_id"]}', + } + ) + p_log = list( + mdb["logs"].find( + { + "action": "edit", + "data_type": "collection", + "comment": f'Deleted dataset {datasets[i]["_id"]}', + } + ) + ) assert len(p_log) == len(collections) i += 1 if i >= len(datasets): @@ -172,18 +260,14 @@ def test_delete_bad(): Should require at least Steward. """ session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) for _ in range(3): ds_uuid = random_string() - response = make_request(session, - f'/api/v1/dataset/{ds_uuid}/', - method='DELETE') + response = make_request(session, f"/api/v1/dataset/{ds_uuid}/", method="DELETE") assert response.code == 404 assert not response.data ds_uuid = uuid.uuid4().hex - response = make_request(session, - f'/api/v1/dataset/{ds_uuid}/', - method='DELETE') + response = make_request(session, f"/api/v1/dataset/{ds_uuid}/", method="DELETE") assert response.code == 404 assert not response.data @@ -195,12 +279,14 @@ def test_dataset_update_permissions(dataset_for_tests): Should require at least Steward or being the owner of the dataset. """ ds_uuid = dataset_for_tests - indata = {'title': 'Updated title'} - responses = make_request_all_roles(f'/api/v1/dataset/{ds_uuid}/', method='PATCH', data=indata) + indata = {"title": "Updated title"} + responses = make_request_all_roles( + f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 @@ -215,11 +301,13 @@ def test_dataset_update_empty(dataset_for_tests): """ ds_uuid = dataset_for_tests indata = {} - responses = make_request_all_roles(f'/api/v1/dataset/{ds_uuid}/', method='PATCH', data=indata) + responses = make_request_all_roles( + f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 @@ -233,23 +321,27 @@ def test_dataset_update(mdb, dataset_for_tests): Should require at least Steward. """ ds_uuid = dataset_for_tests - indata = {'description': 'Test description - updated', - 'title': 'Test title - updated'} + indata = { + "description": "Test description - updated", + "title": "Test title - updated", + } indata.update(TEST_LABEL) session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) - response = make_request(session, f'/api/v1/dataset/{ds_uuid}/', method='PATCH', data=indata) + response = make_request( + session, f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) assert response.code == 200 assert not response.data - dataset = mdb['datasets'].find_one({'_id': ds_uuid}) + dataset = mdb["datasets"].find_one({"_id": ds_uuid}) for field in indata: assert dataset[field] == indata[field] - assert mdb['logs'].find_one({'data._id': ds_uuid, - 'action': 'edit', - 'data_type': 'dataset'}) + assert mdb["logs"].find_one( + {"data._id": ds_uuid, "action": "edit", "data_type": "dataset"} + ) def test_dataset_update_bad(dataset_for_tests): @@ -259,26 +351,28 @@ def test_dataset_update_bad(dataset_for_tests): Should require at least Steward. """ for _ in range(2): - indata = {'title': 'Updated title'} + indata = {"title": "Updated title"} ds_uuid = random_string() - responses = make_request_all_roles(f'/api/v1/dataset/{ds_uuid}/', - method='PATCH', data=indata) + responses = make_request_all_roles( + f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) for response in responses: - if response.role in ('base', 'orders', 'data', 'root'): + if response.role in ("base", "orders", "data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 404 assert not response.data ds_uuid = uuid.uuid4().hex - responses = make_request_all_roles(f'/api/v1/dataset/{ds_uuid}/', - method='PATCH', data=indata) + responses = make_request_all_roles( + f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) for response in responses: - if response.role in ('base', 'orders', 'data', 'root'): + if response.role in ("base", "orders", "data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 404 @@ -286,54 +380,48 @@ def test_dataset_update_bad(dataset_for_tests): ds_uuid = dataset_for_tests session = requests.Session() - as_user(session, USERS['data']) - indata = {'title': ''} - response = make_request(session, f'/api/v1/dataset/{ds_uuid}/', - method='PATCH', data=indata) + as_user(session, USERS["data"]) + indata = {"title": ""} + response = make_request( + session, f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) assert response.code == 400 assert not response.data - indata = {'extra': 'asd'} - response = make_request(session, f'/api/v1/dataset/{ds_uuid}/', - method='PATCH', data=indata) + indata = {"extra": "asd"} + response = make_request( + session, f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) assert response.code == 400 assert not response.data - indata = {'timestamp': 'asd'} - response = make_request(session, f'/api/v1/dataset/{ds_uuid}/', - method='PATCH', data=indata) + indata = {"timestamp": "asd"} + response = make_request( + session, f"/api/v1/dataset/{ds_uuid}/", method="PATCH", data=indata + ) assert response.code == 400 assert not response.data -def test_list_datasets(mdb): - """ - Request a list of all datasets. - - Should also test e.g. pagination once implemented. - """ - responses = make_request_all_roles('/api/v1/dataset/', ret_json=True) - for response in responses: - assert response.code == 200 - assert len(response.data['datasets']) == mdb['datasets'].count_documents({}) - - def test_get_dataset_logs_permissions(mdb): """ Get dataset logs. Assert that DATA_MANAGEMENT or user in editors is required. """ - dataset_data = mdb['datasets'].aggregate([{'$sample': {'size': 1}}]).next() - order_data = mdb['orders'].find_one({'datasets': dataset_data['_id']}) - user_data = mdb['users'].find_one({'$or': [{'_id': {'$in': order_data['editors']}}]}) - responses = make_request_all_roles(f'/api/v1/dataset/{dataset_data["_id"]}/log/', - ret_json=True) + dataset_data = mdb["datasets"].aggregate([{"$sample": {"size": 1}}]).next() + order_data = mdb["orders"].find_one({"datasets": dataset_data["_id"]}) + user_data = mdb["users"].find_one( + {"$or": [{"_id": {"$in": order_data["editors"]}}]} + ) + responses = make_request_all_roles( + f'/api/v1/dataset/{dataset_data["_id"]}/log/', ret_json=True + ) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 200 - assert 'logs' in response.data - elif response.role == 'no-login': + assert "logs" in response.data + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -342,13 +430,13 @@ def test_get_dataset_logs_permissions(mdb): session = requests.Session() - as_user(session, user_data['auth_ids'][0]) - response = make_request(session, - f'/api/v1/dataset/{dataset_data["_id"]}/log/', - ret_json=True) + as_user(session, user_data["auth_ids"][0]) + response = make_request( + session, f'/api/v1/dataset/{dataset_data["_id"]}/log/', ret_json=True + ) assert response.code == 200 - assert 'logs' in response.data + assert "logs" in response.data def test_get_dataset_logs(mdb): @@ -358,14 +446,18 @@ def test_get_dataset_logs(mdb): Confirm that the logs contain only the intended fields. """ session = requests.session() - datasets = mdb['datasets'].aggregate([{'$sample': {'size': 2}}]) + datasets = mdb["datasets"].aggregate([{"$sample": {"size": 2}}]) for dataset in datasets: - logs = list(mdb['logs'].find({'data_type': 'dataset', 'data._id': dataset['_id']})) - as_user(session, USERS['data']) - response = make_request(session, f'/api/v1/dataset/{dataset["_id"]}/log/', ret_json=True) - assert response.data['dataType'] == 'dataset' - assert response.data['entryId'] == str(dataset['_id']) - assert len(response.data['logs']) == len(logs) + logs = list( + mdb["logs"].find({"data_type": "dataset", "data._id": dataset["_id"]}) + ) + as_user(session, USERS["data"]) + response = make_request( + session, f'/api/v1/dataset/{dataset["_id"]}/log/', ret_json=True + ) + assert response.data["dataType"] == "dataset" + assert response.data["entryId"] == str(dataset["_id"]) + assert len(response.data["logs"]) == len(logs) assert response.code == 200 @@ -378,22 +470,20 @@ def test_add_dataset_permissions(mdb): session = requests.Session() db = mdb - orders = db['orders'].aggregate([{'$sample': {'size': 2}}]) + orders = db["orders"].aggregate([{"$sample": {"size": 2}}]) for order in orders: - indata = {'title': 'Test title', - 'order': str(order['_id'])} + indata = {"title": "Test title", "order": str(order["_id"])} indata.update(TEST_LABEL) - responses = make_request_all_roles(f'/api/v1/dataset/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/dataset/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - elif response.role == 'no-login': + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -401,15 +491,12 @@ def test_add_dataset_permissions(mdb): assert not response.data # as order editor - owner = db['users'].find_one({'_id': order['editors'][0]}) - as_user(session, owner['auth_ids'][0]) - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata) + owner = db["users"].find_one({"_id": order["editors"][0]}) + as_user(session, owner["auth_ids"][0]) + response = make_request(session, "/api/v1/dataset/", method="POST", data=indata) assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 + assert "_id" in response.data + assert len(response.data["_id"]) == 36 def test_add_dataset(mdb): @@ -418,33 +505,33 @@ def test_add_dataset(mdb): Set values in all available fields. """ - order = next(mdb['orders'].aggregate([{'$sample': {'size': 1}}])) - indata = {'title': 'Test title', - 'description': 'Test description', - 'order': str(order['_id']),} + order = next(mdb["orders"].aggregate([{"$sample": {"size": 1}}])) + indata = { + "title": "Test title", + "description": "Test description", + "order": str(order["_id"]), + } indata.update(TEST_LABEL) session = requests.session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata, - ret_json=True) + response = make_request( + session, "/api/v1/dataset/", method="POST", data=indata, ret_json=True + ) assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - indata.update({'_id': response.data['_id']}) - mdb_ds = mdb['datasets'].find_one({'_id': uuid.UUID(response.data['_id'])}) - mdb_o = mdb['orders'].find_one({'_id': order['_id']}) - mdb_ds['_id'] = str(mdb_ds['_id']) - mdb_o['datasets'] = [str(ds_uuid) for ds_uuid in mdb_o['datasets']] + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + indata.update({"_id": response.data["_id"]}) + mdb_ds = mdb["datasets"].find_one({"_id": uuid.UUID(response.data["_id"])}) + mdb_o = mdb["orders"].find_one({"_id": order["_id"]}) + mdb_ds["_id"] = str(mdb_ds["_id"]) + mdb_o["datasets"] = [str(ds_uuid) for ds_uuid in mdb_o["datasets"]] for field in indata: - if field == 'order': + if field == "order": continue assert mdb_ds[field] == indata[field] - assert response.data['_id'] in mdb_o['datasets'] + assert response.data["_id"] in mdb_o["datasets"] def test_add_dataset_log(mdb): @@ -453,80 +540,67 @@ def test_add_dataset_log(mdb): Check that both there is both update on order and add on dataset. """ - order = next(mdb['orders'].aggregate([{'$sample': {'size': 1}}])) - indata = {'title': 'Test title', - 'order': str(order['_id'])} + order = next(mdb["orders"].aggregate([{"$sample": {"size": 1}}])) + indata = {"title": "Test title", "order": str(order["_id"])} indata.update(TEST_LABEL) session = requests.session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) - order_logs = list(mdb['logs'].find({'data_type': 'order', 'data._id': order['_id']})) + order_logs = list( + mdb["logs"].find({"data_type": "order", "data._id": order["_id"]}) + ) - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata, - ret_json=True) + response = make_request( + session, "/api/v1/dataset/", method="POST", data=indata, ret_json=True + ) - order_logs_post = list(mdb['logs'].find({'data_type': 'order', 'data._id': order['_id']})) + order_logs_post = list( + mdb["logs"].find({"data_type": "order", "data._id": order["_id"]}) + ) print(order_logs_post) - assert len(order_logs_post) == len(order_logs)+1 - ds_logs_post = list(mdb['logs'].find({'data_type': 'dataset', - 'data._id': uuid.UUID(response.data['_id'])})) + assert len(order_logs_post) == len(order_logs) + 1 + ds_logs_post = list( + mdb["logs"].find( + {"data_type": "dataset", "data._id": uuid.UUID(response.data["_id"])} + ) + ) assert len(ds_logs_post) == 1 - assert ds_logs_post[0]['action'] + assert ds_logs_post[0]["action"] def test_add_dataset_bad_fields(mdb): """Attempt to add datasets with e.g. forbidden fields.""" db = mdb - order = next(db['orders'].aggregate([{'$sample': {'size': 1}}])) + order = next(db["orders"].aggregate([{"$sample": {"size": 1}}])) session = requests.Session() - as_user(session, USERS['data']) - - indata = {'_id': 'asd', - 'title': 'test title', - 'order': str(order["_id"]),} - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata) + as_user(session, USERS["data"]) + + indata = { + "_id": "asd", + "title": "test title", + "order": str(order["_id"]), + } + response = make_request(session, "/api/v1/dataset/", method="POST", data=indata) assert response.code == 403 assert not response.data - indata = {'timestamp': 'asd', - 'title': 'test title'} - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata) + indata = {"timestamp": "asd", "title": "test title"} + response = make_request(session, "/api/v1/dataset/", method="POST", data=indata) assert response.code == 400 assert not response.data - indata = {'extra': [{'asd': 123}], - 'title': 'test title'} - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata) + indata = {"extra": [{"asd": 123}], "title": "test title"} + response = make_request(session, "/api/v1/dataset/", method="POST", data=indata) assert response.code == 400 assert not response.data - indata = {'links': [{'asd': 123}], - 'title': 'test title'} - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata) + indata = {"links": [{"asd": 123}], "title": "test title"} + response = make_request(session, "/api/v1/dataset/", method="POST", data=indata) assert response.code == 400 assert not response.data - indata = {'links': 'Some text', - 'title': 'test title'} - response = make_request(session, - f'/api/v1/dataset/', - method='POST', - data=indata) + indata = {"links": "Some text", "title": "test title"} + response = make_request(session, "/api/v1/dataset/", method="POST", data=indata) assert response.code == 400 assert not response.data diff --git a/backend/tests/test_logins.py b/backend/tests/test_logins.py index 83e71eb2..b15409ee 100644 --- a/backend/tests/test_logins.py +++ b/backend/tests/test_logins.py @@ -6,20 +6,29 @@ import helpers -from helpers import make_request, as_user, make_request_all_roles, USERS, mdb, random_string +from helpers import ( + make_request, + as_user, + make_request_all_roles, + USERS, + mdb, + random_string, +) + # pylint: disable=redefined-outer-name + def test_logout(): """Assure that session is cleared after logging out.""" session = requests.Session() - as_user(session, USERS['root']) - response = make_request(session, '/api/v1/user/me/') - for field in response.data['user']: - assert response.data['user'][field] - response = make_request(session, '/api/v1/logout/', ret_json=False) - response = make_request(session, '/api/v1/user/me/') - for field in response.data['user']: - assert not response.data['user'][field] + as_user(session, USERS["root"]) + response = make_request(session, "/api/v1/user/me/") + for field in response.data["user"]: + assert response.data["user"][field] + response = make_request(session, "/api/v1/logout/", ret_json=False) + response = make_request(session, "/api/v1/user/me/") + for field in response.data["user"]: + assert not response.data["user"][field] def test_key_login(): @@ -27,35 +36,35 @@ def test_key_login(): session = requests.Session() as_user(session, None) for i, userid in enumerate(USERS): - response = make_request(session, - '/api/v1/login/apikey/', - data = {'api-user': USERS[userid], - 'api-key': str(i-1)}, - method='POST') - if userid == 'no-login': + response = make_request( + session, + "/api/v1/login/apikey/", + data={"api-user": USERS[userid], "api-key": str(i - 1)}, + method="POST", + ) + if userid == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 200 assert not response.data - response = make_request(session, - '/api/v1/developer/loginhello') + response = make_request(session, "/api/v1/developer/loginhello") assert response.code == 200 - assert response.data == {'test': 'success'} + assert response.data == {"test": "success"} def test_list_login_types(): """List possible ways to login""" - responses = helpers.make_request_all_roles('/api/v1/login/', ret_json=True) + responses = helpers.make_request_all_roles("/api/v1/login/", ret_json=True) for response in responses: assert response.code == 200 - assert response.data == {'types': ['apikey', 'oidc']} + assert response.data == {"types": ["apikey", "oidc"]} def test_list_oidc_types(): """List supported oidc logins""" - responses = helpers.make_request_all_roles('/api/v1/login/oidc/', ret_json=True) + responses = helpers.make_request_all_roles("/api/v1/login/oidc/", ret_json=True) for response in responses: assert response.code == 200 - assert response.data == {'oidcserver': '/api/v1/login/oidc/oidcserver/login/'} + assert response.data == {"entry": "/api/v1/login/oidc/entry/login/"} diff --git a/backend/tests/test_orders.py b/backend/tests/test_orders.py index 66281893..26b938ed 100644 --- a/backend/tests/test_orders.py +++ b/backend/tests/test_orders.py @@ -7,8 +7,16 @@ import structure import utils -from helpers import make_request, as_user, make_request_all_roles,\ - USERS, random_string, TEST_LABEL, mdb, USER_RE +from helpers import ( + make_request, + as_user, + make_request_all_roles, + USERS, + random_string, + TEST_LABEL, + mdb, + USER_RE, +) # avoid pylint errors because of fixtures # pylint: disable = redefined-outer-name, unused-import @@ -26,25 +34,30 @@ def test_get_order_permissions(mdb): session = requests.Session() db = mdb - orders = list(db['orders'].aggregate([{'$match': {'auth_ids': USER_RE}}, - {'$sample': {'size': 2}}])) + orders = list( + db["orders"].aggregate( + [{"$match": {"auth_ids": USER_RE}}, {"$sample": {"size": 2}}] + ) + ) for order in orders: - owner = db['users'].find_one({'_id': order['editors'][0]}) - responses = make_request_all_roles(f'/api/v1/order/{order["_id"]}/', ret_json=True) + owner = db["users"].find_one({"_id": order["editors"][0]}) + responses = make_request_all_roles( + f'/api/v1/order/{order["_id"]}/', ret_json=True + ) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 200 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - as_user(session, owner['auth_id']) + as_user(session, owner["auth_id"]) response = make_request(session, f'/api/v1/order/{order["_id"]}/') assert response.code == 200 - data = response.data['order'] + data = response.data["order"] def test_get_order(mdb): @@ -54,32 +67,33 @@ def test_get_order(mdb): Request the order and confirm that it contains the correct data. """ session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) db = mdb - orders = list(db['orders'].aggregate([{'$sample': {'size': 3}}])) + orders = list(db["orders"].aggregate([{"$sample": {"size": 3}}])) for order in orders: # to simplify comparison - order['_id'] = str(order['_id']) + order["_id"] = str(order["_id"]) # user entries - for key in ('authors', 'generators', 'editors'): + for key in ("authors", "generators", "editors"): order[key] = utils.user_uuid_data(order[key], db) - order['organisation'] = utils.user_uuid_data(order['organisation'], db)[0] - - for i, ds in enumerate(order['datasets']): - order['datasets'][i] = next(db['datasets'].aggregate([{'$match': {'_id': ds}}, - {'$project': {'_id': 1, - 'title': 1}}])) - order['datasets'][i]['_id'] = str(order['datasets'][i]['_id']) + order["organisation"] = utils.user_uuid_data(order["organisation"], db)[0] + for i, ds in enumerate(order["datasets"]): + order["datasets"][i] = next( + db["datasets"].aggregate( + [{"$match": {"_id": ds}}, {"$project": {"_id": 1, "title": 1}}] + ) + ) + order["datasets"][i]["_id"] = str(order["datasets"][i]["_id"]) response = make_request(session, f'/api/v1/order/{order["_id"]}/') assert response.code == 200 assert response.code == 200 - data = response.data['order'] + data = response.data["order"] assert len(order) == len(data) for field in order: - if field == 'datasets': + if field == "datasets": assert len(order[field]) == len(data[field]) for ds in order[field]: assert ds in data[field] @@ -87,18 +101,17 @@ def test_get_order(mdb): assert order[field] == data[field] - def test_get_order_structure(): """Request the order structure and confirm that it matches the official one""" session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) reference = structure.order() - reference['_id'] = '' + reference["_id"] = "" - response = make_request(session, '/api/v1/order/base/') + response = make_request(session, "/api/v1/order/base/") assert response.code == 200 - data = response.data['order'] + data = response.data["order"] assert data == reference @@ -109,22 +122,22 @@ def test_get_order_bad(): All are expected to return 401, 403, or 404 depending on permissions. """ for _ in range(2): - responses = make_request_all_roles(f'/api/v1/order/{uuid.uuid4()}/') + responses = make_request_all_roles(f"/api/v1/order/{uuid.uuid4()}/") for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data for _ in range(2): - responses = make_request_all_roles(f'/api/v1/order/{random_string()}/') + responses = make_request_all_roles(f"/api/v1/order/{random_string()}/") for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 @@ -138,15 +151,16 @@ def test_get_order_logs_permissions(mdb): Assert that DATA_MANAGEMENT or user in creator is required. """ db = mdb - order_data = db['orders'].aggregate([{'$sample': {'size': 1}}]).next() - user_data = db['users'].find_one({'_id': {'$in': order_data['editors']}}) - responses = make_request_all_roles(f'/api/v1/order/{order_data["_id"]}/log/', - ret_json=True) + order_data = db["orders"].aggregate([{"$sample": {"size": 1}}]).next() + user_data = db["users"].find_one({"_id": {"$in": order_data["editors"]}}) + responses = make_request_all_roles( + f'/api/v1/order/{order_data["_id"]}/log/', ret_json=True + ) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 200 - assert 'logs' in response.data - elif response.role == 'no-login': + assert "logs" in response.data + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -155,13 +169,13 @@ def test_get_order_logs_permissions(mdb): session = requests.Session() - as_user(session, user_data['auth_ids'][0]) - response = make_request(session, - f'/api/v1/order/{order_data["_id"]}/log/', - ret_json=True) + as_user(session, user_data["auth_ids"][0]) + response = make_request( + session, f'/api/v1/order/{order_data["_id"]}/log/', ret_json=True + ) assert response.code == 200 - assert 'logs' in response.data + assert "logs" in response.data def test_get_order_logs(mdb): @@ -172,14 +186,16 @@ def test_get_order_logs(mdb): """ session = requests.session() db = mdb - orders = db['orders'].aggregate([{'$sample': {'size': 2}}]) + orders = db["orders"].aggregate([{"$sample": {"size": 2}}]) for order in orders: - logs = list(db['logs'].find({'data_type': 'order', 'data._id': order['_id']})) - as_user(session, USERS['data']) - response = make_request(session, f'/api/v1/order/{order["_id"]}/log/', ret_json=True) - assert response.data['dataType'] == 'order' - assert response.data['entryId'] == str(order['_id']) - assert len(response.data['logs']) == len(logs) + logs = list(db["logs"].find({"data_type": "order", "data._id": order["_id"]})) + as_user(session, USERS["data"]) + response = make_request( + session, f'/api/v1/order/{order["_id"]}/log/', ret_json=True + ) + assert response.data["dataType"] == "order" + assert response.data["entryId"] == str(order["_id"]) + assert len(response.data["logs"]) == len(logs) assert response.code == 200 @@ -191,10 +207,14 @@ def test_get_order_logs_bad(): """ session = requests.session() for _ in range(2): - as_user(session, USERS['data']) - response = make_request(session, f'/api/v1/order/{uuid.uuid4()}/log/', ret_json=True) + as_user(session, USERS["data"]) + response = make_request( + session, f"/api/v1/order/{uuid.uuid4()}/log/", ret_json=True + ) assert response.code == 200 - response = make_request(session, f'/api/v1/order/{random_string()}/log/', ret_json=True) + response = make_request( + session, f"/api/v1/order/{random_string()}/log/", ret_json=True + ) assert response.code == 404 @@ -207,47 +227,52 @@ def test_list_user_orders_permissions(mdb): session = requests.Session() db = mdb - users = db['users'].aggregate([{'$match': {'permissions': {'$in': ['ORDERS_SELF', - 'DATA_MANAGEMENT']}}}, - {'$sample': {'size': 2}}]) + users = db["users"].aggregate( + [ + {"$match": {"permissions": {"$in": ["ORDERS_SELF", "DATA_MANAGEMENT"]}}}, + {"$sample": {"size": 2}}, + ] + ) for user in users: - responses = make_request_all_roles('/api/v1/order/user/', ret_json=True) + responses = make_request_all_roles("/api/v1/order/user/", ret_json=True) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - assert len(response.data['orders']) == 0 - elif response.role == 'no-login': + assert len(response.data["orders"]) == 0 + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - user_orders = list(db['orders'].find({'editors': user['_id']})) - responses = make_request_all_roles(f'/api/v1/order/user/{user["_id"]}/', ret_json=True) + user_orders = list(db["orders"].find({"editors": user["_id"]})) + responses = make_request_all_roles( + f'/api/v1/order/user/{user["_id"]}/', ret_json=True + ) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): if user_orders: assert response.code == 200 assert response.data else: assert response.code == 200 - assert len(response.data['orders']) == 0 - elif response.role == 'no-login': + assert len(response.data["orders"]) == 0 + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - as_user(session, user['auth_ids'][0]) - response = make_request(session, '/api/v1/order/user/') + as_user(session, user["auth_ids"][0]) + response = make_request(session, "/api/v1/order/user/") if user_orders: assert response.code == 200 assert response.data else: assert response.code == 200 - assert len(response.data['orders']) == 0 + assert len(response.data["orders"]) == 0 def test_list_user_orders(mdb): @@ -259,25 +284,28 @@ def test_list_user_orders(mdb): session = requests.Session() db = mdb - users = db['users'].aggregate([{'$match': {'permissions': {'$in': ['ORDERS_SELF', - 'DATA_MANAGEMENT']}}}, - {'$sample': {'size': 2}}]) + users = db["users"].aggregate( + [ + {"$match": {"permissions": {"$in": ["ORDERS_SELF", "DATA_MANAGEMENT"]}}}, + {"$sample": {"size": 2}}, + ] + ) for user in users: - user_orders = list(db['orders'].find({'editors': user['_id']})) - order_uuids = [str(order['_id']) for order in user_orders] + user_orders = list(db["orders"].find({"editors": user["_id"]})) + order_uuids = [str(order["_id"]) for order in user_orders] - as_user(session, user['auth_ids'][0]) - response = make_request(session, '/api/v1/order/user/') + as_user(session, user["auth_ids"][0]) + response = make_request(session, "/api/v1/order/user/") if user_orders: assert response.code == 200 assert response.data - assert len(user_orders) == len(response.data['orders']) - for order in response.data['orders']: - assert order['_id'] in order_uuids + assert len(user_orders) == len(response.data["orders"]) + for order in response.data["orders"]: + assert order["_id"] in order_uuids else: assert response.code == 200 - assert len(response.data['orders']) == 0 + assert len(response.data["orders"]) == 0 def test_list_user_orders_bad(): @@ -288,13 +316,13 @@ def test_list_user_orders_bad(): """ session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) for _ in range(2): - responses = make_request_all_roles(f'/api/v1/order/user/{uuid.uuid4()}/') + responses = make_request_all_roles(f"/api/v1/order/user/{uuid.uuid4()}/") for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -302,11 +330,11 @@ def test_list_user_orders_bad(): assert not response.data for _ in range(2): - responses = make_request_all_roles(f'/api/v1/order/user/{random_string()}/') + responses = make_request_all_roles(f"/api/v1/order/user/{random_string()}/") for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 @@ -319,18 +347,17 @@ def test_add_order_permissions(): Test permissions. """ - indata = {'title': 'Test title'} + indata = {"title": "Test title"} indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/order/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/order/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - elif response.role == 'no-login': + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -346,61 +373,62 @@ def test_add_order(mdb): """ db = mdb - indata = {'description': 'Test description', - 'title': 'Test title'} + indata = {"description": "Test description", "title": "Test title"} indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/order/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/order/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - order = db['orders'].find_one({'_id': uuid.UUID(response.data['_id'])}) - curr_user = db['users'].find_one({'auth_ids': USERS[response.role]}) - assert order['description'] == indata['description'] - assert order['title'] == indata['title'] - assert curr_user['_id'] in order['editors'] - elif response.role == 'no-login': + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + order = db["orders"].find_one({"_id": uuid.UUID(response.data["_id"])}) + curr_user = db["users"].find_one({"auth_ids": USERS[response.role]}) + assert order["description"] == indata["description"] + assert order["title"] == indata["title"] + assert curr_user["_id"] in order["editors"] + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - orders_user = db['users'].find_one({'auth_ids': USERS['orders']}) - indata = {'description': 'Test description', - 'authors': [str(orders_user['_id'])], - 'editors': [str(orders_user['_id'])], - 'generators': [str(orders_user['_id'])], - 'organisation': str(orders_user['_id']), - 'title': 'Test title'} + orders_user = db["users"].find_one({"auth_ids": USERS["orders"]}) + indata = { + "description": "Test description", + "authors": [str(orders_user["_id"])], + "editors": [str(orders_user["_id"])], + "generators": [str(orders_user["_id"])], + "organisation": str(orders_user["_id"]), + "title": "Test title", + } indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/order/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/order/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - order = db['orders'].find_one({'_id': uuid.UUID(response.data['_id'])}) + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + order = db["orders"].find_one({"_id": uuid.UUID(response.data["_id"])}) - user_list = [orders_user['_id']] - for field in ('description', 'title'): + user_list = [orders_user["_id"]] + for field in ("description", "title"): assert order[field] == indata[field] - for field in ('authors', 'generators'): + for field in ("authors", "generators"): assert order[field] == user_list - curr_user = db['users'].find_one({'auth_ids': USERS[response.role]}) + curr_user = db["users"].find_one({"auth_ids": USERS[response.role]}) - assert set(order['editors']) == set([uuid.UUID(entry) for entry in indata[field]]) - assert order['organisation'] == uuid.UUID(indata['organisation']) - elif response.role == 'no-login': + assert set(order["editors"]) == set( + [uuid.UUID(entry) for entry in indata[field]] + ) + assert order["organisation"] == uuid.UUID(indata["organisation"]) + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -416,26 +444,27 @@ def test_add_order_log(mdb): """ db = mdb - indata = {'description': 'Test description', - 'title': 'Test title'} + indata = {"description": "Test description", "title": "Test title"} indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/order/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/order/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - assert '_id' in response.data - assert len(response.data['_id']) == 36 - order = db['orders'].find_one({'_id': uuid.UUID(response.data['_id'])}) - logs = list(db['logs'].find({'data_type': 'order', - 'data._id': uuid.UUID(response.data['_id'])})) + assert "_id" in response.data + assert len(response.data["_id"]) == 36 + order = db["orders"].find_one({"_id": uuid.UUID(response.data["_id"])}) + logs = list( + db["logs"].find( + {"data_type": "order", "data._id": uuid.UUID(response.data["_id"])} + ) + ) assert len(logs) == 1 - assert logs[0]['data'] == order - assert logs[0]['action'] == 'add' - elif response.role == 'no-login': + assert logs[0]["data"] == order + assert logs[0]["action"] == "add" + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -449,38 +478,40 @@ def test_add_order_bad(): Bad requests. """ - indata = {'description': 'Test description', - 'organisation': 'url@bad.se', - 'title': 'Test title'} + indata = { + "description": "Test description", + "organisation": "url@bad.se", + "title": "Test title", + } indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/order/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/order/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 400 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - indata = {'description': 'Test description', - 'authors': [str(uuid.uuid4())], - 'title': 'Test title'} + indata = { + "description": "Test description", + "authors": [str(uuid.uuid4())], + "title": "Test title", + } indata.update(TEST_LABEL) - responses = make_request_all_roles('/api/v1/order/', - method='POST', - data=indata, - ret_json=True) + responses = make_request_all_roles( + "/api/v1/order/", method="POST", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 400 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -488,15 +519,12 @@ def test_add_order_bad(): assert not response.data session = requests.Session() - as_user(session, USERS['data']) - indata = {'_id': str(uuid.uuid4()), - 'title': 'Test title'} + as_user(session, USERS["data"]) + indata = {"_id": str(uuid.uuid4()), "title": "Test title"} indata.update(TEST_LABEL) - response = make_request(session, - '/api/v1/order/', - method='POST', - data=indata, - ret_json=True) + response = make_request( + session, "/api/v1/order/", method="POST", data=indata, ret_json=True + ) assert response.code == 403 assert not response.data @@ -511,26 +539,31 @@ def test_update_order_permissions(mdb): db = mdb - orders_user = db['users'].find_one({'auth_ids': USERS['orders']}) + orders_user = db["users"].find_one({"auth_ids": USERS["orders"]}) - orders = list(db['orders'].aggregate([{'$match': {'editors': orders_user['_id']}}, - {'$sample': {'size': 3}}])) + orders = list( + db["orders"].aggregate( + [{"$match": {"editors": orders_user["_id"]}}, {"$sample": {"size": 3}}] + ) + ) for order in orders: for role in USERS: as_user(session, USERS[role]) - indata = {'title': f'Test title - updated by {role}'} - response = make_request(session, - f'/api/v1/order/{order["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) - if role in ('orders', 'data', 'root'): + indata = {"title": f"Test title - updated by {role}"} + response = make_request( + session, + f'/api/v1/order/{order["_id"]}/', + method="PATCH", + data=indata, + ret_json=True, + ) + if role in ("orders", "data", "root"): assert response.code == 200 assert not response.data - new_order = db['orders'].find_one({'_id': order['_id']}) - assert new_order['title'] == f'Test title - updated by {role}' - elif role == 'no-login': + new_order = db["orders"].find_one({"_id": order["_id"]}) + assert new_order["title"] == f"Test title - updated by {role}" + elif role == "no-login": assert response.code == 401 assert not response.data else: @@ -549,39 +582,50 @@ def test_update_order_data(mdb): db = mdb - orders_user = db['users'].find_one({'auth_ids': USERS['orders']}) + orders_user = db["users"].find_one({"auth_ids": USERS["orders"]}) - orders = list(db['orders'].aggregate([{'$match': {'editors': orders_user['_id']}}, - {'$sample': {'size': 2}}])) + orders = list( + db["orders"].aggregate( + [{"$match": {"editors": orders_user["_id"]}}, {"$sample": {"size": 2}}] + ) + ) assert len(orders) > 0 - as_user(session, USERS['orders']) + as_user(session, USERS["orders"]) for order in orders: - indata = {'title': 'Test title - updated by orders user', - 'description': 'Test description - updated by orders user'} + indata = { + "title": "Test title - updated by orders user", + "description": "Test description - updated by orders user", + } indata.update(TEST_LABEL) - response = make_request(session, - f'/api/v1/order/{order["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + response = make_request( + session, + f'/api/v1/order/{order["_id"]}/', + method="PATCH", + data=indata, + ret_json=True, + ) assert response.code == 200 assert not response.data - new_order = db['orders'].find_one({'_id': order['_id']}) - new_order['_id'] = str(new_order['_id']) - new_order['authors'] = [str(entry) for entry in new_order['authors']] - new_order['generators'] = [str(entry) for entry in new_order['generators']] - new_order['organisation'] = str(new_order['organisation']) - new_order['datasets'] = [str(ds_uuid) for ds_uuid in new_order['datasets']] + new_order = db["orders"].find_one({"_id": order["_id"]}) + new_order["_id"] = str(new_order["_id"]) + new_order["authors"] = [str(entry) for entry in new_order["authors"]] + new_order["generators"] = [str(entry) for entry in new_order["generators"]] + new_order["organisation"] = str(new_order["organisation"]) + new_order["datasets"] = [str(ds_uuid) for ds_uuid in new_order["datasets"]] for field in indata: assert new_order[field] == indata[field] - assert db['logs'].find_one({'data._id': order['_id'], - 'action': 'edit', - 'data_type': 'order', - 'user': orders_user['_id']}) + assert db["logs"].find_one( + { + "data._id": order["_id"], + "action": "edit", + "data_type": "order", + "user": orders_user["_id"], + } + ) def test_update_order_bad(mdb): @@ -592,90 +636,96 @@ def test_update_order_bad(mdb): """ db = mdb - orders_user = db['users'].find_one({'auth_ids': USERS['orders']}) - orders = list(db['orders'].aggregate([{'$match': {'editors': orders_user['_id']}}, - {'$sample': {'size': 2}}])) + orders_user = db["users"].find_one({"auth_ids": USERS["orders"]}) + orders = list( + db["orders"].aggregate( + [{"$match": {"editors": orders_user["_id"]}}, {"$sample": {"size": 2}}] + ) + ) assert len(orders) > 0 for order in orders: - indata = {'description': 'Test description', - 'authors': str(uuid.uuid4()), - 'title': 'Test title'} - responses = make_request_all_roles(f'/api/v1/order/{order["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + indata = { + "description": "Test description", + "authors": str(uuid.uuid4()), + "title": "Test title", + } + responses = make_request_all_roles( + f'/api/v1/order/{order["_id"]}/', method="PATCH", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 400 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - indata = {'description': 'Test description', - 'editors': str(orders_user['_id']), - 'title': 'Test title'} - responses = make_request_all_roles(f'/api/v1/order/{order["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + indata = { + "description": "Test description", + "editors": str(orders_user["_id"]), + "title": "Test title", + } + responses = make_request_all_roles( + f'/api/v1/order/{order["_id"]}/', method="PATCH", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 400 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - indata = {'description': 'Test description', - 'editors': [str(uuid.uuid4())], - 'title': 'Test title'} - responses = make_request_all_roles(f'/api/v1/order/{order["_id"]}/', - method='PATCH', - data=indata, - ret_json=True) + indata = { + "description": "Test description", + "editors": [str(uuid.uuid4())], + "title": "Test title", + } + responses = make_request_all_roles( + f'/api/v1/order/{order["_id"]}/', method="PATCH", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 400 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - for _ in range(2): - indata = {'title': 'Test title'} - responses = make_request_all_roles(f'/api/v1/order/{uuid.uuid4()}/', - method='PATCH', - data=indata, - ret_json=True) + indata = {"title": "Test title"} + responses = make_request_all_roles( + f"/api/v1/order/{uuid.uuid4()}/", method="PATCH", data=indata, ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - indata = {'title': 'Test title'} - responses = make_request_all_roles(f'/api/v1/order/{random_string}/', - method='PATCH', - data=indata, - ret_json=True) + indata = {"title": "Test title"} + responses = make_request_all_roles( + f"/api/v1/order/{random_string}/", + method="PATCH", + data=indata, + ret_json=True, + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -696,53 +746,60 @@ def test_delete_order(mdb): db = mdb - orders_user = db['users'].find_one({'auth_ids': USERS['orders']}) + orders_user = db["users"].find_one({"auth_ids": USERS["orders"]}) - orders = list(db['orders'].find(TEST_LABEL)) + orders = list(db["orders"].find(TEST_LABEL)) if not orders: assert False i = 0 while i < len(orders): for role in USERS: as_user(session, USERS[role]) - response = make_request(session, - f'/api/v1/order/{orders[i]["_id"]}/', - method='DELETE') - if role in ('orders', 'data', 'root'): - if role != 'orders' or orders_user['_id'] in orders[i]['editors']: + response = make_request( + session, f'/api/v1/order/{orders[i]["_id"]}/', method="DELETE" + ) + if role in ("orders", "data", "root"): + if role != "orders" or orders_user["_id"] in orders[i]["editors"]: assert response.code == 200 assert not response.data - assert not db['orders'].find_one({'_id': orders[i]['_id']}) - assert db['logs'].find_one({'data._id': orders[i]['_id'], - 'action': 'delete', - 'data_type': 'order'}) - for dataset_uuid in orders[i]['datasets']: - assert not db['datasets'].find_one({'_id': dataset_uuid}) - assert db['logs'].find_one({'data._id': dataset_uuid, - 'action': 'delete', - 'data_type': 'dataset'}) + assert not db["orders"].find_one({"_id": orders[i]["_id"]}) + assert db["logs"].find_one( + { + "data._id": orders[i]["_id"], + "action": "delete", + "data_type": "order", + } + ) + for dataset_uuid in orders[i]["datasets"]: + assert not db["datasets"].find_one({"_id": dataset_uuid}) + assert db["logs"].find_one( + { + "data._id": dataset_uuid, + "action": "delete", + "data_type": "dataset", + } + ) i += 1 if i >= len(orders): break else: assert response.code == 403 assert not response.data - elif role == 'no-login': + elif role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - as_user(session, USERS['orders']) - response = make_request(session, - '/api/v1/order/', - data={'title': 'tmp'}, - method='POST') + as_user(session, USERS["orders"]) + response = make_request( + session, "/api/v1/order/", data={"title": "tmp"}, method="POST" + ) assert response.code == 200 - response = make_request(session, - f'/api/v1/order/{response.data["_id"]}/', - method='DELETE') + response = make_request( + session, f'/api/v1/order/{response.data["_id"]}/', method="DELETE" + ) assert response.code == 200 assert not response.data @@ -751,18 +808,18 @@ def test_delete_order_bad(): """Attempt bad order delete requests.""" session = requests.Session() - as_user(session, USERS['data']) + as_user(session, USERS["data"]) for _ in range(2): - response = make_request(session, - f'/api/v1/order/{random_string()}/', - method='DELETE') + response = make_request( + session, f"/api/v1/order/{random_string()}/", method="DELETE" + ) assert response.code == 404 assert not response.data for _ in range(2): - response = make_request(session, - f'/api/v1/order/{uuid.uuid4()}/', - method='DELETE') + response = make_request( + session, f"/api/v1/order/{uuid.uuid4()}/", method="DELETE" + ) assert response.code == 404 assert not response.data @@ -774,24 +831,26 @@ def test_list_all_orders(mdb): Check that the number of fields per order is correct. """ db = mdb - nr_orders = db['orders'].count_documents({}) + nr_orders = db["orders"].count_documents({}) - responses = make_request_all_roles('/api/v1/order/', ret_json=True) + responses = make_request_all_roles("/api/v1/order/", ret_json=True) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 200 - assert len(response.data['orders']) == nr_orders - assert set(response.data['orders'][0].keys()) == {'title', - '_id', - 'tags', - 'properties'} - elif response.role == 'no-login': + assert len(response.data["orders"]) == nr_orders + assert set(response.data["orders"][0].keys()) == { + "title", + "_id", + "tags", + "properties", + } + elif response.role == "no-login": assert response.code == 401 assert not response.data - elif response.role == 'orders': + elif response.role == "orders": assert response.code == 200 - assert len(response.data['orders']) == 0 + assert len(response.data["orders"]) == 0 else: assert response.code == 403 diff --git a/backend/tests/test_permissions.py b/backend/tests/test_permissions.py index baee0a71..d4837243 100644 --- a/backend/tests/test_permissions.py +++ b/backend/tests/test_permissions.py @@ -11,19 +11,21 @@ def test_request_no_permissions_required(): """Request target with no permission requirements.""" - responses = helpers.make_request_all_roles('/api/v1/developer/hello', ret_json=True) + responses = helpers.make_request_all_roles("/api/v1/developer/hello", ret_json=True) for response in responses: assert response.code == 200 - assert response.data == {'test': "success"} + assert response.data == {"test": "success"} def test_request_login_required(): """Request target with no permission requirements apart from being logged in.""" - responses = helpers.make_request_all_roles('/api/v1/developer/loginhello', ret_json=True) + responses = helpers.make_request_all_roles( + "/api/v1/developer/loginhello", ret_json=True + ) for response in responses: - if response.role != 'no-login': + if response.role != "no-login": assert response.code == 200 - assert response.data == {'test': "success"} + assert response.data == {"test": "success"} else: assert response.code == 401 assert not response.data @@ -31,11 +33,13 @@ def test_request_login_required(): def test_request_permission_orders_self(): """Request requiring ORDERS permissions.""" - responses = helpers.make_request_all_roles('/api/v1/developer/hello/ORDERS', ret_json=True) + responses = helpers.make_request_all_roles( + "/api/v1/developer/hello/ORDERS", ret_json=True + ) for response in responses: - if response.role in ('orders', 'data', 'root'): + if response.role in ("orders", "data", "root"): assert response.code == 200 - assert response.data == {'test': "success"} + assert response.data == {"test": "success"} else: assert response.code == 403 assert not response.data @@ -43,11 +47,13 @@ def test_request_permission_orders_self(): def test_request_permission_owners_read(): """Request requiring OWNERS_READ permissions.""" - responses = helpers.make_request_all_roles('/api/v1/developer/hello/OWNERS_READ', ret_json=True) + responses = helpers.make_request_all_roles( + "/api/v1/developer/hello/OWNERS_READ", ret_json=True + ) for response in responses: - if response.role in ('owners', 'data', 'root'): + if response.role in ("owners", "data", "root"): assert response.code == 200 - assert response.data == {'test': "success"} + assert response.data == {"test": "success"} else: assert response.code == 403 assert not response.data @@ -55,11 +61,13 @@ def test_request_permission_owners_read(): def test_request_permission_user_management(): """Request requiring USER_MANAGEMENT permissions.""" - responses = helpers.make_request_all_roles('/api/v1/developer/hello/USER_MANAGEMENT', ret_json=True) + responses = helpers.make_request_all_roles( + "/api/v1/developer/hello/USER_MANAGEMENT", ret_json=True + ) for response in responses: - if response.role in ('users', 'root'): + if response.role in ("users", "root"): assert response.code == 200 - assert response.data == {'test': "success"} + assert response.data == {"test": "success"} else: assert response.code == 403 assert not response.data @@ -67,11 +75,13 @@ def test_request_permission_user_management(): def test_request_permission_data_management(): """Request requiring DATA_MANAGEMENT permissions.""" - responses = helpers.make_request_all_roles('/api/v1/developer/hello/DATA_MANAGEMENT', ret_json=True) + responses = helpers.make_request_all_roles( + "/api/v1/developer/hello/DATA_MANAGEMENT", ret_json=True + ) for response in responses: - if response.role in ('data', 'root'): + if response.role in ("data", "root"): assert response.code == 200 - assert response.data == {'test': "success"} + assert response.data == {"test": "success"} else: assert response.code == 403 assert not response.data @@ -79,43 +89,46 @@ def test_request_permission_data_management(): def test_csrf(): """Perform POST, POST and DELETE requests to confirm that CSRF works correctly.""" - - for method in ('POST', 'PATCH', 'POST', 'DELETE'): - responses = helpers.make_request_all_roles('/api/v1/developer/csrftest', - method=method, - set_csrf=False, - ret_json=True) + + for method in ("POST", "PATCH", "POST", "DELETE"): + responses = helpers.make_request_all_roles( + "/api/v1/developer/csrftest", method=method, set_csrf=False, ret_json=True + ) for response in responses: assert response.code == 400 assert not response.data - responses = helpers.make_request_all_roles('/api/v1/developer/csrftest', - method=method, - ret_json=True) + responses = helpers.make_request_all_roles( + "/api/v1/developer/csrftest", method=method, ret_json=True + ) for response in responses: assert response.code == 200 - assert response.data == {'test': "success"} + assert response.data == {"test": "success"} def test_api_key_auth(): """Request target with login requirment using an API key""" - response = requests.get(helpers.BASE_URL + '/api/v1/developer/loginhello', - headers={'X-API-Key': '0', - 'X-API-User': 'base::testers'}) + response = requests.get( + helpers.BASE_URL + "/api/v1/developer/loginhello", + headers={"X-API-Key": "0", "X-API-User": "base::testers"}, + ) assert response.status_code == 200 - assert json.loads(response.text) == {'test': 'success'} - response = requests.get(helpers.BASE_URL + '/api/v1/developer/loginhello', - headers={'X-API-Key': '0', - 'X-API-User': 'root::testers'}) + assert json.loads(response.text) == {"test": "success"} + response = requests.get( + helpers.BASE_URL + "/api/v1/developer/loginhello", + headers={"X-API-Key": "0", "X-API-User": "root::testers"}, + ) assert response.status_code == 401 assert not response.text - response = requests.get(helpers.BASE_URL + '/api/v1/developer/loginhello', - headers={'X-API-Key': 'asd', - 'X-API-User': 'root::testers'}) + response = requests.get( + helpers.BASE_URL + "/api/v1/developer/loginhello", + headers={"X-API-Key": "asd", "X-API-User": "root::testers"}, + ) assert response.status_code == 401 assert not response.text - response = requests.get(helpers.BASE_URL + '/api/v1/developer/loginhello', - headers={'X-API-Key': '0', - 'X-API-User': 'asd'}) + response = requests.get( + helpers.BASE_URL + "/api/v1/developer/loginhello", + headers={"X-API-Key": "0", "X-API-User": "asd"}, + ) assert response.status_code == 401 assert not response.text diff --git a/backend/tests/test_users.py b/backend/tests/test_users.py index 590d5619..b98d4204 100644 --- a/backend/tests/test_users.py +++ b/backend/tests/test_users.py @@ -3,7 +3,15 @@ import requests import uuid -from helpers import make_request, as_user, make_request_all_roles, USERS, mdb, random_string +from helpers import ( + make_request, + as_user, + make_request_all_roles, + USERS, + mdb, + random_string, +) + # pylint: disable=redefined-outer-name @@ -13,13 +21,12 @@ def test_list_users(mdb): Assert that USER_MANAGEMENT is required. """ - responses = make_request_all_roles('/api/v1/user/', - ret_json=True) + responses = make_request_all_roles("/api/v1/user/", ret_json=True) for response in responses: - if response.role in ('users', 'root', 'orders'): + if response.role in ("users", "root", "orders"): assert response.code == 200 - assert len(response.data['users']) == mdb['users'].count_documents({}) - elif response.role == 'no-login': + assert len(response.data["users"]) == mdb["users"].count_documents({}) + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -29,13 +36,12 @@ def test_list_users(mdb): def test_list_info(): """Retrieve info about current user.""" - responses = make_request_all_roles('/api/v1/user/me/', - ret_json=True) + responses = make_request_all_roles("/api/v1/user/me/", ret_json=True) for response in responses: assert response.code == 200 - assert len(response.data['user']) == 9 - if response.role != 'no-login': - assert response.data['user']['name'] == f'{response.role.capitalize()}' + assert len(response.data["user"]) == 9 + if response.role != "no-login": + assert response.data["user"]["name"] == f"{response.role.capitalize()}" def test_update_current_user(mdb): @@ -45,40 +51,35 @@ def test_update_current_user(mdb): indata = {} for user in USERS: as_user(session, USERS[user]) - user_info = mdb['users'].find_one({'auth_ids': USERS[user]}) - response = make_request(session, - '/api/v1/user/me/', - ret_json=True, - method='PATCH', - data=indata) - if user != 'no-login': + user_info = mdb["users"].find_one({"auth_ids": USERS[user]}) + response = make_request( + session, "/api/v1/user/me/", ret_json=True, method="PATCH", data=indata + ) + if user != "no-login": assert response.code == 200 else: assert response.code == 401 assert not response.data - new_user_info = mdb['users'].find_one({'auth_ids': USERS[user]}) + new_user_info = mdb["users"].find_one({"auth_ids": USERS[user]}) assert user_info == new_user_info - indata = {'affiliation': 'Updated University', - 'name': 'Updated name'} + indata = {"affiliation": "Updated University", "name": "Updated name"} session = requests.Session() for user in USERS: as_user(session, USERS[user]) - user_info = mdb['users'].find_one({'auth_ids': USERS[user]}) - response = make_request(session, - '/api/v1/user/me/', - ret_json=True, - method='PATCH', - data=indata) - if user != 'no-login': + user_info = mdb["users"].find_one({"auth_ids": USERS[user]}) + response = make_request( + session, "/api/v1/user/me/", ret_json=True, method="PATCH", data=indata + ) + if user != "no-login": assert response.code == 200 assert not response.data - new_user_info = mdb['users'].find_one({'auth_ids': USERS[user]}) + new_user_info = mdb["users"].find_one({"auth_ids": USERS[user]}) for key in new_user_info: if key in indata.keys(): assert new_user_info[key] == indata[key] else: - mdb['users'].update_one(new_user_info, {'$set': user_info}) + mdb["users"].update_one(new_user_info, {"$set": user_info}) else: assert response.code == 401 assert not response.data @@ -86,61 +87,56 @@ def test_update_current_user(mdb): def test_update_current_user_bad(): """Update the info about the current user.""" - indata = {'_id': str(uuid.uuid4())} - responses = make_request_all_roles('/api/v1/user/me/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"_id": str(uuid.uuid4())} + responses = make_request_all_roles( + "/api/v1/user/me/", ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - indata = {'api_key': uuid.uuid4().hex} - responses = make_request_all_roles('/api/v1/user/me/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"api_key": uuid.uuid4().hex} + responses = make_request_all_roles( + "/api/v1/user/me/", ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - indata = {'auth_ids': [uuid.uuid4().hex]} - responses = make_request_all_roles('/api/v1/user/me/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"auth_ids": [uuid.uuid4().hex]} + responses = make_request_all_roles( + "/api/v1/user/me/", ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - indata = {'email': 'email@example.com'} - responses = make_request_all_roles('/api/v1/user/me/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"email": "email@example.com"} + responses = make_request_all_roles( + "/api/v1/user/me/", ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - indata = {'permissions': ['USER_MANAGEMENT', 'DATA_MANAGEMENT']} - responses = make_request_all_roles('/api/v1/user/me/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"permissions": ["USER_MANAGEMENT", "DATA_MANAGEMENT"]} + responses = make_request_all_roles( + "/api/v1/user/me/", ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 else: assert response.code == 403 @@ -149,42 +145,44 @@ def test_update_current_user_bad(): def test_update_user(mdb): """Update the info for a user.""" - user_info = mdb['users'].find_one({'auth_ids': USERS['base']}) + user_info = mdb["users"].find_one({"auth_ids": USERS["base"]}) indata = {} - responses = make_request_all_roles(f'/api/v1/user/{user_info["_id"]}/', - ret_json=True, - method='PATCH', - data=indata) + responses = make_request_all_roles( + f'/api/v1/user/{user_info["_id"]}/', ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role in ('users', 'root'): + if response.role in ("users", "root"): assert response.code == 200 - new_user_info = mdb['users'].find_one({'auth_ids': {'$in': user_info['auth_ids']}}) + new_user_info = mdb["users"].find_one( + {"auth_ids": {"$in": user_info["auth_ids"]}} + ) assert user_info == new_user_info - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - indata = {'affiliation': 'Updated University', - 'name': 'Updated name'} + indata = {"affiliation": "Updated University", "name": "Updated name"} session = requests.session() for user_type in USERS: as_user(session, USERS[user_type]) - response = make_request(session, - f'/api/v1/user/{user_info["_id"]}/', - ret_json=True, - method='PATCH', - data=indata) - if user_type in ('users', 'root'): + response = make_request( + session, + f'/api/v1/user/{user_info["_id"]}/', + ret_json=True, + method="PATCH", + data=indata, + ) + if user_type in ("users", "root"): assert response.code == 200 assert not response.data - new_user_info = mdb['users'].find_one({'auth_ids': user_info['auth_ids']}) + new_user_info = mdb["users"].find_one({"auth_ids": user_info["auth_ids"]}) for key in indata: assert new_user_info[key] == indata[key] - mdb['users'].update_one(new_user_info, {'$set': user_info}) - elif user_type == 'no-login': + mdb["users"].update_one(new_user_info, {"$set": user_info}) + elif user_type == "no-login": assert response.code == 401 assert not response.data else: @@ -198,130 +196,122 @@ def test_update_user_bad(mdb): Bad requests. """ - user_info = mdb['users'].find_one({'auth_ids': USERS['base']}) + user_info = mdb["users"].find_one({"auth_ids": USERS["base"]}) - indata = {'_id': str(uuid.uuid4())} - responses = make_request_all_roles(f'/api/v1/user/{user_info["_id"]}/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"_id": str(uuid.uuid4())} + responses = make_request_all_roles( + f'/api/v1/user/{user_info["_id"]}/', ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - indata = {'api_key': uuid.uuid4().hex} - responses = make_request_all_roles(f'/api/v1/user/{user_info["_id"]}/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"api_key": uuid.uuid4().hex} + responses = make_request_all_roles( + f'/api/v1/user/{user_info["_id"]}/', ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - indata = {'email': 'bad@email'} - responses = make_request_all_roles(f'/api/v1/user/{user_info["_id"]}/', - ret_json=True, - method='PATCH', - data=indata) + indata = {"email": "bad@email"} + responses = make_request_all_roles( + f'/api/v1/user/{user_info["_id"]}/', ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role in ('users', 'root'): + if response.role in ("users", "root"): assert response.code == 400 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data indata = {} - responses = make_request_all_roles(f'/api/v1/user/{uuid.uuid4()}/', - ret_json=True, - method='PATCH', - data=indata) + responses = make_request_all_roles( + f"/api/v1/user/{uuid.uuid4()}/", ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role in ('users', 'root'): + if response.role in ("users", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data indata = {} - responses = make_request_all_roles(f'/api/v1/user/{random_string()}/', - ret_json=True, - method='PATCH', - data=indata) + responses = make_request_all_roles( + f"/api/v1/user/{random_string()}/", ret_json=True, method="PATCH", data=indata + ) for response in responses: - if response.role in ('users', 'root'): + if response.role in ("users", "root"): assert response.code == 404 - elif response.role == 'no-login': + elif response.role == "no-login": assert response.code == 401 else: assert response.code == 403 assert not response.data - def test_add_user(mdb): """Add a user.""" - indata = {'email': 'new_user@added.example.com'} + indata = {"email": "new_user@added.example.com"} session = requests.Session() for role in USERS: as_user(session, USERS[role]) - response = make_request(session, - '/api/v1/user/', - ret_json=True, - method='POST', - data=indata) - if role in ('users', 'root', 'orders'): + response = make_request( + session, "/api/v1/user/", ret_json=True, method="POST", data=indata + ) + if role in ("users", "root", "orders"): assert response.code == 200 - assert '_id' in response.data - new_user_info = mdb['users'].find_one({'_id': uuid.UUID(response.data['_id'])}) - assert indata['email'] == new_user_info['email'] - indata['email'] = 'new_' + indata['email'] - elif role == 'no-login': + assert "_id" in response.data + new_user_info = mdb["users"].find_one( + {"_id": uuid.UUID(response.data["_id"])} + ) + assert indata["email"] == new_user_info["email"] + indata["email"] = "new_" + indata["email"] + elif role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 403 assert not response.data - indata = {'affiliation': 'Added University', - 'name': 'Added name', - 'email': 'user2@added.example.com', - 'permissions': ['ORDERS']} + indata = { + "affiliation": "Added University", + "name": "Added name", + "email": "user2@added.example.com", + "permissions": ["ORDERS"], + } session = requests.session() - as_user(session, USERS['orders']) - response = make_request(session, - '/api/v1/user/', - ret_json=True, - method='POST', - data=indata) + as_user(session, USERS["orders"]) + response = make_request( + session, "/api/v1/user/", ret_json=True, method="POST", data=indata + ) assert response.code == 403 - as_user(session, USERS['root']) - response = make_request(session, - '/api/v1/user/', - ret_json=True, - method='POST', - data=indata) + as_user(session, USERS["root"]) + response = make_request( + session, "/api/v1/user/", ret_json=True, method="POST", data=indata + ) assert response.code == 200 - assert '_id' in response.data - new_user_info = mdb['users'].find_one({'_id': uuid.UUID(response.data['_id'])}) + assert "_id" in response.data + new_user_info = mdb["users"].find_one({"_id": uuid.UUID(response.data["_id"])}) for key in indata: assert new_user_info[key] == indata[key] def test_delete_user(mdb): """Test deleting users (added when testing to add users)""" - re_users = re.compile('@added.example.com') - users = list(mdb['users'].find({'email': re_users}, {'_id': 1})) + re_users = re.compile("@added.example.com") + users = list(mdb["users"].find({"email": re_users}, {"_id": 1})) if not users: assert False session = requests.Session() @@ -329,20 +319,24 @@ def test_delete_user(mdb): while i < len(users): for role in USERS: as_user(session, USERS[role]) - response = make_request(session, - f'/api/v1/user/{users[i]["_id"]}/', - method='DELETE') - if role in ('users', 'root'): + response = make_request( + session, f'/api/v1/user/{users[i]["_id"]}/', method="DELETE" + ) + if role in ("users", "root"): assert response.code == 200 assert not response.data - assert not mdb['users'].find_one({'_id': users[i]['_id']}) - assert mdb['logs'].find_one({'data._id': users[i]['_id'], - 'action': 'delete', - 'data_type': 'user'}) + assert not mdb["users"].find_one({"_id": users[i]["_id"]}) + assert mdb["logs"].find_one( + { + "data._id": users[i]["_id"], + "action": "delete", + "data_type": "user", + } + ) i += 1 if i >= len(users): break - elif role == 'no-login': + elif role == "no-login": assert response.code == 401 assert not response.data else: @@ -352,46 +346,47 @@ def test_delete_user(mdb): def test_key_reset(mdb): """Test generation of new API keys""" - mod_user = {'auth_ids': 'facility18::local'} + mod_user = {"auth_ids": "facility18::local"} mod_user_info = mdb.users.find_one(mod_user) session = requests.Session() for i, userid in enumerate(USERS): as_user(session, USERS[userid]) - response = make_request(session, - '/api/v1/user/me/apikey/', - method='POST') - if userid == 'no-login': + response = make_request(session, "/api/v1/user/me/apikey/", method="POST") + if userid == "no-login": assert response.code == 401 assert not response.data continue assert response.code == 200 - new_key = response.data['key'] - response = make_request(session, - '/api/v1/login/apikey/', - data = {'api-user': USERS[userid], - 'api-key': new_key}, - method='POST') + new_key = response.data["key"] + response = make_request( + session, + "/api/v1/login/apikey/", + data={"api-user": USERS[userid], "api-key": new_key}, + method="POST", + ) assert response.code == 200 assert not response.data - mdb.users.update_one({'auth_ids': userid}, {'$set': {'api_salt': 'abc', - 'api_key': str(i-1)}}) - - response = make_request(session, - f'/api/v1/user/{mod_user_info["_id"]}/apikey/', - method='POST') - if userid not in ('users', 'root'): + mdb.users.update_one( + {"auth_ids": userid}, {"$set": {"api_salt": "abc", "api_key": str(i - 1)}} + ) + + response = make_request( + session, f'/api/v1/user/{mod_user_info["_id"]}/apikey/', method="POST" + ) + if userid not in ("users", "root"): assert response.code == 403 assert not response.data else: assert response.code == 200 - new_key = response.data['key'] - response = make_request(session, - '/api/v1/login/apikey/', - data = {'api-user': mod_user['auth_ids'], - 'api-key': new_key}, - method='POST') + new_key = response.data["key"] + response = make_request( + session, + "/api/v1/login/apikey/", + data={"api-user": mod_user["auth_ids"], "api-key": new_key}, + method="POST", + ) assert response.code == 200 assert not response.data @@ -402,14 +397,13 @@ def test_get_user_logs_permissions(mdb): Assert that USER_MANAGEMENT or actual user is required. """ - user_uuid = mdb['users'].find_one({'auth_ids': USERS['base']}, {'_id': 1})['_id'] - responses = make_request_all_roles(f'/api/v1/user/{user_uuid}/log/', - ret_json=True) + user_uuid = mdb["users"].find_one({"auth_ids": USERS["base"]}, {"_id": 1})["_id"] + responses = make_request_all_roles(f"/api/v1/user/{user_uuid}/log/", ret_json=True) for response in responses: - if response.role in ('base', 'users', 'root'): + if response.role in ("base", "users", "root"): assert response.code == 200 - assert 'logs' in response.data - elif response.role == 'no-login': + assert "logs" in response.data + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -424,14 +418,16 @@ def test_get_user_logs(mdb): Confirm that the logs contain only the intended fields. """ session = requests.session() - users = mdb['users'].aggregate([{'$sample': {'size': 2}}]) + users = mdb["users"].aggregate([{"$sample": {"size": 2}}]) for user in users: - logs = list(mdb['logs'].find({'data_type': 'user', 'data._id': user['_id']})) - as_user(session, USERS['users']) - response = make_request(session, f'/api/v1/user/{user["_id"]}/log/', ret_json=True) - assert response.data['dataType'] == 'user' - assert response.data['entryId'] == str(user['_id']) - assert len(response.data['logs']) == len(logs) + logs = list(mdb["logs"].find({"data_type": "user", "data._id": user["_id"]})) + as_user(session, USERS["users"]) + response = make_request( + session, f'/api/v1/user/{user["_id"]}/log/', ret_json=True + ) + assert response.data["dataType"] == "user" + assert response.data["entryId"] == str(user["_id"]) + assert len(response.data["logs"]) == len(logs) assert response.code == 200 @@ -441,15 +437,14 @@ def test_get_current_user_logs(): Should return logs for all logged in users. """ - responses = make_request_all_roles('/api/v1/user/me/log/', - ret_json=True) + responses = make_request_all_roles("/api/v1/user/me/log/", ret_json=True) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 200 - assert 'logs' in response.data + assert "logs" in response.data def test_get_user_actions_access(mdb): @@ -458,14 +453,15 @@ def test_get_user_actions_access(mdb): Assert that USER_MANAGEMENT or actual user is required. """ - user_uuid = mdb['users'].find_one({'auth_ids': USERS['base']}, {'_id': 1})['_id'] - responses = make_request_all_roles(f'/api/v1/user/{user_uuid}/actions/', - ret_json=True) + user_uuid = mdb["users"].find_one({"auth_ids": USERS["base"]}, {"_id": 1})["_id"] + responses = make_request_all_roles( + f"/api/v1/user/{user_uuid}/actions/", ret_json=True + ) for response in responses: - if response.role in ('base', 'users', 'root'): + if response.role in ("base", "users", "root"): assert response.code == 200 - assert 'logs' in response.data - elif response.role == 'no-login': + assert "logs" in response.data + elif response.role == "no-login": assert response.code == 401 assert not response.data else: @@ -479,12 +475,11 @@ def test_get_current_user_actions(): Should return logs for all logged in users. """ - responses = make_request_all_roles('/api/v1/user/me/actions/', - ret_json=True) + responses = make_request_all_roles("/api/v1/user/me/actions/", ret_json=True) for response in responses: - if response.role == 'no-login': + if response.role == "no-login": assert response.code == 401 assert not response.data else: assert response.code == 200 - assert 'logs' in response.data + assert "logs" in response.data diff --git a/backend/user.py b/backend/user.py index 9c5c6566..4a4b2a80 100644 --- a/backend/user.py +++ b/backend/user.py @@ -20,14 +20,16 @@ import structure import utils -blueprint = flask.Blueprint('user', __name__) # pylint: disable=invalid-name +blueprint = flask.Blueprint("user", __name__) # pylint: disable=invalid-name -PERMISSIONS = {'ORDERS': ('ORDERS', 'USER_ADD', 'USER_SEARCH'), - 'OWNERS_READ': ('OWNERS_READ',), - 'USER_ADD': ('USER_ADD',), - 'USER_SEARCH': ('USER_SEARCH',), - 'USER_MANAGEMENT': ('USER_MANAGEMENT', 'USER_ADD', 'USER_SEARCH'), - 'DATA_MANAGEMENT': ('ORDERS', 'OWNERS_READ', 'DATA_MANAGEMENT')} +PERMISSIONS = { + "ORDERS": ("ORDERS", "USER_ADD", "USER_SEARCH"), + "OWNERS_READ": ("OWNERS_READ",), + "USER_ADD": ("USER_ADD",), + "USER_SEARCH": ("USER_SEARCH",), + "USER_MANAGEMENT": ("USER_MANAGEMENT", "USER_ADD", "USER_SEARCH"), + "DATA_MANAGEMENT": ("ORDERS", "OWNERS_READ", "DATA_MANAGEMENT"), +} # Decorators @@ -37,22 +39,24 @@ def login_required(func): Otherwise abort with status 401 Unauthorized. """ + @functools.wraps(func) def wrap(*args, **kwargs): if not flask.g.current_user: flask.abort(status=401) return func(*args, **kwargs) + return wrap # requests -@blueprint.route('/permissions/') +@blueprint.route("/permissions/") def get_permission_info(): """Get a list of all permission types.""" - return utils.response_json({'permissions': list(PERMISSIONS.keys())}) + return utils.response_json({"permissions": list(PERMISSIONS.keys())}) -@blueprint.route('/') +@blueprint.route("/") @login_required def list_users(): """ @@ -60,22 +64,21 @@ def list_users(): Admin access should be required. """ - if not has_permission('USER_SEARCH'): + if not has_permission("USER_SEARCH"): flask.abort(403) - fields = {'api_key': 0, - 'api_salt': 0} + fields = {"api_key": 0, "api_salt": 0} - if not has_permission('USER_MANAGEMENT'): - fields['auth_ids'] = 0 - fields['permissions'] = 0 + if not has_permission("USER_MANAGEMENT"): + fields["auth_ids"] = 0 + fields["permissions"] = 0 - result = tuple(flask.g.db['users'].find(projection=fields)) + result = tuple(flask.g.db["users"].find(projection=fields)) - return utils.response_json({'users': result}) + return utils.response_json({"users": result}) -@blueprint.route('/structure/', methods=['GET']) +@blueprint.route("/structure/", methods=["GET"]) def get_user_data_structure(): """ Get an empty user entry. @@ -84,12 +87,12 @@ def get_user_data_structure(): flask.Response: JSON structure with a list of users. """ empty_user = structure.user() - empty_user['_id'] = '' - return utils.response_json({'user': empty_user}) + empty_user["_id"] = "" + return utils.response_json({"user": empty_user}) # requests -@blueprint.route('/me/') +@blueprint.route("/me/") def get_current_user_info(): """ List basic information about the current user. @@ -98,25 +101,27 @@ def get_current_user_info(): flask.Response: json structure for the user """ data = flask.g.current_user - outstructure = {'_id': '', - 'affiliation': '', - 'auth_ids': [], - 'email': '', - 'contact': '', - 'name': '', - 'orcid': '', - 'permissions': '', - 'url': ''} + outstructure = { + "_id": "", + "affiliation": "", + "auth_ids": [], + "email": "", + "contact": "", + "name": "", + "orcid": "", + "permissions": "", + "url": "", + } if data: for field in outstructure: if field in data: outstructure[field] = data[field] - return utils.response_json({'user': outstructure}) + return utils.response_json({"user": outstructure}) # requests -@blueprint.route('/me/apikey/', methods=['POST']) -@blueprint.route('//apikey/', methods=['POST']) +@blueprint.route("/me/apikey/", methods=["POST"]) +@blueprint.route("//apikey/", methods=["POST"]) @login_required def gen_new_api_key(identifier: str = None): """ @@ -131,32 +136,37 @@ def gen_new_api_key(identifier: str = None): if not identifier: user_data = flask.g.current_user else: - if not has_permission('USER_MANAGEMENT'): + if not has_permission("USER_MANAGEMENT"): flask.abort(403) try: user_uuid = utils.str_to_uuid(identifier) except ValueError: flask.abort(status=404) - if not (user_data := flask.g.db['users'].find_one({'_id': user_uuid})): # pylint: disable=superfluous-parens + if not ( + user_data := flask.g.db["users"].find_one({"_id": user_uuid}) + ): # pylint: disable=superfluous-parens flask.abort(status=404) apikey = utils.gen_api_key() new_hash = utils.gen_api_key_hash(apikey.key, apikey.salt) - new_values = {'api_key': new_hash, 'api_salt': apikey.salt} + new_values = {"api_key": new_hash, "api_salt": apikey.salt} user_data.update(new_values) - result = flask.g.db['users'].update_one({'_id': user_data['_id']}, - {'$set': new_values}) + result = flask.g.db["users"].update_one( + {"_id": user_data["_id"]}, {"$set": new_values} + ) if not result.acknowledged: - flask.current_app.logger.error('Updating API key for user %s failed', user_data['_id']) + flask.current_app.logger.error( + "Updating API key for user %s failed", user_data["_id"] + ) flask.Response(status=500) else: - utils.make_log('user', 'edit', 'New API key', user_data) + utils.make_log("user", "edit", "New API key", user_data) - return utils.response_json({'key': apikey.key}) + return utils.response_json({"key": apikey.key}) -@blueprint.route('//', methods=['GET']) +@blueprint.route("//", methods=["GET"]) @login_required def get_user_data(identifier: str): """ @@ -168,7 +178,7 @@ def get_user_data(identifier: str): Returns: flask.Response: Information about the user as json. """ - if not has_permission('USER_MANAGEMENT'): + if not has_permission("USER_MANAGEMENT"): flask.abort(403) try: @@ -176,17 +186,19 @@ def get_user_data(identifier: str): except ValueError: flask.abort(status=404) - if not (user_info := flask.g.db['users'].find_one({'_id': user_uuid})): # pylint: disable=superfluous-parens + if not ( + user_info := flask.g.db["users"].find_one({"_id": user_uuid}) + ): # pylint: disable=superfluous-parens flask.abort(status=404) # The hash and salt should never leave the system - del user_info['api_key'] - del user_info['api_salt'] + del user_info["api_key"] + del user_info["api_salt"] - return utils.response_json({'user': user_info}) + return utils.response_json({"user": user_info}) -@blueprint.route('/', methods=['POST']) +@blueprint.route("/", methods=["POST"]) @login_required def add_user(): """ @@ -195,7 +207,7 @@ def add_user(): Returns: flask.Response: Information about the user as json. """ - if not has_permission('USER_ADD'): + if not has_permission("USER_ADD"): flask.abort(403) new_user = structure.user() @@ -203,41 +215,40 @@ def add_user(): indata = flask.json.loads(flask.request.data) except json.decoder.JSONDecodeError: flask.abort(status=400) - validation = utils.basic_check_indata(indata, new_user, ('_id', - 'api_key', - 'api_salt', - 'auth_ids')) + validation = utils.basic_check_indata( + indata, new_user, ("_id", "api_key", "api_salt", "auth_ids") + ) if not validation.result: flask.abort(status=validation.status) - if 'email' not in indata: - flask.current_app.logger.debug('Email must be set') + if "email" not in indata: + flask.current_app.logger.debug("Email must be set") flask.abort(status=400) - old_user = flask.g.db['users'].find_one({'email': indata['email']}) + old_user = flask.g.db["users"].find_one({"email": indata["email"]}) if old_user: - flask.current_app.logger.debug('User already exists') + flask.current_app.logger.debug("User already exists") flask.abort(status=400) - if not has_permission('USER_MANAGEMENT') and 'permissions' in indata: - flask.current_app.logger.debug('USER_MANAGEMENT required for permissions') + if not has_permission("USER_MANAGEMENT") and "permissions" in indata: + flask.current_app.logger.debug("USER_MANAGEMENT required for permissions") flask.abort(403) new_user.update(indata) - new_user['auth_ids'] = [f'{new_user["_id"]}::local'] + new_user["auth_ids"] = [f'{new_user["_id"]}::local'] - result = flask.g.db['users'].insert_one(new_user) + result = flask.g.db["users"].insert_one(new_user) if not result.acknowledged: - flask.current_app.logger.error('User Addition failed: %s', new_user['email']) + flask.current_app.logger.error("User Addition failed: %s", new_user["email"]) flask.Response(status=500) else: - utils.make_log('user', 'add', 'User added by admin', new_user) + utils.make_log("user", "add", "User added by admin", new_user) - return utils.response_json({'_id': result.inserted_id}) + return utils.response_json({"_id": result.inserted_id}) -@blueprint.route('//', methods=['DELETE']) +@blueprint.route("//", methods=["DELETE"]) @login_required def delete_user(identifier: str): """ @@ -249,7 +260,7 @@ def delete_user(identifier: str): Returns: flask.Response: Response code. """ - if not has_permission('USER_MANAGEMENT'): + if not has_permission("USER_MANAGEMENT"): flask.abort(403) try: @@ -257,20 +268,20 @@ def delete_user(identifier: str): except ValueError: flask.abort(status=404) - if not flask.g.db['users'].find_one({'_id': user_uuid}): + if not flask.g.db["users"].find_one({"_id": user_uuid}): flask.abort(status=404) - result = flask.g.db['users'].delete_one({'_id': user_uuid}) + result = flask.g.db["users"].delete_one({"_id": user_uuid}) if not result.acknowledged: - flask.current_app.logger.error('User deletion failed: %s', user_uuid) + flask.current_app.logger.error("User deletion failed: %s", user_uuid) flask.Response(status=500) else: - utils.make_log('user', 'delete', 'User delete', {'_id': user_uuid}) + utils.make_log("user", "delete", "User delete", {"_id": user_uuid}) return flask.Response(status=200) -@blueprint.route('/me/', methods=['PATCH']) +@blueprint.route("/me/", methods=["PATCH"]) @login_required def update_current_user_info(): """ @@ -285,29 +296,29 @@ def update_current_user_info(): indata = flask.json.loads(flask.request.data) except json.decoder.JSONDecodeError: flask.abort(status=400) - validation = utils.basic_check_indata(indata, user_data, ('_id', - 'api_key', - 'api_salt', - 'auth_ids', - 'email', - 'permissions')) + validation = utils.basic_check_indata( + indata, + user_data, + ("_id", "api_key", "api_salt", "auth_ids", "email", "permissions"), + ) if not validation[0]: flask.abort(status=validation[1]) user_data.update(indata) - result = flask.g.db['users'].update_one({'_id': user_data['_id']}, - {'$set': user_data}) + result = flask.g.db["users"].update_one( + {"_id": user_data["_id"]}, {"$set": user_data} + ) if not result.acknowledged: - flask.current_app.logger.error('User self-update failed: %s', indata) + flask.current_app.logger.error("User self-update failed: %s", indata) flask.Response(status=500) else: - utils.make_log('user', 'edit', 'User self-updated', user_data) + utils.make_log("user", "edit", "User self-updated", user_data) return flask.Response(status=200) -@blueprint.route('//', methods=['PATCH']) +@blueprint.route("//", methods=["PATCH"]) @login_required def update_user_info(identifier: str): """ @@ -319,7 +330,7 @@ def update_user_info(identifier: str): Returns: flask.Response: Response code. """ - if not has_permission('USER_MANAGEMENT'): + if not has_permission("USER_MANAGEMENT"): flask.abort(403) try: @@ -327,25 +338,26 @@ def update_user_info(identifier: str): except ValueError: flask.abort(status=404) - if not (user_data := flask.g.db['users'].find_one({'_id': user_uuid})): # pylint: disable=superfluous-parens + if not ( + user_data := flask.g.db["users"].find_one({"_id": user_uuid}) + ): # pylint: disable=superfluous-parens flask.abort(status=404) try: indata = flask.json.loads(flask.request.data) except json.decoder.JSONDecodeError: flask.abort(status=400) - validation = utils.basic_check_indata(indata, user_data, ('_id', - 'api_key', - 'api_salt', - 'auth_ids')) + validation = utils.basic_check_indata( + indata, user_data, ("_id", "api_key", "api_salt", "auth_ids") + ) if not validation.result: flask.abort(status=validation.status) - if 'email' in indata: - old_user = flask.g.db['users'].find_one({'email': indata['email']}) - if old_user and old_user['_id'] != user_data['_id']: - flask.current_app.logger.debug('User already exists') + if "email" in indata: + old_user = flask.g.db["users"].find_one({"email": indata["email"]}) + if old_user and old_user["_id"] != user_data["_id"]: + flask.current_app.logger.debug("User already exists") flask.abort(status=400) # Avoid "updating" and making log if there are no changes @@ -356,20 +368,21 @@ def update_user_info(identifier: str): break if indata and is_different: - result = flask.g.db['users'].update_one({'_id': user_data['_id']}, - {'$set': indata}) + result = flask.g.db["users"].update_one( + {"_id": user_data["_id"]}, {"$set": indata} + ) if not result.acknowledged: - flask.current_app.logger.error('User update failed: %s', indata) + flask.current_app.logger.error("User update failed: %s", indata) flask.Response(status=500) else: user_data.update(indata) - utils.make_log('user', 'edit', 'User updated', user_data) + utils.make_log("user", "edit", "User updated", user_data) return flask.Response(status=200) -@blueprint.route('/me/log/', methods=['GET']) -@blueprint.route('//log/', methods=['GET']) +@blueprint.route("/me/log/", methods=["GET"]) +@blueprint.route("//log/", methods=["GET"]) @login_required def get_user_log(identifier: str = None): """ @@ -384,9 +397,11 @@ def get_user_log(identifier: str = None): flask.Response: Information about the user as json. """ if identifier is None: - identifier = str(flask.g.current_user['_id']) + identifier = str(flask.g.current_user["_id"]) - if str(flask.g.current_user['_id']) != identifier and not has_permission('USER_MANAGEMENT'): + if str(flask.g.current_user["_id"]) != identifier and not has_permission( + "USER_MANAGEMENT" + ): flask.abort(403) try: @@ -394,20 +409,22 @@ def get_user_log(identifier: str = None): except ValueError: flask.abort(status=404) - user_logs = list(flask.g.db['logs'].find({'data_type': 'user', 'data._id': user_uuid})) + user_logs = list( + flask.g.db["logs"].find({"data_type": "user", "data._id": user_uuid}) + ) for log in user_logs: - del log['data_type'] + del log["data_type"] utils.incremental_logs(user_logs) - return utils.response_json({'entry_id': user_uuid, - 'data_type': 'user', - 'logs': user_logs}) + return utils.response_json( + {"entry_id": user_uuid, "data_type": "user", "logs": user_logs} + ) -@blueprint.route('/me/actions/', methods=['GET']) -@blueprint.route('//actions/', methods=['GET']) +@blueprint.route("/me/actions/", methods=["GET"]) +@blueprint.route("//actions/", methods=["GET"]) @login_required def get_user_actions(identifier: str = None): """ @@ -422,9 +439,11 @@ def get_user_actions(identifier: str = None): flask.Response: Information about the user as json. """ if identifier is None: - identifier = str(flask.g.current_user['_id']) + identifier = str(flask.g.current_user["_id"]) - if str(flask.g.current_user['_id']) != identifier and not has_permission('USER_MANAGEMENT'): + if str(flask.g.current_user["_id"]) != identifier and not has_permission( + "USER_MANAGEMENT" + ): flask.abort(403) try: @@ -433,13 +452,13 @@ def get_user_actions(identifier: str = None): flask.abort(status=404) # only report a list of actions, not the actual data - user_logs = list(flask.g.db['logs'].find({'user': user_uuid}, {'user': 0})) + user_logs = list(flask.g.db["logs"].find({"user": user_uuid}, {"user": 0})) for entry in user_logs: - entry['entry_id'] = entry['data']['_id'] - del entry['data'] + entry["entry_id"] = entry["data"]["_id"] + del entry["data"] - return utils.response_json({'logs': user_logs}) + return utils.response_json({"logs": user_logs}) # helper functions @@ -453,35 +472,38 @@ def add_new_user(user_info: dict): Args: user_info (dict): Information about the user """ - db_user = flask.g.db['users'].find_one({'email': user_info['email']}) + db_user = flask.g.db["users"].find_one({"email": user_info["email"]}) if db_user: - db_user['auth_ids'].append(user_info['auth_id']) - result = flask.g.db['users'].update_one({'email': user_info['email']}, - {'$set': {'auth_ids': db_user['auth_ids']}}) + db_user["auth_ids"].append(user_info["auth_id"]) + result = flask.g.db["users"].update_one( + {"email": user_info["email"]}, {"$set": {"auth_ids": db_user["auth_ids"]}} + ) if not result.acknowledged: - flask.current_app.logger.error('Failed to add new auth_id to user with email %s', - user_info['email']) + flask.current_app.logger.error( + "Failed to add new auth_id to user with email %s", user_info["email"] + ) flask.Response(status=500) else: - utils.make_log('user', - 'edit', - 'Add OIDC entry to auth_ids', - db_user, - no_user=True) + utils.make_log( + "user", "edit", "Add OIDC entry to auth_ids", db_user, no_user=True + ) else: new_user = structure.user() - new_user['email'] = user_info['email'] - new_user['name'] = user_info['name'] - new_user['auth_ids'] = [user_info['auth_id']] + new_user["email"] = user_info["email"] + new_user["name"] = user_info["name"] + new_user["auth_ids"] = [user_info["auth_id"]] - result = flask.g.db['users'].insert_one(new_user) + result = flask.g.db["users"].insert_one(new_user) if not result.acknowledged: - flask.current_app.logger.error('Failed to add user with email %s via oidc', - user_info['email']) + flask.current_app.logger.error( + "Failed to add user with email %s via oidc", user_info["email"] + ) flask.Response(status=500) else: - utils.make_log('user', 'add', 'Creating new user from OAuth', new_user, no_user=True) + utils.make_log( + "user", "add", "Creating new user from OAuth", new_user, no_user=True + ) def do_login(auth_id: str): @@ -493,12 +515,12 @@ def do_login(auth_id: str): Returns bool: Whether the login succeeded. """ - user = flask.g.db['users'].find_one({'auth_ids': auth_id}) + user = flask.g.db["users"].find_one({"auth_ids": auth_id}) if not user: return False - flask.session['user_id'] = user['_id'] + flask.session["user_id"] = user["_id"] flask.session.permanent = True return True @@ -510,7 +532,7 @@ def get_current_user(): Returns: dict: The current user. """ - return get_user(user_uuid=flask.session.get('user_id')) + return get_user(user_uuid=flask.session.get("user_id")) def get_user(user_uuid=None): @@ -524,7 +546,7 @@ def get_user(user_uuid=None): dict: The current user. """ if user_uuid: - user = flask.g.db['users'].find_one({'_id': user_uuid}) + user = flask.g.db["users"].find_one({"_id": user_uuid}) if user: return user return None @@ -542,8 +564,11 @@ def has_permission(permission: str): """ if not flask.g.permissions and permission: return False - user_permissions = set(chain.from_iterable(PERMISSIONS[permission] - for permission in flask.g.permissions)) + user_permissions = set( + chain.from_iterable( + PERMISSIONS[permission] for permission in flask.g.permissions + ) + ) if permission not in user_permissions: return False return True diff --git a/backend/utils.py b/backend/utils.py index bd4d6b52..b4850e31 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -3,11 +3,12 @@ from collections import abc, namedtuple from typing import Any, Union import datetime -import hashlib +import html import re import secrets import uuid +import argon2 import bson import flask import pymongo @@ -16,12 +17,12 @@ import validate -ValidationResult = namedtuple('ValidationResult', ['result', 'status']) +ValidationResult = namedtuple("ValidationResult", ["result", "status"]) -def basic_check_indata(indata: dict, - reference_data: dict, - prohibited: Union[tuple, list]) -> tuple: +def basic_check_indata( + indata: dict, reference_data: dict, prohibited: Union[tuple, list] +) -> tuple: """ Perform basic checks of indata. @@ -43,18 +44,20 @@ def basic_check_indata(indata: dict, if prohibited is None: prohibited = [] - if 'title' in reference_data and \ - not reference_data['title'] and \ - not indata.get('title'): - flask.current_app.logger.debug('Title empty') + if ( + "title" in reference_data + and not reference_data["title"] + and not indata.get("title") + ): + flask.current_app.logger.debug("Title empty") return ValidationResult(result=False, status=400) for key in indata: if key in prohibited and indata[key] != reference_data[key]: - flask.current_app.logger.debug('Prohibited key (%s) with new value', key) + flask.current_app.logger.debug("Prohibited key (%s) with new value", key) return ValidationResult(result=False, status=403) if key not in reference_data: - flask.current_app.logger.debug('Bad key (%s)', key) + flask.current_app.logger.debug("Bad key (%s)", key) return ValidationResult(result=False, status=400) if indata[key] != reference_data[key]: if not validate.validate_field(key, indata[key]): @@ -62,6 +65,22 @@ def basic_check_indata(indata: dict, return ValidationResult(result=True, status=200) +def secure_description(data: str): + """ + Process the description to make sure it does not contain dangerous data. + + Current checks: + * Escape HTML + + Args: + data: The description to process. + + Returns: + str: The processed description. + """ + return html.escape(data) + + # csrf def verify_csrf_token(): """ @@ -69,9 +88,9 @@ def verify_csrf_token(): Aborts with status 400 if the verification fails. """ - token = flask.request.headers.get('X-CSRFToken') - if not token or (token != flask.request.cookies.get('_csrf_token')): - flask.current_app.logger.warning('Bad csrf token received') + token = flask.request.headers.get("X-CSRFToken") + if not token or (token != flask.request.cookies.get("_csrf_token")): + flask.current_app.logger.warning("Bad csrf token received") flask.abort(status=400) @@ -93,9 +112,8 @@ def gen_api_key(): Returns: APIkey: The API key with salt. """ - ApiKey = namedtuple('ApiKey', ['key', 'salt']) - return ApiKey(key=secrets.token_hex(48), - salt=secrets.token_hex(32)) + ApiKey = namedtuple("ApiKey", ["key", "salt"]) + return ApiKey(key=secrets.token_urlsafe(64), salt=secrets.token_hex(32)) def gen_api_key_hash(api_key: str, salt: str): @@ -109,8 +127,8 @@ def gen_api_key_hash(api_key: str, salt: str): Returns: str: SHA512 hash as hex. """ - ct_bytes = bytes.fromhex(api_key + salt) - return hashlib.sha512(ct_bytes).hexdigest() + ph = argon2.PasswordHasher() + return ph.hash(api_key + salt) def verify_api_key(username: str, api_key: str): @@ -123,18 +141,15 @@ def verify_api_key(username: str, api_key: str): username (str): The username to check. api_key (str): The received API key (hex). """ - user_info = flask.g.db['users'].find_one({'auth_ids': username}) + ph = argon2.PasswordHasher() + user_info = flask.g.db["users"].find_one({"auth_ids": username}) if not user_info: - flask.current_app.logger.warning('API key verification failed (bad username)') + flask.current_app.logger.info("API key verification failed (bad username)") flask.abort(status=401) try: - ct_bytes = bytes.fromhex(api_key + user_info['api_salt']) - except ValueError: - flask.current_app.logger.warning('Non-hex API key provided') - flask.abort(status=401) - new_hash = hashlib.sha512(ct_bytes).hexdigest() - if not new_hash == user_info['api_key']: - flask.current_app.logger.warning('API key verification failed (bad hash)') + ph.verify(user_info["api_key"], api_key + user_info["api_salt"]) + except argon2.exceptions.VerifyMismatchError: + flask.current_app.logger.info("API key verification failed (bad hash)") flask.abort(status=401) @@ -148,13 +163,17 @@ def get_dbclient(conf) -> pymongo.mongo_client.MongoClient: Returns: pymongo.mongo_client.MongoClient: The client connection. """ - return pymongo.MongoClient(host=conf['mongo']['host'], - port=conf['mongo']['port'], - username=conf['mongo']['user'], - password=conf['mongo']['password']) + return pymongo.MongoClient( + host=conf["mongo"]["host"], + port=conf["mongo"]["port"], + username=conf["mongo"]["user"], + password=conf["mongo"]["password"], + ) -def get_db(dbserver: pymongo.mongo_client.MongoClient, conf) -> pymongo.database.Database: +def get_db( + dbserver: pymongo.mongo_client.MongoClient, conf +) -> pymongo.database.Database: """ Get the connection to the MongoDB database. @@ -165,9 +184,10 @@ def get_db(dbserver: pymongo.mongo_client.MongoClient, conf) -> pymongo.database Returns: pymongo.database.Database: The database connection. """ - codec_options = bson.codec_options.CodecOptions(uuid_representation=bson.binary.STANDARD) - return dbserver.get_database(conf['mongo']['db'], - codec_options=(codec_options)) + codec_options = bson.codec_options.CodecOptions( + uuid_representation=bson.binary.STANDARD + ) + return dbserver.get_database(conf["mongo"]["db"], codec_options=(codec_options)) def new_uuid() -> uuid.UUID: @@ -218,16 +238,16 @@ def convert_keys_to_camel(chunk: Any) -> Any: new_chunk = {} for key, value in chunk.items(): - if key == '_id': + if key == "_id": new_chunk[key] = value continue # First character should be the same as in the original string - new_key = key[0] + ''.join([a[0].upper() + a[1:] for a in key.split('_')])[1:] + new_key = key[0] + "".join([a[0].upper() + a[1:] for a in key.split("_")])[1:] new_chunk[new_key] = convert_keys_to_camel(value) return new_chunk -REGEX = {'email': re.compile(r'.*@.*\..*')} +REGEX = {"email": re.compile(r".*@.*\..*")} def is_email(indata: str): @@ -242,7 +262,7 @@ def is_email(indata: str): """ if not isinstance(indata, str): return False - return bool(REGEX['email'].search(indata)) + return bool(REGEX["email"].search(indata)) def response_json(json_structure: dict): @@ -270,12 +290,14 @@ def make_timestamp(): # pylint: disable=too-many-arguments -def make_log(data_type: str, - action: str, - comment: str, - data: dict = None, - no_user: bool = False, - dbsession=None): +def make_log( + data_type: str, + action: str, + comment: str, + data: dict = None, + no_user: bool = False, + dbsession=None, +): """ Log a change in the system. @@ -299,19 +321,25 @@ def make_log(data_type: str, """ log = structure.log() if no_user: - active_user = 'system' + active_user = "system" else: - active_user = flask.g.current_user['_id'] - - log.update({'action': action, - 'comment': comment, - 'data_type': data_type, - 'data': data, - 'user': active_user}) - result = flask.g.db['logs'].insert_one(log, session=dbsession) + active_user = flask.g.current_user["_id"] + + log.update( + { + "action": action, + "comment": comment, + "data_type": data_type, + "data": data, + "user": active_user, + } + ) + result = flask.g.db["logs"].insert_one(log, session=dbsession) if not result.acknowledged: - flask.current_app.logger.error(f'Log failed: A:{action} C:{comment} D:{data} ' + - f'DT: {data_type} U: {flask.g.current_user["_id"]}') + flask.current_app.logger.error( + f"Log failed: A:{action} C:{comment} D:{data} " + + f'DT: {data_type} U: {flask.g.current_user["_id"]}' + ) return result.acknowledged @@ -324,14 +352,14 @@ def incremental_logs(logs: list): ``logs`` is changed in-place. """ - logs.sort(key=lambda x: x['timestamp']) - for i in range(len(logs)-1, 0, -1): + logs.sort(key=lambda x: x["timestamp"]) + for i in range(len(logs) - 1, 0, -1): del_keys = [] - for key in logs[i]['data']: - if logs[i]['data'][key] == logs[i-1]['data'][key]: + for key in logs[i]["data"]: + if logs[i]["data"][key] == logs[i - 1]["data"][key]: del_keys.append(key) for key in del_keys: - del logs[i]['data'][key] + del logs[i]["data"][key] def check_email_uuid(user_identifier: str) -> Union[str, uuid.UUID]: @@ -352,22 +380,23 @@ def check_email_uuid(user_identifier: str) -> Union[str, uuid.UUID]: Union[str, uuid.UUID]: The new value for the field. """ if is_email(user_identifier): - user_entry = flask.g.db['users'].find_one({'email': user_identifier}) + user_entry = flask.g.db["users"].find_one({"email": user_identifier}) if user_entry: - return user_entry['_id'] + return user_entry["_id"] return user_identifier try: user_uuid = str_to_uuid(user_identifier) except ValueError: - return '' - user_entry = flask.g.db['users'].find_one({'_id': user_uuid}) + return "" + user_entry = flask.g.db["users"].find_one({"_id": user_uuid}) if user_entry: - return user_entry['_id'] - return '' + return user_entry["_id"] + return "" -def user_uuid_data(user_ids: Union[str, list, uuid.UUID], - mongodb: pymongo.database.Database) -> list: +def user_uuid_data( + user_ids: Union[str, list, uuid.UUID], mongodb: pymongo.database.Database +) -> list: """ Retrieve some extra information about a user using a uuid as input. @@ -386,11 +415,15 @@ def user_uuid_data(user_ids: Union[str, list, uuid.UUID], user_uuids = [str_to_uuid(entry) for entry in user_ids] else: user_uuids = [user_ids] - data = mongodb['users'].find({'_id': {'$in': user_uuids}}) - return [{'_id': str(entry['_id']), - 'affiliation': entry['affiliation'], - 'name': entry['name'], - 'contact': entry['contact'], - 'url': entry['url'], - 'orcid': entry['orcid']} - for entry in data] + data = mongodb["users"].find({"_id": {"$in": user_uuids}}) + return [ + { + "_id": str(entry["_id"]), + "affiliation": entry["affiliation"], + "name": entry["name"], + "contact": entry["contact"], + "url": entry["url"], + "orcid": entry["orcid"], + } + for entry in data + ] diff --git a/backend/validate.py b/backend/validate.py index cbf0ad03..f788fc21 100644 --- a/backend/validate.py +++ b/backend/validate.py @@ -37,13 +37,15 @@ def validate_field(field_key: str, field_value: Any) -> bool: try: VALIDATION_MAPPER[field_key](field_value) except KeyError: - flask.current_app.logger.debug('Unknown key: %s', field_key) + flask.current_app.logger.debug("Unknown key: %s", field_key) return False except ValueError as err: - flask.current_app.logger.debug('Indata validation failed: %s - %s', field_key, err) + flask.current_app.logger.debug( + "Indata validation failed: %s - %s", field_key, err + ) return False except exceptions.AuthError as err: - flask.current_app.logger.debug('Permission failed: %s - %s', field_key, err) + flask.current_app.logger.debug("Permission failed: %s - %s", field_key, err) return False return True @@ -64,16 +66,16 @@ def validate_datasets(data: list) -> bool: ValueError: Validation failed. """ if not isinstance(data, list): - raise ValueError(f'Must be list ({data})') + raise ValueError(f"Must be list ({data})") for ds_entry in data: if not isinstance(ds_entry, str): - raise ValueError(f'Must be str ({ds_entry})') + raise ValueError(f"Must be str ({ds_entry})") try: ds_uuid = uuid.UUID(ds_entry) except ValueError as err: - raise ValueError(f'Not a valid uuid ({data})') from err - if not flask.g.db['datasets'].find_one({'_id': ds_uuid}): - raise ValueError(f'Uuid not in db ({data})') + raise ValueError(f"Not a valid uuid ({data})") from err + if not flask.g.db["datasets"].find_one({"_id": ds_uuid}): + raise ValueError(f"Uuid not in db ({data})") return True @@ -93,9 +95,9 @@ def validate_email(data) -> bool: ValueError: Validation failed. """ if not isinstance(data, str): - raise ValueError(f'Not a string ({data})') + raise ValueError(f"Not a string ({data})") if not utils.is_email(data): - raise ValueError(f'Not a valid email address ({data})') + raise ValueError(f"Not a valid email address ({data})") return True @@ -113,10 +115,10 @@ def validate_list_of_strings(data: list) -> bool: ValueError: Validation failed. """ if not isinstance(data, list): - raise ValueError(f'Not a list ({data})') + raise ValueError(f"Not a list ({data})") for entry in data: if not isinstance(entry, str): - raise ValueError(f'Not a string ({entry})') + raise ValueError(f"Not a string ({entry})") return True @@ -136,10 +138,10 @@ def validate_permissions(data: list) -> bool: ValueError: Validation failed. """ if not isinstance(data, list): - raise ValueError('Must be a list') + raise ValueError("Must be a list") for entry in data: if entry not in user.PERMISSIONS: - raise ValueError(f'Bad entry ({entry})') + raise ValueError(f"Bad entry ({entry})") return True @@ -157,7 +159,7 @@ def validate_string(data: str) -> bool: ValueError: Validation failed. """ if not isinstance(data, str): - raise ValueError(f'Not a string ({data})') + raise ValueError(f"Not a string ({data})") return True @@ -177,15 +179,14 @@ def validate_cross_references(data: list) -> bool: ValueError: Validation failed. """ if not isinstance(data, list): - raise ValueError(f'Not a list ({data})') + raise ValueError(f"Not a list ({data})") for entry in data: if not isinstance(entry, dict): - raise ValueError(f'List entries must be dicts ({entry})') - if list(entry.keys()) != ['title', 'value']: - raise KeyError(f'Incorrect keys ({entry.keys})') - if not isinstance(entry['title'], str) or \ - not isinstance(entry['value'], str): - raise ValueError(f'Values must be strings ({entry.values()})') + raise ValueError(f"List entries must be dicts ({entry})") + if list(entry.keys()) != ["title", "value"]: + raise KeyError(f"Incorrect keys ({entry.keys})") + if not isinstance(entry["title"], str) or not isinstance(entry["value"], str): + raise ValueError(f"Values must be strings ({entry.values()})") return True @@ -204,13 +205,13 @@ def validate_properties(data: dict) -> bool: Raises: ValueError: Validation failed. """ - if not user.has_permission('DATA_MANAGEMENT'): - raise exceptions.AuthError('Permission DATA_MANAGEMENT required') + if not user.has_permission("DATA_MANAGEMENT"): + raise exceptions.AuthError("Permission DATA_MANAGEMENT required") if not isinstance(data, dict): - raise ValueError(f'Not a dict ({data})') + raise ValueError(f"Not a dict ({data})") for key in data: if not isinstance(key, str) or not isinstance(data[key], str): - raise ValueError(f'Keys and values must be strings ({key}, {data[key]})') + raise ValueError(f"Keys and values must be strings ({key}, {data[key]})") return True @@ -230,10 +231,10 @@ def validate_tags(data: Union[tuple, list]) -> bool: ValueError: Validation failed. """ if not isinstance(data, list) and not isinstance(data, tuple): - raise ValueError(f'Not a list ({data})') + raise ValueError(f"Not a list ({data})") for value in data: if not isinstance(value, str): - raise ValueError(f'All list entries must be str ({value})') + raise ValueError(f"All list entries must be str ({value})") return True @@ -253,7 +254,7 @@ def validate_title(data: str) -> bool: ValueError: Validation failed. """ if validate_string(data) and not data: - raise ValueError('Must not be empty') + raise ValueError("Must not be empty") return True @@ -273,9 +274,9 @@ def validate_url(data: str) -> bool: ValueError: Validation failed. """ if not isinstance(data, str): - raise ValueError('Must be a string') - if data and not data.startswith('http://') and not data.startswith('https://'): - raise ValueError('URLs must start with http(s)://') + raise ValueError("Must be a string") + if data and not data.startswith("http://") and not data.startswith("https://"): + raise ValueError("URLs must start with http(s)://") return True @@ -295,7 +296,7 @@ def validate_user(data: str) -> bool: ValueError: Validation failed. """ if not isinstance(data, str): - raise ValueError(f'Bad data type (must be str): {data}') + raise ValueError(f"Bad data type (must be str): {data}") if not data: return True @@ -303,9 +304,9 @@ def validate_user(data: str) -> bool: try: user_uuid = uuid.UUID(data) except ValueError as err: - raise ValueError(f'Not a valid uuid ({data})') from err - if not flask.g.db['users'].find_one({'_id': user_uuid}): - raise ValueError(f'Uuid not in db ({data})') + raise ValueError(f"Not a valid uuid ({data})") from err + if not flask.g.db["users"].find_one({"_id": user_uuid}): + raise ValueError(f"Uuid not in db ({data})") return True @@ -328,33 +329,35 @@ def validate_user_list(data: Union[tuple, list]) -> bool: ValueError: Validation failed. """ if not isinstance(data, list): - raise ValueError(f'Bad data type (must be list): {data}') + raise ValueError(f"Bad data type (must be list): {data}") for u_uuid in data: try: user_uuid = uuid.UUID(u_uuid) except ValueError as err: - raise ValueError(f'Not a valid uuid ({data})') from err - if not flask.g.db['users'].find_one({'_id': user_uuid}): - raise ValueError(f'Uuid not in db ({data})') + raise ValueError(f"Not a valid uuid ({data})") from err + if not flask.g.db["users"].find_one({"_id": user_uuid}): + raise ValueError(f"Uuid not in db ({data})") return True -VALIDATION_MAPPER = {'affiliation': validate_string, - 'auth_ids': validate_list_of_strings, - 'authors': validate_user_list, - 'contact': validate_string, - 'cross_references': validate_cross_references, - 'description': validate_string, - 'datasets': validate_datasets, - 'editors': validate_user_list, - 'email': validate_email, - 'generators': validate_user_list, - 'name': validate_string, - 'orcid': validate_string, - 'organisation': validate_user, - 'permissions': validate_permissions, - 'properties': validate_properties, - 'tags': validate_tags, - 'title': validate_title, - 'url': validate_url} +VALIDATION_MAPPER = { + "affiliation": validate_string, + "auth_ids": validate_list_of_strings, + "authors": validate_user_list, + "contact": validate_string, + "cross_references": validate_cross_references, + "description": validate_string, + "datasets": validate_datasets, + "editors": validate_user_list, + "email": validate_email, + "generators": validate_user_list, + "name": validate_string, + "orcid": validate_string, + "organisation": validate_user, + "permissions": validate_permissions, + "properties": validate_properties, + "tags": validate_tags, + "title": validate_title, + "url": validate_url, +} diff --git a/docs/build/html/.buildinfo b/docs/build/html/.buildinfo index 42a0f807..303bfc80 100644 --- a/docs/build/html/.buildinfo +++ b/docs/build/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: dd6174e40970c54baed18b7fa2e66762 +config: a5bf3c7429818817824ca7b29a92f078 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/html/api.html b/docs/build/html/api.html index 54bd9f75..220e4667 100644 --- a/docs/build/html/api.html +++ b/docs/build/html/api.html @@ -533,7 +533,7 @@

Navigation

diff --git a/docs/build/html/app.html b/docs/build/html/app.html new file mode 100644 index 00000000..c51040fb --- /dev/null +++ b/docs/build/html/app.html @@ -0,0 +1,170 @@ + + + + + + + + app module — Data Tracker documentation + + + + + + + + + + + + +
+
+
+
+ +
+

app module

+

Main app for the Data Tracker.

+
+
+app.api_base()[source]
+

List entities.

+
+ +
+
+app.error_bad_request(_)[source]
+

Make sure a simple 400 is returned instead of an html page.

+
+ +
+
+app.error_forbidden(_)[source]
+

Make sure a simple 403 is returned instead of an html page.

+
+ +
+
+app.error_not_found(_)[source]
+

Make sure a simple 404 is returned instead of an html page.

+
+ +
+
+app.error_unauthorized(_)[source]
+

Make sure a simple 401 is returned instead of an html page.

+
+ +
+
+app.finalize(response)[source]
+

Finalize the response and clean up.

+
+ +
+
+app.key_login()[source]
+

Log in using an apikey.

+
+ +
+
+app.login_types()[source]
+

List login types.

+
+ +
+
+app.logout()[source]
+

Log out the current user.

+
+ +
+
+app.oidc_authorize(auth_name)[source]
+

Authorize a login using OpenID Connect (e.g. Elixir AAI).

+
+ +
+
+app.oidc_login(auth_name)[source]
+

Perform a login using OpenID Connect (e.g. Elixir AAI).

+
+ +
+
+app.oidc_types()[source]
+

List OpenID Connect types.

+
+ +
+
+app.prepare()[source]
+

Open the database connection and get the current user.

+
+ +
+ + +
+
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/docs/build/html/code.app.html b/docs/build/html/code.app.html index b5a951d0..48e26ab7 100644 --- a/docs/build/html/code.app.html +++ b/docs/build/html/code.app.html @@ -187,7 +187,7 @@

Navigation

diff --git a/docs/build/html/code.collection.html b/docs/build/html/code.collection.html index b01c9ba0..4632ab50 100644 --- a/docs/build/html/code.collection.html +++ b/docs/build/html/code.collection.html @@ -91,6 +91,20 @@

Navigation

+
+
+collection.get_collection_data_structure()[source]
+

Get an empty collection entry.

+
+
Returns
+

JSON structure with a list of collections.

+
+
Return type
+

flask.Response

+
+
+
+
collection.get_collection_log(identifier: str = None)[source]
@@ -224,7 +238,7 @@

Navigation

diff --git a/docs/build/html/code.config.html b/docs/build/html/code.config.html index 743cfdb2..7ff0b26c 100644 --- a/docs/build/html/code.config.html +++ b/docs/build/html/code.config.html @@ -145,7 +145,7 @@

Navigation

diff --git a/docs/build/html/code.dataset.html b/docs/build/html/code.dataset.html index 27136a64..4eef8bad 100644 --- a/docs/build/html/code.dataset.html +++ b/docs/build/html/code.dataset.html @@ -48,6 +48,17 @@

Navigation

dataset.py

Dataset requests.

+
+
+dataset.add_dataset()[source]
+

Add a dataset to the given order.

+
+
Parameters
+

identifier (str) – The order to add the dataset to.

+
+
+
+
dataset.build_dataset_info(identifier: str)[source]
@@ -94,6 +105,20 @@

Navigation

+
+
+dataset.get_dataset_data_structure()[source]
+

Get an empty dataset entry.

+
+
Returns
+

JSON structure with a list of datasets.

+
+
Return type
+

flask.Response

+
+
+
+
dataset.get_dataset_log(identifier: str = None)[source]
@@ -219,7 +244,7 @@

Navigation

diff --git a/docs/build/html/code.developer.html b/docs/build/html/code.developer.html index 06104dbe..3e43fa43 100644 --- a/docs/build/html/code.developer.html +++ b/docs/build/html/code.developer.html @@ -179,7 +179,7 @@

Navigation

diff --git a/docs/build/html/code.order.html b/docs/build/html/code.order.html index b743101f..e1182edb 100644 --- a/docs/build/html/code.order.html +++ b/docs/build/html/code.order.html @@ -53,17 +53,6 @@

Navigation

  • If you have permission ORDERS you have CRUD access to your own orders.

  • If you have permission DATA_MANAGER you have CRUD access to any orders.

  • -
    -
    -order.add_dataset(identifier)[source]
    -

    Add a dataset to the given order.

    -
    -
    Parameters
    -

    identifier (str) – The order to add the dataset to.

    -
    -
    -
    -
    order.add_order()[source]
    @@ -288,7 +277,7 @@

    Navigation

    diff --git a/docs/build/html/code.project.html b/docs/build/html/code.project.html new file mode 100644 index 00000000..03a6ab73 --- /dev/null +++ b/docs/build/html/code.project.html @@ -0,0 +1,207 @@ + + + + + + + + project.py — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    project.py

    +

    Project requests.

    +
    +
    +project.add_project()[source]
    +

    Add a project.

    +
    +
    Returns
    +

    Json structure with the _id of the project.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +project.delete_project(identifier: str)[source]
    +

    Delete a project.

    +

    Can be deleted only by an owner or user with DATA_MANAGEMENT permissions.

    +
    +
    Parameters
    +

    identifier (str) – The project uuid.

    +
    +
    +
    + +
    +
    +project.get_project(identifier)[source]
    +

    Retrieve the project with uuid <identifier>.

    +
    +
    Parameters
    +

    identifier (str) – uuid for the wanted project

    +
    +
    Returns
    +

    json structure for the project

    +
    +
    Return type
    +

    flask.Request

    +
    +
    +
    + +
    +
    +project.get_project_log(identifier: str = None)[source]
    +

    Get change logs for the user entry with uuid identifier.

    +

    Can be accessed by owners and admin (DATA_MANAGEMENT).

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the project.

    +
    +
    Returns
    +

    Logs as json.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +project.get_random(amount: int = 1)[source]
    +

    Retrieve random project(s).

    +
    +
    Parameters
    +

    amount (int) – number of requested projects

    +
    +
    Returns
    +

    json structure for the project(s)

    +
    +
    Return type
    +

    flask.Request

    +
    +
    +
    + +
    +
    +project.list_project()[source]
    +

    Provide a simplified list of all available projects.

    +
    + +
    +
    +project.list_user_projects()[source]
    +

    List project owned by the user.

    +
    +
    Returns
    +

    JSON structure.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +project.update_project(identifier)[source]
    +

    Update a project.

    +
    +
    Parameters
    +

    identifier (str) – The project uuid.

    +
    +
    Returns
    +

    Status code.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/code.structure.html b/docs/build/html/code.structure.html index fc99350a..2f277ba5 100644 --- a/docs/build/html/code.structure.html +++ b/docs/build/html/code.structure.html @@ -180,7 +180,7 @@

    Navigation

    diff --git a/docs/build/html/code.user.html b/docs/build/html/code.user.html index 6064c057..37c6d192 100644 --- a/docs/build/html/code.user.html +++ b/docs/build/html/code.user.html @@ -215,6 +215,20 @@

    Navigation

    +
    +
    +user.get_user_data_structure()[source]
    +

    Get an empty user entry.

    +
    +
    Returns
    +

    JSON structure with a list of users.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    +
    user.get_user_log(identifier: str = None)[source]
    @@ -356,7 +370,7 @@

    Navigation

    diff --git a/docs/build/html/code.utils.html b/docs/build/html/code.utils.html index 88496d3c..4f319d68 100644 --- a/docs/build/html/code.utils.html +++ b/docs/build/html/code.utils.html @@ -90,7 +90,7 @@

    Navigation

    (bool: whether the check passed, code: Suggested http code)

    Return type
    -

    tuple

    +

    namedtuple

    @@ -135,6 +135,23 @@

    Navigation

    +
    +
    +utils.escape_html(data: str) → str[source]
    +

    Escape e.g. html tags for the provided text.

    +
    +
    Parameters
    +

    data (str) – The text to escape.

    +
    +
    Returns
    +

    The escaped text.

    +
    +
    Return type
    +

    str

    +
    +
    +
    +
    utils.gen_api_key()[source]
    @@ -444,7 +461,7 @@

    Navigation

    diff --git a/docs/build/html/code.validate.html b/docs/build/html/code.validate.html index efcaaab6..a239a11b 100644 --- a/docs/build/html/code.validate.html +++ b/docs/build/html/code.validate.html @@ -46,6 +46,27 @@

    Navigation

    Validators for indata.

    Indata can be sent to validate_field, which will use the corresponding functions to check each field.

    +
    +
    +validate.validate_cross_references(data: list) → bool[source]
    +

    Validate input for the cross_references field.

    +

    It must be a list.

    +
    +
    Parameters
    +

    data (dict) – The data to be validated.

    +
    +
    Returns
    +

    Validation passed.

    +
    +
    Return type
    +

    bool

    +
    +
    Raises
    +

    ValueError – Validation failed.

    +
    +
    +
    +
    validate.validate_datasets(data: list) → bool[source]
    @@ -155,12 +176,13 @@

    Navigation

    -
    -validate.validate_string(data: str) → bool[source]
    -

    Validate input for field that must have a str value.

    +
    +validate.validate_properties(data: dict) → bool[source]
    +

    Validate input for the properties field.

    +

    It must be a dict. The user must have DATA_MANAGEMENT permissions.

    Parameters
    -

    data (str) – The data to be validated.

    +

    data (dict) – The data to be validated.

    Returns

    Validation passed.

    @@ -175,13 +197,12 @@

    Navigation

    -
    -validate.validate_tags_std(data: dict) → bool[source]
    -

    Validate input for the tags_standard field.

    -

    It must be a dict.

    +
    +validate.validate_string(data: str) → bool[source]
    +

    Validate input for field that must have a str value.

    Parameters
    -

    data (dict) – The data to be validated.

    +

    data (str) – The data to be validated.

    Returns

    Validation passed.

    @@ -196,10 +217,10 @@

    Navigation

    -
    -validate.validate_tags_user(data: dict) → bool[source]
    -

    Validate input for the tags_user field.

    -

    It must be a dict.

    +
    +validate.validate_tags(data: Union[tuple, list]) → bool[source]
    +

    Validate input for the tags field.

    +

    It must be a list or tuple.

    Parameters

    data (dict) – The data to be validated.

    @@ -281,7 +302,7 @@

    Navigation

    -validate.validate_user_list(data: Union[str, list]) → bool[source]
    +validate.validate_user_list(data: Union[tuple, list]) → bool[source]

    Validate input for a field containing a list of user uuid(s).

    For compatibility, the input may be UUIDs as either string (single user) or a list (single or multiple users).

    @@ -357,7 +378,7 @@

    Navigation

    diff --git a/docs/build/html/config.html b/docs/build/html/config.html new file mode 100644 index 00000000..0fc2bf09 --- /dev/null +++ b/docs/build/html/config.html @@ -0,0 +1,128 @@ + + + + + + + + config module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    config module

    +

    Settings manager for the data tracker.

    +

    Read settings from ./config.yaml, ../config.yaml or from the provided path.

    +
    +
    +config.init() → dict[source]
    +

    Read the config from a config.yaml file.

    +
    +
    Returns
    +

    The config.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +config.read_config(path: str = '')[source]
    +

    Look for settings.yaml and parse the settings from there.

    +

    The file is expected to be found in the current, parent or provided folder.

    +
    +
    Parameters
    +

    path (str) – The yaml file to use

    +
    +
    Returns
    +

    The loaded settings

    +
    +
    Return type
    +

    dict

    +
    +
    Raises
    +

    FileNotFoundError – No settings file found

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/data_structure.html b/docs/build/html/data_structure.html index d28b5cac..4d8ae2ba 100644 --- a/docs/build/html/data_structure.html +++ b/docs/build/html/data_structure.html @@ -135,22 +135,17 @@

    Summary

    receivers

    -

    List of users who received data from facility

    -

    Empty

    -

    Hidden

    - -

    datasets

    +

    datasets

    List of associated datasets

    Empty

    Visible (via dataset)

    -

    tags_standard

    +

    tags_standard

    Tags defined in the system

    Empty

    Hidden

    -

    tags_user

    +

    tags_user

    Tags defined by the users

    Empty

    Hidden

    @@ -217,28 +212,21 @@

    Fieldsreceivers +
    datasets
      -
    • List of users.

    • -
    • Corresponds to the users who received the data from the facility

    • -
    • Default: Empty

    • -
    -
    -
    datasets
    -
    • List of datasets associated to the order.

    • Cannot be modified directly but must be modified through specialised means.

    • Default: Empty

    -
    tags_standard
    -
    @@ -892,7 +879,7 @@

    Navigation

    diff --git a/docs/build/html/dataset.html b/docs/build/html/dataset.html new file mode 100644 index 00000000..23d5da74 --- /dev/null +++ b/docs/build/html/dataset.html @@ -0,0 +1,227 @@ + + + + + + + + dataset module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    dataset module

    +

    Dataset requests.

    +
    +
    +dataset.add_dataset()[source]
    +

    Add a dataset to the given order.

    +
    +
    Parameters
    +

    identifier (str) – The order to add the dataset to.

    +
    +
    +
    + +
    +
    +dataset.build_dataset_info(identifier: str)[source]
    +

    Query for a dataset from the database.

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the dataset.

    +
    +
    Returns
    +

    The prepared dataset entry.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +dataset.delete_dataset(identifier: str)[source]
    +

    Delete a dataset.

    +

    Can be deleted only by editors or user with DATA_MANAGEMENT permissions.

    +
    +
    Parameters
    +

    identifier (str) – The dataset uuid.

    +
    +
    +
    + +
    +
    +dataset.get_dataset(identifier)[source]
    +

    Retrieve the dataset with uuid <identifier>.

    +
    +
    Parameters
    +

    identifier (str) – uuid for the wanted dataset

    +
    +
    Returns
    +

    json structure for the dataset

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +dataset.get_dataset_data_structure()[source]
    +

    Get an empty dataset entry.

    +
    +
    Returns
    +

    JSON structure with a list of datasets.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +dataset.get_dataset_log(identifier: str = None)[source]
    +

    Get change logs for the user entry with uuid identifier.

    +

    Can be accessed by creator (order), receiver (order), and admin (DATA_MANAGEMENT).

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the dataset.

    +
    +
    Returns
    +

    Logs as json.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +dataset.get_random_ds(amount: int = 1)[source]
    +

    Retrieve random dataset(s).

    +
    +
    Parameters
    +

    amount (int) – number of requested datasets

    +
    +
    Returns
    +

    json structure for the dataset(s)

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +dataset.list_datasets()[source]
    +

    Provide a simplified list of all available datasets.

    +
    + +
    +
    +dataset.list_user_data()[source]
    +

    List all datasets belonging to current user.

    +
    + +
    +
    +dataset.update_dataset(identifier)[source]
    +

    Update a dataset with new values.

    +
    +
    Parameters
    +

    identifier (str) – uuid for the wanted dataset

    +
    +
    Returns
    +

    success: 200, failure: 400

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/developer.html b/docs/build/html/developer.html new file mode 100644 index 00000000..06fc0fca --- /dev/null +++ b/docs/build/html/developer.html @@ -0,0 +1,162 @@ + + + + + + + + developer module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    developer module

    +

    Routes and functions intended to aid development and testing.

    +
    +
    +developer.api_hello()[source]
    +

    Test request.

    +
    + +
    +
    +developer.csrf_test()[source]
    +

    Test csrf tokens.

    +
    + +
    +
    +developer.get_added_ds()[source]
    +

    Get datasets added during testing.

    +
    + +
    +
    +developer.list_config()[source]
    +

    List all session variables.

    +
    + +
    +
    +developer.list_current_user()[source]
    +

    List all session variables.

    +
    + +
    +
    +developer.list_session()[source]
    +

    List all session variables.

    +
    + +
    +
    +developer.login(identifier: str)[source]
    +

    Log in without password.

    +
    +
    Parameters
    +

    identifer (str) – User auth_id.

    +
    +
    +
    + +
    +
    +developer.login_hello()[source]
    +

    Test request requiring login.

    +
    + +
    +
    +developer.permission_hello(permission: str)[source]
    +

    Test request requiring the given permission.

    +
    +
    Parameters
    +

    permission (str) – The permission to test for.

    +
    +
    +
    + +
    +
    +developer.stop_server()[source]
    +

    Shutdown the flask server.

    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/development.html b/docs/build/html/development.html index 2e092f20..b6a0de6c 100644 --- a/docs/build/html/development.html +++ b/docs/build/html/development.html @@ -127,7 +127,7 @@

    Navigation

    diff --git a/docs/build/html/development.quick_environment.html b/docs/build/html/development.quick_environment.html index 283937cb..effeda5d 100644 --- a/docs/build/html/development.quick_environment.html +++ b/docs/build/html/development.quick_environment.html @@ -141,7 +141,7 @@

    Navigation

    diff --git a/docs/build/html/genindex.html b/docs/build/html/genindex.html index 5f7fee2e..271e33dc 100644 --- a/docs/build/html/genindex.html +++ b/docs/build/html/genindex.html @@ -65,25 +65,27 @@

    A

    @@ -92,11 +94,11 @@

    A

    B

    @@ -104,7 +106,7 @@

    B

    C

    -
  • collection() (in module structure) +
  • collection() (in module structure), [1]
  • @@ -138,29 +140,31 @@

    D

    dataset -
  • dataset() (in module structure) +
  • dataset() (in module structure), [1]
  • delete_collection() (in module collection)
  • -
  • delete_dataset() (in module dataset) +
  • delete_dataset() (in module dataset), [1]
  • @@ -168,15 +172,17 @@

    D

    E

    @@ -184,7 +190,7 @@

    E

    F

    @@ -192,55 +198,69 @@

    F

    G

    @@ -248,7 +268,7 @@

    G

    H

    @@ -256,13 +276,13 @@

    H

    I

    @@ -270,7 +290,7 @@

    I

    K

    @@ -280,37 +300,41 @@

    L

    @@ -318,31 +342,33 @@

    L

    M

    + + + + +
    diff --git a/docs/build/html/index.html b/docs/build/html/index.html index e01b4a8e..0f0dcb27 100644 --- a/docs/build/html/index.html +++ b/docs/build/html/index.html @@ -138,7 +138,7 @@

    Navigation

    diff --git a/docs/build/html/modules.html b/docs/build/html/modules.html index 35711a63..12ae988a 100644 --- a/docs/build/html/modules.html +++ b/docs/build/html/modules.html @@ -120,7 +120,7 @@

    Navigation

    diff --git a/docs/build/html/modules/app.html b/docs/build/html/modules/app.html index 91c88c9e..2671c018 100644 --- a/docs/build/html/modules/app.html +++ b/docs/build/html/modules/app.html @@ -40,6 +40,7 @@

    Source code for app

     """Main app for the Data Tracker."""
     
     import json
    +import datetime
     import logging
     
     import flask
    @@ -51,11 +52,16 @@ 

    Source code for app

     import collection
     import user
     import utils
    +import db_management
     
     from authlib.integrations.flask_client import OAuth
     
     app = flask.Flask(__name__)  # pylint: disable=invalid-name
    -app.config.update(config.init())
    +appconf = config.init()
    +db_management.check_db(appconf)
    +app.config.update(appconf)
    +app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=31)
    +
     
     if app.config['dev_mode']['api']:
         app.register_blueprint(developer.blueprint, url_prefix='/api/v1/developer')
    @@ -76,8 +82,8 @@ 

    Source code for app

         """Open the database connection and get the current user."""
         flask.g.dbclient = utils.get_dbclient(flask.current_app.config)
         flask.g.db = utils.get_db(flask.g.dbclient, flask.current_app.config)
    -    if apikey := flask.request.headers.get('X-API-Key'):
    -        if not (apiuser := flask.request.headers.get('X-API-User')):  # pylint: disable=superfluous-parens
    +    if apikey := flask.request.headers.get('X-API-Key'):
    +        if not (apiuser := flask.request.headers.get('X-API-User')):  # pylint: disable=superfluous-parens
                 flask.abort(status=400)
             utils.verify_api_key(apiuser, apikey)
             flask.g.current_user = flask.g.db['users'].find_one({'auth_ids': apiuser})
    @@ -134,6 +140,7 @@ 

    Source code for app

         redirect_uri = flask.url_for('oidc_authorize',
                                      auth_name=auth_name,
                                      _external=True)
    +    flask.session['incoming_url'] = flask.request.args.get('origin') or '/'
         return client.authorize_redirect(redirect_uri)
    @@ -149,14 +156,17 @@

    Source code for app

         else:
             user_info = client.userinfo()
         if auth_name != 'elixir':
    -        user_info['auth_id'] = f'{user_info["email"]}::{auth_name}'
    +        user_info['auth_id'] = f'{user_info["email"]}::{auth_name}'
         else:
             user_info['auth_id'] = token['sub']
         if not user.do_login(user_info['auth_id']):
             user.add_new_user(user_info)
             user.do_login(user_info['auth_id'])
     
    -    return flask.redirect('/')
    + response = flask.redirect(flask.session['incoming_url']) + del flask.session['incoming_url'] + response.set_cookie('loggedIn', 'true', max_age=datetime.timedelta(days=31)) + return response
    # requests @@ -169,19 +179,22 @@

    Source code for app

             flask.abort(status=400)
     
         if 'api-user' not in indata or 'api-key' not in indata:
    -        logging.debug('API key login - bad keys: %s', indata)
    +        app.logger.debug('API key login - bad keys: %s', indata)
             return flask.Response(status=400)
         utils.verify_api_key(indata['api-user'], indata['api-key'])
         user.do_login(auth_id=indata['api-user'])
    -    return flask.Response(status=200)
    + response = flask.Response(status=200) + response.set_cookie('loggedIn', 'true', max_age=datetime.timedelta(days=31)) + return response
    [docs]@app.route('/api/v1/logout/') def logout(): """Log out the current user.""" flask.session.clear() - response = flask.redirect('/', code=302) + response = flask.Response(status=200) response.set_cookie('_csrf_token', utils.gen_csrf_token(), 0) + response.set_cookie('loggedIn', 'false', 0) return response
    @@ -212,6 +225,11 @@

    Source code for app

     # to allow coverage check for testing
     if __name__ == '__main__':
         app.run(host='0.0.0.0', port=5000)
    +else:
    +    # Assume this means it's handled by gunicorn
    +    gunicorn_logger = logging.getLogger('gunicorn.error')
    +    app.logger.handlers = gunicorn_logger.handlers
    +    app.logger.setLevel(gunicorn_logger.level)
     
    @@ -252,7 +270,7 @@

    Navigation

    diff --git a/docs/build/html/modules/collection.html b/docs/build/html/modules/collection.html index c511007c..7ac46d7d 100644 --- a/docs/build/html/modules/collection.html +++ b/docs/build/html/modules/collection.html @@ -39,7 +39,6 @@

    Navigation

    Source code for collection

     """Collection requests."""
     import json
    -import logging
     
     import flask
     
    @@ -53,7 +52,10 @@ 

    Source code for collection

     
    [docs]@blueprint.route('/', methods=['GET']) def list_collection(): """Provide a simplified list of all available collections.""" - results = list(flask.g.db['collections'].find()) + results = list(flask.g.db['collections'].find(projection={'title': 1, + '_id': 1, + 'tags': 1, + 'properties': 1})) return utils.response_json({'collections': results})
    @@ -77,7 +79,8 @@

    Source code for collection

             if not flask.g.current_user or\
                (not user.has_permission('DATA_MANAGEMENT') or
                 flask.g.current_user['_id'] not in result['editors']):
    -            logging.debug('Not allowed to access editors field %s', flask.g.current_user)
    +            flask.current_app.logger.debug('Not allowed to access editors field %s',
    +                                           flask.g.current_user)
                 del result['editors']
     
                 # return {_id, _title} for datasets
    @@ -112,7 +115,8 @@ 

    Source code for collection

         if not flask.g.current_user or\
            (not user.has_permission('DATA_MANAGEMENT') and
             flask.g.current_user['_id'] not in result['editors']):
    -        logging.debug('Not allowed to access editors field %s', flask.g.current_user)
    +        flask.current_app.logger.debug('Not allowed to access editors field %s',
    +                                       flask.g.current_user)
             del result['editors']
         else:
             result['editors'] = utils.user_uuid_data(result['editors'], flask.g.db)
    @@ -125,6 +129,19 @@ 

    Source code for collection

         return utils.response_json({'collection': result})
    +
    [docs]@blueprint.route('/structure/', methods=['GET']) +def get_collection_data_structure(): + """ + Get an empty collection entry. + + Returns: + flask.Response: JSON structure with a list of collections. + """ + empty_collection = structure.collection() + empty_collection['_id'] = '' + return utils.response_json({'collection': empty_collection})
    + +
    [docs]@blueprint.route('/', methods=['POST']) @user.login_required def add_collection(): # pylint: disable=too-many-branches @@ -147,6 +164,11 @@

    Source code for collection

         if not validation[0]:
             flask.abort(status=validation[1])
     
    +    # properties may only be set by users with DATA_MANAGEMENT
    +    if 'properties' in indata:
    +        if not user.has_permission('DATA_MANAGEMENT'):
    +            flask.abort(403)
    +
         if 'title' not in indata:
             flask.abort(status=400)
     
    @@ -154,25 +176,13 @@ 

    Source code for collection

             indata['editors'] = [flask.g.current_user['_id']]
     
         if 'datasets' in indata:
    -        for i, dataset_uuid_str in enumerate(indata['datasets']):
    -            dataset_uuid = utils.str_to_uuid(dataset_uuid_str)
    -            indata['datasets'][i] = dataset_uuid
    -            # allow new ones only if owner or DATA_MANAGEMENT
    -            order_info = flask.g.db['orders'].find_one({'datasets': dataset_uuid})
    -            if not order_info:
    -                flask.abort(status=400)
    -            if not user.has_permission('DATA_MANAGEMENT') and\
    -               flask.g.current_user['_id'] not in order_info['editors'] and\
    -               flask.g.current_user['_id'] not in order_info['authors'] and\
    -               flask.g.current_user['_id'] not in order_info['generators']:
    -                flask.abort(status=400)
    -
    +        indata['datasets'] = [utils.str_to_uuid(value) for value in indata['datasets']]
         collection.update(indata)
     
         # add to db
         result = flask.g.db['collections'].insert_one(collection)
         if not result.acknowledged:
    -        logging.error('Collection insert failed: %s', collection)
    +        flask.current_app.logger.error('Collection insert failed: %s', collection)
         else:
             utils.make_log('collection', 'add', 'Collection added', collection)
     
    @@ -205,7 +215,7 @@ 

    Source code for collection

     
         result = flask.g.db['collections'].delete_one({'_id': ds_uuid})
         if not result.acknowledged:
    -        logging.error('Failed to delete collection %s', ds_uuid)
    +        flask.current_app.logger.error('Failed to delete collection %s', ds_uuid)
             return flask.Response(status=500)
         utils.make_log('collection', 'delete', 'Deleted collection', data={'_id': ds_uuid})
     
    @@ -241,7 +251,7 @@ 

    Source code for collection

         if not user.has_permission('DATA_MANAGEMENT') and \
            flask.g.current_user['_id'] not in collection['editors'] and\
            flask.g.current_user['email'] not in collection['editors']:
    -        logging.debug('Unauthorized update attempt (collection %s, user %s)',
    +        flask.current_app.logger.debug('Unauthorized update attempt (collection %s, user %s)',
                           collection_uuid,
                           flask.g.current_user['_id'])
             flask.abort(status=403)
    @@ -251,27 +261,16 @@ 

    Source code for collection

         if not validation[0]:
             flask.abort(status=validation[1])
     
    -    if indata.get('owners'):
    -        for i, owner in enumerate(indata['owners']):
    -            if utils.is_email(owner):
    -                owner_info = flask.g.db['users'].find_one({'email': owner})
    -                if owner_info:
    -                    indata['owners'][i] = owner_info['_id']
    +    # properties may only be set by users with DATA_MANAGEMENT
    +    if 'properties' in indata:
    +        if not user.has_permission('DATA_MANAGEMENT'):
    +            flask.abort(403)
     
         if 'datasets' in indata:
    -        for i, dataset_uuid_str in enumerate(indata['datasets']):
    -            dataset_uuid = utils.str_to_uuid(dataset_uuid_str)
    -            indata['datasets'][i] = dataset_uuid
    -            # do not reject existing datasets
    -            if dataset_uuid in collection['datasets']:
    -                continue
    -            # allow new ones only if owner or DATA_MANAGEMENT
    -            order_info = flask.g.db['orders'].find_one({'datasets': dataset_uuid})
    -            if not order_info:
    -                flask.abort(status=400)
    -            if not user.has_permission('DATA_MANAGEMENT') and\
    -               flask.g.current_user['_id'] not in order_info['editors']:
    -                flask.abort(status=400)
    +        indata['datasets'] = [utils.str_to_uuid(value) for value in indata['datasets']]
    +
    +    if 'editors' in indata and not indata['editors']:
    +        indata['editors'] = [flask.g.current_user['_id']]
     
         is_different = False
         for field in indata:
    @@ -282,7 +281,7 @@ 

    Source code for collection

         if indata and is_different:
             result = flask.g.db['collections'].update_one({'_id': collection['_id']}, {'$set': indata})
             if not result.acknowledged:
    -            logging.error('Collection update failed: %s', indata)
    +            flask.current_app.logger.error('Collection update failed: %s', indata)
             else:
                 collection.update(indata)
                 utils.make_log('collection', 'edit', 'Collection updated', collection)
    @@ -381,7 +380,7 @@ 

    Navigation

    diff --git a/docs/build/html/modules/config.html b/docs/build/html/modules/config.html index 64c4d23d..1534c2cc 100644 --- a/docs/build/html/modules/config.html +++ b/docs/build/html/modules/config.html @@ -50,7 +50,7 @@

    Source code for config

     import yaml
     
     
    -
    [docs]def read_config(path: str = ''): +
    [docs]def read_config(path: str = ''): """ Look for settings.yaml and parse the settings from there. @@ -79,7 +79,7 @@

    Source code for config

             return yaml.load(in_file, Loader=yaml.FullLoader)
    -
    [docs]def init() -> dict: +
    [docs]def init() -> dict: """ Read the config from a config.yaml file. @@ -106,7 +106,7 @@

    Source code for config

             for oidc_entry in config['oidc']:
                 base_name = oidc_entry.upper()
                 for conf_part in config['oidc'][oidc_entry]:
    -                config[f'{base_name}_{conf_part.upper()}'] = config['oidc'][oidc_entry][conf_part]
    +                config[f'{base_name}_{conf_part.upper()}'] = config['oidc'][oidc_entry][conf_part]
         config['oidc_names'] = config['oidc'].keys()
         del config['oidc']
     
    @@ -154,7 +154,7 @@ 

    Navigation

    diff --git a/docs/build/html/modules/dataset.html b/docs/build/html/modules/dataset.html index 0ac73796..3db52fb3 100644 --- a/docs/build/html/modules/dataset.html +++ b/docs/build/html/modules/dataset.html @@ -38,39 +38,42 @@

    Navigation

    Source code for dataset

     """Dataset requests."""
    +import itertools
     import json
    -import logging
     
     import flask
     
    +import structure
     import utils
     import user
     
     blueprint = flask.Blueprint('dataset', __name__)  # pylint: disable=invalid-name
     
     
    -
    [docs]@blueprint.route('/', methods=['GET']) +
    [docs]@blueprint.route('/', methods=['GET']) def list_datasets(): """Provide a simplified list of all available datasets.""" results = list(flask.g.db['datasets'].find(projection={'title': 1, - '_id': 1})) + '_id': 1, + 'tags': 1, + 'properties': 1})) return utils.response_json({'datasets': results})
    -
    [docs]@blueprint.route('/user/', methods=['GET']) +
    [docs]@blueprint.route('/user/', methods=['GET']) @user.login_required def list_user_data(): """List all datasets belonging to current user.""" - user_orders = list(flask.g.db['orders'].find({'$or': [{'receivers': flask.session['user_id']}, - {'editors': flask.session['user_id']}]}, + user_orders = list(flask.g.db['orders'].find({'editors': flask.session['user_id']}, {'datasets': 1})) - uuids = list(ds for entry in user_orders for ds in entry['datasets']) - user_datasets = list(flask.g.db['datasets'].find({'_id': {'$in': uuids}})) + dataset_uuids = list(itertools.chain.from_iterable(order['datasets'] + for order in user_orders)) + user_datasets = list(flask.g.db['datasets'].find({'_id': {'$in': dataset_uuids}})) return utils.response_json({'datasets': user_datasets})
    -
    [docs]@blueprint.route('/random/', methods=['GET']) +
    [docs]@blueprint.route('/random/', methods=['GET']) @blueprint.route('/random/<int:amount>/', methods=['GET']) def get_random_ds(amount: int = 1): """ @@ -90,7 +93,20 @@

    Source code for dataset

         return utils.response_json({'datasets': results})
    -
    [docs]@blueprint.route('/<identifier>/', methods=['GET']) +
    [docs]@blueprint.route('/structure/', methods=['GET']) +def get_dataset_data_structure(): + """ + Get an empty dataset entry. + + Returns: + flask.Response: JSON structure with a list of datasets. + """ + empty_dataset = structure.dataset() + empty_dataset['_id'] = '' + return utils.response_json({'dataset': empty_dataset})
    + + +
    [docs]@blueprint.route('/<identifier>/', methods=['GET']) def get_dataset(identifier): """ Retrieve the dataset with uuid <identifier>. @@ -107,8 +123,76 @@

    Source code for dataset

             return flask.Response(status=404)
         return utils.response_json({'dataset': result})
    +
    [docs]@blueprint.route('/', methods=['POST']) +@user.login_required +def add_dataset(): # pylint: disable=too-many-branches + """ + Add a dataset to the given order. + + Args: + identifier (str): The order to add the dataset to. + """ + # permissions + try: + indata = flask.json.loads(flask.request.data) + except json.decoder.JSONDecodeError: + flask.abort(status=400) + if not 'order' in indata: + flask.current_app.logger.debug('Order field missing') + flask.abort(status=400) + try: + order_uuid = utils.str_to_uuid(indata['order']) + except ValueError: + flask.current_app.logger.debug('Incorrect order UUID (%s)', indata['order']) + flask.abort(status=400) + order = flask.g.db['orders'].find_one({'_id': order_uuid}) + if not order: + flask.current_app.logger.debug('Order (%s) not in db', indata['order']) + flask.abort(status=400) + if not (user.has_permission('DATA_MANAGEMENT') or + flask.g.current_user['_id'] in order['editors']): + return flask.abort(status=403) + del indata['order'] + + # properties may only be set by users with DATA_MANAGEMENT + if 'properties' in indata: + if not user.has_permission('DATA_MANAGEMENT'): + flask.abort(403) + + # create new dataset + dataset = structure.dataset() + validation = utils.basic_check_indata(indata, dataset, ['_id']) + if not validation.result: + flask.abort(status=validation.status) + dataset.update(indata) + + # add to db + result_ds = flask.g.db['datasets'].insert_one(dataset) + if not result_ds.acknowledged: + flask.current_app.logger.error('Dataset insert failed: %s', dataset) + else: + utils.make_log('dataset', + 'add', + f'Dataset added for order {order_uuid}', + dataset) + + result_o = flask.g.db['orders'].update_one({'_id': order_uuid}, + {'$push': {'datasets': dataset['_id']}}) + if not result_o.acknowledged: + flask.current_app.logger.error('Order %s insert failed: ADD dataset %s', + order_uuid, dataset['_id']) + else: + order = flask.g.db['orders'].find_one({'_id': order_uuid}) + + utils.make_log('order', + 'edit', + f'Dataset {result_ds.inserted_id} added for order', + order) + + return utils.response_json({'_id': result_ds.inserted_id})
    + -
    [docs]@blueprint.route('/<identifier>/', methods=['DELETE']) +
    [docs]@blueprint.route('/<identifier>/', methods=['DELETE']) @user.login_required def delete_dataset(identifier: str): """ @@ -135,7 +219,7 @@

    Source code for dataset

     
         result = flask.g.db['datasets'].delete_one({'_id': ds_uuid})
         if not result.acknowledged:
    -        logging.error('Failed to delete dataset %s', ds_uuid)
    +        flask.current_app.logger.error('Failed to delete dataset %s', ds_uuid)
             return flask.Response(status=500)
         utils.make_log('dataset', 'delete', 'Deleted dataset', data={'_id': ds_uuid})
     
    @@ -143,26 +227,26 @@ 

    Source code for dataset

             result = flask.g.db['orders'].update_one({'_id': entry['_id']},
                                                      {'$pull': {'datasets': ds_uuid}})
             if not result.acknowledged:
    -            logging.error('Failed to delete dataset %s in order %s',
    +            flask.current_app.logger.error('Failed to delete dataset %s in order %s',
                               ds_uuid, entry['_id'])
                 return flask.Response(status=500)
             new_data = flask.g.db['orders'].find_one({'_id': entry['_id']})
    -        utils.make_log('order', 'edit', f'Deleted dataset {ds_uuid}', new_data)
    +        utils.make_log('order', 'edit', f'Deleted dataset {ds_uuid}', new_data)
     
         for entry in flask.g.db['collections'].find({'datasets': ds_uuid}):
             flask.g.db['collections'].update_one({'_id': entry['_id']},
                                                  {'$pull': {'datasets': ds_uuid}})
             if not result.acknowledged:
    -            logging.error('Failed to delete dataset %s in project %s',
    +            flask.current_app.logger.error('Failed to delete dataset %s in project %s',
                               ds_uuid, entry['_id'])
                 return flask.Response(status=500)
             new_data = flask.g.db['collections'].find_one({'_id': entry['_id']})
    -        utils.make_log('collection', 'edit', f'Deleted dataset {ds_uuid}', new_data)
    +        utils.make_log('collection', 'edit', f'Deleted dataset {ds_uuid}', new_data)
     
         return flask.Response(status=200)
    -
    [docs]@blueprint.route('/<identifier>/', methods=['PATCH']) +
    [docs]@blueprint.route('/<identifier>/', methods=['PATCH']) @user.login_required def update_dataset(identifier): """ @@ -196,6 +280,11 @@

    Source code for dataset

         if not validation[0]:
             flask.abort(status=validation[1])
     
    +    # properties may only be set by users with DATA_MANAGEMENT
    +    if 'properties' in indata:
    +        if not user.has_permission('DATA_MANAGEMENT'):
    +            flask.abort(403)
    +
         is_different = False
         for field in indata:
             if indata[field] != dataset[field]:
    @@ -205,7 +294,7 @@ 

    Source code for dataset

         if is_different:
             result = flask.g.db['datasets'].update_one({'_id': dataset['_id']}, {'$set': indata})
             if not result.acknowledged:
    -            logging.error('Dataset update failed: %s', dataset)
    +            flask.current_app.logger.error('Dataset update failed: %s', dataset)
                 flask.abort(status=500)
             else:
                 dataset.update(indata)
    @@ -214,7 +303,7 @@ 

    Source code for dataset

         return flask.Response(status=200)
    -
    [docs]@blueprint.route('/<identifier>/log/', methods=['GET']) +
    [docs]@blueprint.route('/<identifier>/log/', methods=['GET']) @user.login_required def get_dataset_log(identifier: str = None): """ @@ -252,7 +341,7 @@

    Source code for dataset

     
     
     # helper functions
    -
    [docs]def build_dataset_info(identifier: str): +
    [docs]def build_dataset_info(identifier: str): """ Query for a dataset from the database. @@ -270,6 +359,10 @@

    Source code for dataset

         if not dataset:
             return None
         order = flask.g.db['orders'].find_one({'datasets': dataset_uuid})
    +
    +    if (user.has_permission('DATA_MANAGEMENT') or\
    +        flask.g.db.current_user['id'] in order['editors']):
    +        dataset['order'] = order['_id']
         dataset['related'] = list(flask.g.db['datasets'].find({'_id': {'$in': order['datasets']}},
                                                               {'title': 1}))
         dataset['related'].remove({'_id': dataset['_id'], 'title': dataset['title']})
    @@ -325,7 +418,7 @@ 

    Navigation

    diff --git a/docs/build/html/modules/developer.html b/docs/build/html/modules/developer.html index 105ba18c..cfc4cb8c 100644 --- a/docs/build/html/modules/developer.html +++ b/docs/build/html/modules/developer.html @@ -39,6 +39,7 @@

    Navigation

    Source code for developer

     """Routes and functions intended to aid development and testing."""
     import copy
    +import datetime
     
     import flask
     
    @@ -47,7 +48,7 @@ 

    Source code for developer

     blueprint = flask.Blueprint('developer', __name__)  # pylint: disable=invalid-name
     
     
    -
    [docs]@blueprint.route('/login/<identifier>') +
    [docs]@blueprint.route('/login/<identifier>') def login(identifier: str): """ Log in without password. @@ -57,24 +58,26 @@

    Source code for developer

         """
         res = user.do_login(auth_id=identifier)
         if res:
    -        return flask.Response(status=200)
    +        response = flask.Response(status=200)
    +        response.set_cookie('loggedIn', 'true', max_age=datetime.timedelta(days=31))
    +        return response
         return flask.Response(status=500)
    -
    [docs]@blueprint.route('/hello') +
    [docs]@blueprint.route('/hello') def api_hello(): """Test request.""" return flask.jsonify({'test': 'success'})
    -
    [docs]@blueprint.route('/loginhello') +
    [docs]@blueprint.route('/loginhello') @user.login_required def login_hello(): """Test request requiring login.""" return flask.jsonify({'test': 'success'})
    -
    [docs]@blueprint.route('/hello/<permission>') +
    [docs]@blueprint.route('/hello/<permission>') def permission_hello(permission: str): """ Test request requiring the given permission. @@ -88,13 +91,13 @@

    Source code for developer

         return flask.jsonify({'test': 'success'})
    -
    [docs]@blueprint.route('/csrftest', methods=['POST', 'PATCH', 'POST', 'DELETE']) +
    [docs]@blueprint.route('/csrftest', methods=['POST', 'PATCH', 'POST', 'DELETE']) def csrf_test(): """Test csrf tokens.""" return flask.jsonify({'test': 'success'})
    -
    [docs]@blueprint.route('/test_datasets') +
    [docs]@blueprint.route('/test_datasets') def get_added_ds(): """Get datasets added during testing.""" added = list(flask.g.db['datasets'].find({'description': 'Test dataset'}, @@ -102,7 +105,7 @@

    Source code for developer

         return flask.jsonify({'datasets': added})
    -
    [docs]@blueprint.route('/session') +
    [docs]@blueprint.route('/session') def list_session(): """List all session variables.""" session = copy.deepcopy(flask.session) @@ -111,7 +114,7 @@

    Source code for developer

         return flask.jsonify(session)
    -
    [docs]@blueprint.route('/user/me') +
    [docs]@blueprint.route('/user/me') def list_current_user(): """List all session variables.""" current_user = flask.g.current_user @@ -120,7 +123,7 @@

    Source code for developer

         return flask.jsonify(current_user)
    -
    [docs]@blueprint.route('/config') +
    [docs]@blueprint.route('/config') def list_config(): """List all session variables.""" config = copy.deepcopy(flask.current_app.config) @@ -129,7 +132,7 @@

    Source code for developer

         return flask.jsonify(config)
    -
    [docs]@blueprint.route('/quit') +
    [docs]@blueprint.route('/quit') def stop_server(): """Shutdown the flask server.""" flask.request.environ.get('werkzeug.server.shutdown')() @@ -174,7 +177,7 @@

    Navigation

    diff --git a/docs/build/html/modules/index.html b/docs/build/html/modules/index.html index e31bf0c1..5fcbd595 100644 --- a/docs/build/html/modules/index.html +++ b/docs/build/html/modules/index.html @@ -42,6 +42,7 @@

    All modules for which code is available

  • dataset
  • developer
  • order
  • +
  • project
  • structure
  • user
  • utils
  • @@ -85,7 +86,7 @@

    Navigation

    diff --git a/docs/build/html/modules/order.html b/docs/build/html/modules/order.html index 4bb924c8..8589d2d6 100644 --- a/docs/build/html/modules/order.html +++ b/docs/build/html/modules/order.html @@ -46,7 +46,6 @@

    Source code for order

     * If you have permission ``DATA_MANAGER`` you have CRUD access to any orders.
     """
     import json
    -import logging
     
     import flask
     
    @@ -57,7 +56,7 @@ 

    Source code for order

     blueprint = flask.Blueprint('order', __name__)  # pylint: disable=invalid-name
     
     
    -
    [docs]@blueprint.before_request +
    [docs]@blueprint.before_request def prepare(): """ All order request require ``ORDERS``. @@ -70,7 +69,7 @@

    Source code for order

             flask.abort(status=403)
    -
    [docs]@blueprint.route('/', methods=['GET']) +
    [docs]@blueprint.route('/', methods=['GET']) def list_orders(): """ List all orders visible to the current user. @@ -80,7 +79,9 @@

    Source code for order

         """
         if user.has_permission('DATA_MANAGEMENT'):
             orders = list(flask.g.db['orders'].find(projection={'_id': 1,
    -                                                            'title': 1}))
    +                                                            'title': 1,
    +                                                            'tags': 1,
    +                                                            'properties': 1}))
         else:
             orders = list(flask.g.db['orders']
                           .find({'editors': flask.g.current_user['_id']},
    @@ -90,7 +91,7 @@ 

    Source code for order

         return utils.response_json({'orders': orders})
    -
    [docs]@blueprint.route('/structure/', methods=['GET']) +
    [docs]@blueprint.route('/structure/', methods=['GET']) def get_order_data_structure(): """ Get an empty order entry. @@ -103,7 +104,7 @@

    Source code for order

         return utils.response_json({'order': empty_order})
    -
    [docs]@blueprint.route('/user/', defaults={'user_id': None}, methods=['GET']) +
    [docs]@blueprint.route('/user/', defaults={'user_id': None}, methods=['GET']) @blueprint.route('/user/<user_id>/', methods=['GET']) def list_orders_user(user_id: str): """ @@ -133,7 +134,7 @@

    Source code for order

         return utils.response_json({'orders': orders})
    -
    [docs]@blueprint.route('/<identifier>/', methods=['GET']) +
    [docs]@blueprint.route('/<identifier>/', methods=['GET']) def get_order(identifier): """ Retrieve the order with the provided uuid. @@ -162,7 +163,7 @@

    Source code for order

         return utils.response_json({'order': order_data})
    -
    [docs]@blueprint.route('/<identifier>/log/', methods=['GET']) +
    [docs]@blueprint.route('/<identifier>/log/', methods=['GET']) def get_order_logs(identifier): """ List changes to the dataset. @@ -198,7 +199,7 @@

    Source code for order

                                     'logs': order_logs})
    -
    [docs]@blueprint.route('/base/', methods=['GET']) +
    [docs]@blueprint.route('/base/', methods=['GET']) def get_empty_order(): """ Provide the basic data structure for an empty order. @@ -213,7 +214,7 @@

    Source code for order

         return utils.response_json({'order': order})
    -
    [docs]@blueprint.route('/', methods=['POST']) +
    [docs]@blueprint.route('/', methods=['POST']) def add_order(): """ Add an order. @@ -226,92 +227,42 @@

    Source code for order

         try:
             indata = flask.json.loads(flask.request.data)
         except json.decoder.JSONDecodeError:
    -        logging.debug('Bad json')
    +        flask.current_app.logger.debug('Bad json')
             flask.abort(status=400)
     
         validation = utils.basic_check_indata(indata, new_order, ['_id', 'datasets'])
         if not validation.result:
             flask.abort(status=validation.status)
     
    +    # properties may only be set by users with DATA_MANAGEMENT
    +    if 'properties' in indata:
    +        if not user.has_permission('DATA_MANAGEMENT'):
    +            flask.abort(403)
    +
    +    # convert all incoming uuids to uuid.UUID
         for field in ('editors', 'authors', 'generators'):
             if field in indata:
                 indata[field] = [utils.str_to_uuid(entry) for entry in indata[field]]
         if 'organisation' in indata:
    -        indata['organisation'] = utils.str_to_uuid(indata['organisation'])
    +        if indata['organisation']:
    +            indata['organisation'] = utils.str_to_uuid(indata['organisation'])
     
         new_order.update(indata)
     
    -    if flask.g.current_user['_id'] not in new_order['editors']:
    +    if not new_order['editors']:
             new_order['editors'].append(flask.g.current_user['_id'])
     
         # add to db
         result = flask.g.db['orders'].insert_one(new_order)
         if not result.acknowledged:
    -        logging.error('Order insert failed: %s', new_order)
    +        flask.current_app.logger.error('Order insert failed: %s', new_order)
         else:
             utils.make_log('order', 'add', 'Order added', new_order)
     
         return utils.response_json({'_id': result.inserted_id})
    -
    [docs]@blueprint.route('/<identifier>/dataset/', methods=['POST']) -def add_dataset(identifier): # pylint: disable=too-many-branches - """ - Add a dataset to the given order. - - Args: - identifier (str): The order to add the dataset to. - """ - # permissions - try: - order_uuid = utils.str_to_uuid(identifier) - except ValueError: - flask.abort(status=404) - order = flask.g.db['orders'].find_one({'_id': order_uuid}) - if not order: - flask.abort(status=404) - if not (user.has_permission('DATA_MANAGEMENT') or - flask.g.current_user['_id'] in order['editors']): - return flask.abort(status=403) - - # create new dataset - dataset = structure.dataset() - try: - indata = flask.json.loads(flask.request.data) - except json.decoder.JSONDecodeError: - flask.abort(status=400) - - validation = utils.basic_check_indata(indata, dataset, ['_id']) - if not validation.result: - flask.abort(status=validation.status) - dataset.update(indata) - - # add to db - result_ds = flask.g.db['datasets'].insert_one(dataset) - if not result_ds.acknowledged: - logging.error('Dataset insert failed: %s', dataset) - else: - utils.make_log('dataset', - 'add', - f'Dataset added for order {order_uuid}', - dataset) - - result_o = flask.g.db['orders'].update_one({'_id': order_uuid}, - {'$push': {'datasets': dataset['_id']}}) - if not result_o.acknowledged: - logging.error('Order %s insert failed: ADD dataset %s', order_uuid, dataset['_id']) - else: - order = flask.g.db['orders'].find_one({'_id': order_uuid}) - - utils.make_log('order', - 'update', - f'Dataset {result_ds.inserted_id} added for order', - order) - - return utils.response_json({'_id': result_ds.inserted_id})
    - - -
    [docs]@blueprint.route('/<identifier>/', methods=['DELETE']) +
    [docs]@blueprint.route('/<identifier>/', methods=['DELETE']) def delete_order(identifier: str): """ Delete the order with the given identifier. @@ -333,14 +284,14 @@

    Source code for order

         for dataset_uuid in order['datasets']:
             result = flask.g.db['datasets'].delete_one({'_id': dataset_uuid})
             if not result.acknowledged:
    -            logging.error('Dataset %s delete failed (order %s deletion):',
    +            flask.current_app.logger.error('Dataset %s delete failed (order %s deletion):',
                               dataset_uuid, order_uuid)
                 flask.abort(status=500)
             else:
                 utils.make_log('dataset', 'delete', 'Deleting order', {'_id': dataset_uuid})
         result = flask.g.db['orders'].delete_one(order)
         if not result.acknowledged:
    -        logging.error('Order deletion failed: %s', order_uuid)
    +        flask.current_app.logger.error('Order deletion failed: %s', order_uuid)
             flask.abort(status=500)
         else:
             utils.make_log('order', 'delete', 'Order deleted', {'_id': order_uuid})
    @@ -348,7 +299,7 @@ 

    Source code for order

         return flask.Response(status=200)
    -
    [docs]@blueprint.route('/<identifier>/', methods=['PATCH']) +
    [docs]@blueprint.route('/<identifier>/', methods=['PATCH']) def update_order(identifier: str): # pylint: disable=too-many-branches """ Update an existing order. @@ -379,11 +330,17 @@

    Source code for order

         if not validation.result:
             flask.abort(status=validation.status)
     
    +    # properties may only be set by users with DATA_MANAGEMENT
    +    if 'properties' in indata:
    +        if not user.has_permission('DATA_MANAGEMENT'):
    +            flask.abort(403)
    +
         for field in ('editors', 'authors', 'generators'):
             if field in indata:
                 indata[field] = [utils.str_to_uuid(entry) for entry in indata[field]]
         if 'organisation' in indata:
    -        indata['organisation'] = utils.str_to_uuid(indata['organisation'])
    +        if indata['organisation']:
    +            indata['organisation'] = utils.str_to_uuid(indata['organisation'])
     
         is_different = False
         for field in indata:
    @@ -393,17 +350,20 @@ 

    Source code for order

     
         order.update(indata)
     
    +    if not order['editors']:
    +        order['editors'] = [flask.g.current_user['_id']]
    +
         if is_different:
             result = flask.g.db['orders'].update_one({'_id': order['_id']}, {'$set': order})
             if not result.acknowledged:
    -            logging.error('Order update failed: %s', order)
    +            flask.current_app.logger.error('Order update failed: %s', order)
             else:
                 utils.make_log('order', 'edit', 'Order updated', order)
     
         return flask.Response(status=200)
    -
    [docs]def prepare_order_response(order_data: dict, mongodb): +
    [docs]def prepare_order_response(order_data: dict, mongodb): """ Prepare an order by e.g. converting user uuids to names etc. @@ -416,10 +376,14 @@

    Source code for order

         order_data['authors'] = utils.user_uuid_data(order_data['authors'], mongodb)
         order_data['generators'] = utils.user_uuid_data(order_data['generators'], mongodb)
         order_data['editors'] = utils.user_uuid_data(order_data['editors'], mongodb)
    -    if org_entry := utils.user_uuid_data(order_data['organisation'], mongodb):
    -        order_data['organisation'] = org_entry[0]
    +    if order_data['organisation']:
    +        if org_entry := utils.user_uuid_data(order_data['organisation'], mongodb):
    +            order_data['organisation'] = org_entry[0]
    +        else:
    +            flask.current_app.logger.error('Reference to non-existing organisation: %s',
    +                                           order_data['organisation'])
         else:
    -        order_data['organisation'] = ''
    +        order_data['organisation'] = {}
     
         # convert dataset list into {title, _id}
         order_data['datasets'] = list(mongodb['datasets'].find({'_id': {'$in': order_data['datasets']}},
    @@ -464,7 +428,7 @@ 

    Navigation

    diff --git a/docs/build/html/modules/project.html b/docs/build/html/modules/project.html new file mode 100644 index 00000000..1ef7cc41 --- /dev/null +++ b/docs/build/html/modules/project.html @@ -0,0 +1,401 @@ + + + + + + + + project — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +

    Source code for project

    +"""Project requests."""
    +import json
    +import logging
    +
    +import flask
    +
    +import structure
    +import user
    +import utils
    +
    +blueprint = flask.Blueprint('project', __name__)  # pylint: disable=invalid-name
    +
    +
    +
    [docs]@blueprint.route('/', methods=['GET']) +def list_project(): + """Provide a simplified list of all available projects.""" + results = list(flask.g.db['projects'].find()) + return utils.response_json({'projects': results})
    + + +
    [docs]@blueprint.route('/random/', methods=['GET']) +@blueprint.route('/random/<int:amount>', methods=['GET']) +def get_random(amount: int = 1): + """ + Retrieve random project(s). + + Args: + amount (int): number of requested projects + + Returns: + flask.Request: json structure for the project(s) + + """ + results = list(flask.g.db['projects'].aggregate([{'$sample': {'size': amount}}])) + + for result in results: + # only show owner if owner/admin + if not flask.g.current_user or\ + (not user.has_permission('DATA_MANAGEMENT') and + flask.g.current_user['_id'] not in result['owners'] and + flask.g.current_user['email'] not in result['owners']): + logging.debug('Not allowed to access owners %s', flask.g.current_user) + del result['owners'] + + # return {_id, _title} for datasets + result['datasets'] = [flask.g.db.datasets.find_one({'_id': dataset}, + {'title': 1}) + for dataset in result['datasets']] + return utils.response_json({'projects': results})
    + + +
    [docs]@blueprint.route('/<identifier>/', methods=['GET']) +def get_project(identifier): + """ + Retrieve the project with uuid <identifier>. + + Args: + identifier (str): uuid for the wanted project + + Returns: + flask.Request: json structure for the project + + """ + try: + uuid = utils.str_to_uuid(identifier) + except ValueError: + flask.abort(status=404) + + result = flask.g.db['projects'].find_one({'_id': uuid}) + if not result: + return flask.Response(status=404) + + # only show owner if owner/admin + if not flask.g.current_user or\ + (not user.has_permission('DATA_MANAGEMENT') and + flask.g.current_user['_id'] not in result['owners'] and + flask.g.current_user['email'] not in result['owners']): + logging.debug('Not allowed to access owners %s', flask.g.current_user) + del result['owners'] + else: + for i, owner in enumerate(result['owners']): + if not utils.is_email(owner): + owner_info = flask.g.db['users'].find_one(owner) + result['owners'][i] = owner_info['email'] + + # return {_id, _title} for datasets + result['datasets'] = [flask.g.db.datasets.find_one({'_id': dataset}, + {'title': 1}) + for dataset in result['datasets']] + + return utils.response_json({'project': result})
    + + +
    [docs]@blueprint.route('/', methods=['POST']) +@user.login_required +def add_project(): # pylint: disable=too-many-branches + """ + Add a project. + + Returns: + flask.Response: Json structure with the ``_id`` of the project. + """ + # create new project + project = structure.project() + + try: + indata = flask.json.loads(flask.request.data) + except json.decoder.JSONDecodeError: + flask.abort(status=400) + + # indata validation + validation = utils.basic_check_indata(indata, project, prohibited=('_id')) + if not validation[0]: + flask.abort(status=validation[1]) + + if 'title' not in indata: + flask.abort(status=400) + + if not indata.get('owners'): + indata['owners'] = [flask.g.current_user['_id']] + else: + for i, owner in enumerate(indata['owners']): + if utils.is_email(owner): + owner_info = flask.g.db['users'].find_one({'email': owner}) + if owner_info: + indata['owners'][i] = owner_info['_id'] + + if 'datasets' in indata: + for i, dataset_uuid_str in enumerate(indata['datasets']): + dataset_uuid = utils.str_to_uuid(dataset_uuid_str) + indata['datasets'][i] = dataset_uuid + # allow new ones only if owner or DATA_MANAGEMENT + order_info = flask.g.db['orders'].find_one({'datasets': dataset_uuid}) + if not order_info: + flask.abort(status=400) + if not user.has_permission('DATA_MANAGEMENT') and\ + order_info['creator'] != flask.g.current_user['_id'] and\ + order_info['receiver'] != flask.g.current_user['_id']: + flask.abort(status=400) + + project.update(indata) + + # add to db + result = flask.g.db['projects'].insert_one(project) + if not result.acknowledged: + logging.error('Project insert failed: %s', project) + else: + utils.make_log('project', 'add', 'Project added', project) + + return utils.response_json({'_id': result.inserted_id})
    + + +
    [docs]@blueprint.route('/<identifier>/', methods=['DELETE']) +@user.login_required +def delete_project(identifier: str): + """ + Delete a project. + + Can be deleted only by an owner or user with DATA_MANAGEMENT permissions. + + Args: + identifier (str): The project uuid. + """ + try: + ds_uuid = utils.str_to_uuid(identifier) + except ValueError: + return flask.abort(status=404) + project = flask.g.db['projects'].find_one({'_id': ds_uuid}) + if not project: + flask.abort(status=404) + + # permission check + if not user.has_permission('DATA_MANAGEMENT') and \ + flask.g.current_user['_id'] not in project['owners']: + flask.abort(status=403) + + result = flask.g.db['projects'].delete_one({'_id': ds_uuid}) + if not result.acknowledged: + logging.error(f'Failed to delete project {ds_uuid}') + return flask.Response(status=500) + utils.make_log('project', 'delete', 'Deleted project', data={'_id': ds_uuid}) + + return flask.Response(status=200)
    + + +
    [docs]@blueprint.route('/<identifier>/', methods=['PATCH']) +@user.login_required +def update_project(identifier): # pylint: disable=too-many-branches + """ + Update a project. + + Args: + identifier (str): The project uuid. + + Returns: + flask.Response: Status code. + """ + try: + project_uuid = utils.str_to_uuid(identifier) + except ValueError: + return flask.abort(status=404) + project = flask.g.db['projects'].find_one({'_id': project_uuid}) + if not project: + flask.abort(status=404) + + try: + indata = flask.json.loads(flask.request.data) + except json.decoder.JSONDecodeError: + flask.abort(status=400) + + # permission check + if not user.has_permission('DATA_MANAGEMENT') and \ + flask.g.current_user['_id'] not in project['owners'] and\ + flask.g.current_user['email'] not in project['owners']: + logging.debug('Unauthorized update attempt (project %s, user %s)', + project_uuid, + flask.g.current_user['_id']) + flask.abort(status=403) + + # indata validation + validation = utils.basic_check_indata(indata, project, prohibited=('_id')) + if not validation[0]: + flask.abort(status=validation[1]) + + if indata.get('owners'): + for i, owner in enumerate(indata['owners']): + if utils.is_email(owner): + owner_info = flask.g.db['users'].find_one({'email': owner}) + if owner_info: + indata['owners'][i] = owner_info['_id'] + + if 'datasets' in indata: + for i, dataset_uuid_str in enumerate(indata['datasets']): + dataset_uuid = utils.str_to_uuid(dataset_uuid_str) + indata['datasets'][i] = dataset_uuid + # do not reject existing datasets + if dataset_uuid in project['datasets']: + continue + # allow new ones only if owner or DATA_MANAGEMENT + order_info = flask.g.db['orders'].find_one({'datasets': dataset_uuid}) + if not order_info: + flask.abort(status=400) + if not user.has_permission('DATA_MANAGEMENT') and\ + order_info['creator'] != flask.g.current_user['_id'] and\ + order_info['receiver'] != flask.g.current_user['_id']: + flask.abort(status=400) + + is_different = False + for field in indata: + if indata[field] != project[field]: + is_different = True + break + + if indata and is_different: + result = flask.g.db['projects'].update_one({'_id': project['_id']}, {'$set': indata}) + if not result.acknowledged: + logging.error('Project update failed: %s', indata) + else: + project.update(indata) + utils.make_log('project', 'edit', 'Project updated', project) + + return flask.Response(status=200)
    + + +
    [docs]@blueprint.route('/user/', methods=['GET']) +@user.login_required +def list_user_projects(): # pylint: disable=too-many-branches + """ + List project owned by the user. + + Returns: + flask.Response: JSON structure. + """ + results = list(flask.g.db['projects'] + .find({'$or': [{'owners': flask.g.current_user['_id']}, + {'owners': flask.g.current_user['email']}]})) + logging.debug(results) + return utils.response_json({'projects': results})
    + + +
    [docs]@blueprint.route('/<identifier>/log/', methods=['GET']) +@user.login_required +def get_project_log(identifier: str = None): + """ + Get change logs for the user entry with uuid ``identifier``. + + Can be accessed by owners and admin (DATA_MANAGEMENT). + + Args: + identifier (str): The uuid of the project. + + Returns: + flask.Response: Logs as json. + """ + try: + project_uuid = utils.str_to_uuid(identifier) + except ValueError: + flask.abort(status=404) + + if not user.has_permission('DATA_MANAGEMENT'): + project_data = flask.g.db['projects'].find_one({'_id': project_uuid}) + if not project_data: + flask.abort(403) + if flask.g.current_user['_id'] not in project_data['owners'] and \ + flask.g.current_user['email'] not in project_data['owners']: + flask.abort(403) + + project_logs = list(flask.g.db['logs'].find({'data_type': 'project', 'data._id': project_uuid})) + + for log in project_logs: + del log['data_type'] + + utils.incremental_logs(project_logs) + + return utils.response_json({'entry_id': project_uuid, + 'data_type': 'project', + 'logs': project_logs})
    +
    + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/modules/structure.html b/docs/build/html/modules/structure.html index f9ef4a19..4121cadc 100644 --- a/docs/build/html/modules/structure.html +++ b/docs/build/html/modules/structure.html @@ -46,7 +46,7 @@

    Source code for structure

     import utils
     
     
    -
    [docs]def dataset(): +
    [docs]def dataset(): """ Provide a basic data structure for a dataset document. @@ -57,11 +57,11 @@

    Source code for structure

                 'description': '',
                 'cross_references': [],
                 'title': '',
    -            'tags_standard': {},
    -            'tags_user': {}}
    + 'properties': {}, + 'tags': []}
    -
    [docs]def order(): +
    [docs]def order(): """ Provide a basic data structure for an order document. @@ -76,11 +76,11 @@

    Source code for structure

                 'organisation': '',
                 'editors': [],
                 'datasets': [],
    -            'tags_standard': {},
    -            'tags_user': {}}
    + 'properties': {}, + 'tags': []}
    -
    [docs]def collection(): +
    [docs]def collection(): """ Provide a basic data structure for a project document. @@ -88,16 +88,16 @@

    Source code for structure

             dict: The data structure for a project.
         """
         return {'_id': utils.new_uuid(),
    +            'cross_references': [],
                 'datasets': [],
                 'description': '',
    -            'tags_standard': {},
    -            'tags_user': {},
    -            'cross_references': [],
    +            'properties': {},
    +            'tags': [],
                 'editors': [],
                 'title': ''}
    -
    [docs]def user(): +
    [docs]def user(): """ Provide a basic data structure for a user document. @@ -117,7 +117,7 @@

    Source code for structure

                 'url': ''}
    -
    [docs]def log(): +
    [docs]def log(): """ Provide a basic data structure for a log document. @@ -171,7 +171,7 @@

    Navigation

    diff --git a/docs/build/html/modules/user.html b/docs/build/html/modules/user.html index 812d4afa..bff506ab 100644 --- a/docs/build/html/modules/user.html +++ b/docs/build/html/modules/user.html @@ -53,7 +53,6 @@

    Source code for user

     from itertools import chain
     import functools
     import json
    -import logging
     
     import flask
     
    @@ -71,7 +70,7 @@ 

    Source code for user

     
     
     # Decorators
    -
    [docs]def login_required(func): +
    [docs]def login_required(func): """ Confirm that the user is logged in. @@ -86,13 +85,13 @@

    Source code for user

     
     
     # requests
    -
    [docs]@blueprint.route('/permissions/') +
    [docs]@blueprint.route('/permissions/') def get_permission_info(): """Get a list of all permission types.""" - return flask.jsonify({'permissions': list(PERMISSIONS.keys())})
    + return utils.response_json({'permissions': list(PERMISSIONS.keys())})
    -
    [docs]@blueprint.route('/') +
    [docs]@blueprint.route('/') @login_required def list_users(): """ @@ -115,8 +114,21 @@

    Source code for user

         return utils.response_json({'users': result})
    +
    [docs]@blueprint.route('/structure/', methods=['GET']) +def get_user_data_structure(): + """ + Get an empty user entry. + + Returns: + flask.Response: JSON structure with a list of users. + """ + empty_user = structure.user() + empty_user['_id'] = '' + return utils.response_json({'user': empty_user})
    + + # requests -
    [docs]@blueprint.route('/me/') +
    [docs]@blueprint.route('/me/') def get_current_user_info(): """ List basic information about the current user. @@ -125,7 +137,8 @@

    Source code for user

             flask.Response: json structure for the user
         """
         data = flask.g.current_user
    -    outstructure = {'affiliation': '',
    +    outstructure = {'_id': '',
    +                    'affiliation': '',
                         'auth_ids': [],
                         'email': '',
                         'contact': '',
    @@ -141,7 +154,7 @@ 

    Source code for user

     
     
     # requests
    -
    [docs]@blueprint.route('/me/apikey/', methods=['POST']) +
    [docs]@blueprint.route('/me/apikey/', methods=['POST']) @blueprint.route('/<identifier>/apikey/', methods=['POST']) @login_required def gen_new_api_key(identifier: str = None): @@ -164,7 +177,7 @@

    Source code for user

             except ValueError:
                 flask.abort(status=404)
     
    -        if not (user_data := flask.g.db['users'].find_one({'_id': user_uuid})):  # pylint: disable=superfluous-parens
    +        if not (user_data := flask.g.db['users'].find_one({'_id': user_uuid})):  # pylint: disable=superfluous-parens
                 flask.abort(status=404)
     
         apikey = utils.gen_api_key()
    @@ -174,7 +187,7 @@ 

    Source code for user

         result = flask.g.db['users'].update_one({'_id': user_data['_id']},
                                                 {'$set': new_values})
         if not result.acknowledged:
    -        logging.error('Updating API key for user %s failed', user_data['_id'])
    +        flask.current_app.logger.error('Updating API key for user %s failed', user_data['_id'])
             flask.Response(status=500)
         else:
             utils.make_log('user', 'edit', 'New API key', user_data)
    @@ -182,7 +195,7 @@ 

    Source code for user

         return utils.response_json({'key': apikey.key})
    -
    [docs]@blueprint.route('/<identifier>/', methods=['GET']) +
    [docs]@blueprint.route('/<identifier>/', methods=['GET']) @login_required def get_user_data(identifier: str): """ @@ -202,13 +215,17 @@

    Source code for user

         except ValueError:
             flask.abort(status=404)
     
    -    if not (user_info := flask.g.db['users'].find_one({'_id': user_uuid})):  # pylint: disable=superfluous-parens
    +    if not (user_info := flask.g.db['users'].find_one({'_id': user_uuid})):  # pylint: disable=superfluous-parens
             flask.abort(status=404)
     
    +    # The hash and salt should never leave the system
    +    del user_info['api_key']
    +    del user_info['api_salt']
    +
         return utils.response_json({'user': user_info})
    -
    [docs]@blueprint.route('/', methods=['POST']) +
    [docs]@blueprint.route('/', methods=['POST']) @login_required def add_user(): """ @@ -227,18 +244,31 @@

    Source code for user

             flask.abort(status=400)
         validation = utils.basic_check_indata(indata, new_user, ('_id',
                                                                  'api_key',
    -                                                             'api_salt'))
    -    if not validation[0]:
    -        flask.abort(status=validation[1])
    +                                                             'api_salt',
    +                                                             'auth_ids'))
    +    if not validation.result:
    +        flask.abort(status=validation.status)
     
    -    if 'auth_ids' not in indata:
    +    if 'email' not in indata:
    +        flask.current_app.logger.debug('Email must be set')
             flask.abort(status=400)
     
    +    old_user = flask.g.db['users'].find_one({'email': indata['email']})
    +    if old_user:
    +        flask.current_app.logger.debug('User already exists')
    +        flask.abort(status=400)
    +
    +    if not has_permission('USER_MANAGEMENT') and 'permissions' in indata:
    +        flask.current_app.logger.debug('USER_MANAGEMENT required for permissions')
    +        flask.abort(403)
    +
         new_user.update(indata)
     
    +    new_user['auth_ids'] = [f'{new_user["_id"]}::local']
    +
         result = flask.g.db['users'].insert_one(new_user)
         if not result.acknowledged:
    -        logging.error('User Addition failed: %s', new_user['email'])
    +        flask.current_app.logger.error('User Addition failed: %s', new_user['email'])
             flask.Response(status=500)
         else:
             utils.make_log('user', 'add', 'User added by admin', new_user)
    @@ -246,7 +276,7 @@ 

    Source code for user

         return utils.response_json({'_id': result.inserted_id})
    -
    [docs]@blueprint.route('/<identifier>/', methods=['DELETE']) +
    [docs]@blueprint.route('/<identifier>/', methods=['DELETE']) @login_required def delete_user(identifier: str): """ @@ -271,7 +301,7 @@

    Source code for user

     
         result = flask.g.db['users'].delete_one({'_id': user_uuid})
         if not result.acknowledged:
    -        logging.error('User deletion failed: %s', user_uuid)
    +        flask.current_app.logger.error('User deletion failed: %s', user_uuid)
             flask.Response(status=500)
         else:
             utils.make_log('user', 'delete', 'User delete', {'_id': user_uuid})
    @@ -279,7 +309,7 @@ 

    Source code for user

         return flask.Response(status=200)
    -
    [docs]@blueprint.route('/me/', methods=['PATCH']) +
    [docs]@blueprint.route('/me/', methods=['PATCH']) @login_required def update_current_user_info(): """ @@ -308,7 +338,7 @@

    Source code for user

         result = flask.g.db['users'].update_one({'_id': user_data['_id']},
                                                 {'$set': user_data})
         if not result.acknowledged:
    -        logging.error('User self-update failed: %s', indata)
    +        flask.current_app.logger.error('User self-update failed: %s', indata)
             flask.Response(status=500)
         else:
             utils.make_log('user', 'edit', 'User self-updated', user_data)
    @@ -316,7 +346,7 @@ 

    Source code for user

         return flask.Response(status=200)
    -
    [docs]@blueprint.route('/<identifier>/', methods=['PATCH']) +
    [docs]@blueprint.route('/<identifier>/', methods=['PATCH']) @login_required def update_user_info(identifier: str): """ @@ -336,7 +366,7 @@

    Source code for user

         except ValueError:
             flask.abort(status=404)
     
    -    if not (user_data := flask.g.db['users'].find_one({'_id': user_uuid})):  # pylint: disable=superfluous-parens
    +    if not (user_data := flask.g.db['users'].find_one({'_id': user_uuid})):  # pylint: disable=superfluous-parens
             flask.abort(status=404)
     
         try:
    @@ -345,10 +375,17 @@ 

    Source code for user

             flask.abort(status=400)
         validation = utils.basic_check_indata(indata, user_data, ('_id',
                                                                   'api_key',
    -                                                              'api_salt'))
    +                                                              'api_salt',
    +                                                              'auth_ids'))
     
    -    if not validation[0]:
    -        flask.abort(status=validation[1])
    +    if not validation.result:
    +        flask.abort(status=validation.status)
    +
    +    if 'email' in indata:
    +        old_user = flask.g.db['users'].find_one({'email': indata['email']})
    +        if old_user and old_user['_id'] != user_data['_id']:
    +            flask.current_app.logger.debug('User already exists')
    +            flask.abort(status=400)
     
         # Avoid "updating" and making log if there are no changes
         is_different = False
    @@ -361,7 +398,7 @@ 

    Source code for user

             result = flask.g.db['users'].update_one({'_id': user_data['_id']},
                                                     {'$set': indata})
             if not result.acknowledged:
    -            logging.error('User update failed: %s', indata)
    +            flask.current_app.logger.error('User update failed: %s', indata)
                 flask.Response(status=500)
             else:
                 user_data.update(indata)
    @@ -370,7 +407,7 @@ 

    Source code for user

         return flask.Response(status=200)
    -
    [docs]@blueprint.route('/me/log/', methods=['GET']) +
    [docs]@blueprint.route('/me/log/', methods=['GET']) @blueprint.route('/<identifier>/log/', methods=['GET']) @login_required def get_user_log(identifier: str = None): @@ -408,7 +445,7 @@

    Source code for user

                                     'logs': user_logs})
    -
    [docs]@blueprint.route('/me/actions/', methods=['GET']) +
    [docs]@blueprint.route('/me/actions/', methods=['GET']) @blueprint.route('/<identifier>/actions/', methods=['GET']) @login_required def get_user_actions(identifier: str = None): @@ -445,7 +482,7 @@

    Source code for user

     
     
     # helper functions
    -
    [docs]def add_new_user(user_info: dict): +
    [docs]def add_new_user(user_info: dict): """ Add a new user to the database from first oidc login. @@ -455,19 +492,20 @@

    Source code for user

         Args:
             user_info (dict): Information about the user
         """
    -    email_user = flask.g.db['users'].find_one({'email': user_info['email']})
    -    if email_user:
    -        email_user['auth_ids'].append(user_info['auth_id'])
    +    db_user = flask.g.db['users'].find_one({'email': user_info['email']})
    +    if db_user:
    +        db_user['auth_ids'].append(user_info['auth_id'])
             result = flask.g.db['users'].update_one({'email': user_info['email']},
    -                                                {'$set': {'auth_ids': email_user['auth_ids']}})
    +                                                {'$set': {'auth_ids': db_user['auth_ids']}})
             if not result.acknowledged:
    -            logging.error('Failed to add new auth_id to user with email %s', user_info['email'])
    +            flask.current_app.logger.error('Failed to add new auth_id to user with email %s',
    +                                           user_info['email'])
                 flask.Response(status=500)
             else:
                 utils.make_log('user',
                                'edit',
    -                           'Edit entry to auth_ids to user from OAuth',
    -                           email_user,
    +                           'Add OIDC entry to auth_ids',
    +                           db_user,
                                no_user=True)
     
         else:
    @@ -478,13 +516,14 @@ 

    Source code for user

     
             result = flask.g.db['users'].insert_one(new_user)
             if not result.acknowledged:
    -            logging.error('Failed to add user with email %s via oidc', user_info['email'])
    +            flask.current_app.logger.error('Failed to add user with email %s via oidc',
    +                                           user_info['email'])
                 flask.Response(status=500)
             else:
                 utils.make_log('user', 'add', 'Creating new user from OAuth', new_user, no_user=True)
    -
    [docs]def do_login(auth_id: str): +
    [docs]def do_login(auth_id: str): """ Set all relevant variables for a logged in user. @@ -503,7 +542,7 @@

    Source code for user

         return True
    -
    [docs]def get_current_user(): +
    [docs]def get_current_user(): """ Get the current user. @@ -513,7 +552,7 @@

    Source code for user

         return get_user(user_uuid=flask.session.get('user_id'))
    -
    [docs]def get_user(user_uuid=None): +
    [docs]def get_user(user_uuid=None): """ Get information about the user. @@ -530,7 +569,7 @@

    Source code for user

         return None
    -
    [docs]def has_permission(permission: str): +
    [docs]def has_permission(permission: str): """ Check if the current user permissions fulfills the requirement. @@ -587,7 +626,7 @@

    Navigation

    diff --git a/docs/build/html/modules/utils.html b/docs/build/html/modules/utils.html index 94ece1eb..8e5d5dd5 100644 --- a/docs/build/html/modules/utils.html +++ b/docs/build/html/modules/utils.html @@ -43,11 +43,12 @@

    Source code for utils

     from typing import Any, Union
     import datetime
     import hashlib
    -import logging
    +import html
     import re
     import secrets
     import uuid
     
    +import argon2
     import bson
     import flask
     import pymongo
    @@ -59,7 +60,7 @@ 

    Source code for utils

     ValidationResult = namedtuple('ValidationResult', ['result', 'status'])
     
     
    -
    [docs]def basic_check_indata(indata: dict, +
    [docs]def basic_check_indata(indata: dict, reference_data: dict, prohibited: Union[tuple, list]) -> tuple: """ @@ -78,7 +79,7 @@

    Source code for utils

                 values in ``reference_data``. Defaults to ``None``.
     
         Returns:
    -        tuple: (``bool``: whether the check passed, ``code``: Suggested http code)
    +        namedtuple: (``bool``: whether the check passed, ``code``: Suggested http code)
         """
         if prohibited is None:
             prohibited = []
    @@ -86,23 +87,36 @@ 

    Source code for utils

         if 'title' in reference_data and \
            not reference_data['title'] and \
            not indata.get('title'):
    -        logging.debug('Title empty')
    +        flask.current_app.logger.debug('Title empty')
             return ValidationResult(result=False, status=400)
     
         for key in indata:
             if key in prohibited and indata[key] != reference_data[key]:
    -            logging.debug('Prohibited key (%s) with new value', key)
    +            flask.current_app.logger.debug('Prohibited key (%s) with new value', key)
                 return ValidationResult(result=False, status=403)
             if key not in reference_data:
    -            logging.debug('Bad key (%s)', key)
    -            return ValidationResult(result=False, status=400)
    -        if not validate.validate_field(key, indata[key]):
    +            flask.current_app.logger.debug('Bad key (%s)', key)
                 return ValidationResult(result=False, status=400)
    +        if indata[key] != reference_data[key]:
    +            if not validate.validate_field(key, indata[key]):
    +                return ValidationResult(result=False, status=400)
         return ValidationResult(result=True, status=200)
    +
    [docs]def escape_html(data: str) -> str: + ''' + Escape e.g. html tags for the provided text. + + Args: + data (str): The text to escape. + + Returns: + str: The escaped text. + ''' + return html.escape(data)
    + # csrf -
    [docs]def verify_csrf_token(): +
    [docs]def verify_csrf_token(): """ Compare the csrf token from the request (header) with the one in the cookie.session. @@ -110,11 +124,11 @@

    Source code for utils

         """
         token = flask.request.headers.get('X-CSRFToken')
         if not token or (token != flask.request.cookies.get('_csrf_token')):
    -        logging.warning('Bad csrf token received')
    +        flask.current_app.logger.warning('Bad csrf token received')
             flask.abort(status=400)
    -
    [docs]def gen_csrf_token() -> str: +
    [docs]def gen_csrf_token() -> str: """ Generate a csrf token. @@ -125,7 +139,7 @@

    Source code for utils

     
     
     # API key
    -
    [docs]def gen_api_key(): +
    [docs]def gen_api_key(): """ Generate an API key with salt. @@ -137,7 +151,7 @@

    Source code for utils

                       salt=secrets.token_hex(32))
    -
    [docs]def gen_api_key_hash(api_key: str, salt: str): +
    [docs]def gen_api_key_hash(api_key: str, salt: str): """ Generate a hash of the api_key for storing/comparing to db. @@ -148,11 +162,11 @@

    Source code for utils

         Returns:
             str: SHA512 hash as hex.
         """
    -    ct_bytes = bytes.fromhex(api_key + salt)
    -    return hashlib.sha512(ct_bytes).hexdigest()
    + ph = argon2.PasswordHasher() + return ph.hash(api_key + salt)
    -
    [docs]def verify_api_key(username: str, api_key: str): +
    [docs]def verify_api_key(username: str, api_key: str): """ Verify an API key against the value in the database. @@ -162,22 +176,17 @@

    Source code for utils

             username (str): The username to check.
             api_key (str): The received API key (hex).
         """
    +    ph = argon2.PasswordHasher()
         user_info = flask.g.db['users'].find_one({'auth_ids': username})
         if not user_info:
    -        logging.warning('API key verification failed (bad username)')
    -        flask.abort(status=401)
    -    try:
    -        ct_bytes = bytes.fromhex(api_key + user_info['api_salt'])
    -    except ValueError:
    -        logging.warning('Non-hex API key provided')
    +        flask.current_app.logger.warning('API key verification failed (bad username)')
             flask.abort(status=401)
    -    new_hash = hashlib.sha512(ct_bytes).hexdigest()
    -    if not new_hash == user_info['api_key']:
    -        logging.warning('API key verification failed (bad hash)')
    +    if not ph.verify(user_info['api_key'], api_key + user_info['salt']):
    +        flask.current_app.logger.warning('API key verification failed (bad hash)')
             flask.abort(status=401)
    -
    [docs]def get_dbclient(conf) -> pymongo.mongo_client.MongoClient: +
    [docs]def get_dbclient(conf) -> pymongo.mongo_client.MongoClient: """ Get the connection to the MongoDB database server. @@ -193,7 +202,7 @@

    Source code for utils

                                    password=conf['mongo']['password'])
    -
    [docs]def get_db(dbserver: pymongo.mongo_client.MongoClient, conf) -> pymongo.database.Database: +
    [docs]def get_db(dbserver: pymongo.mongo_client.MongoClient, conf) -> pymongo.database.Database: """ Get the connection to the MongoDB database. @@ -209,7 +218,7 @@

    Source code for utils

                                      codec_options=(codec_options))
    -
    [docs]def new_uuid() -> uuid.UUID: +
    [docs]def new_uuid() -> uuid.UUID: """ Generate a uuid for a field in a MongoDB document. @@ -219,7 +228,7 @@

    Source code for utils

         return uuid.uuid4()
    -
    [docs]def str_to_uuid(in_uuid: Union[str, uuid.UUID]) -> uuid.UUID: +
    [docs]def str_to_uuid(in_uuid: Union[str, uuid.UUID]) -> uuid.UUID: """ Convert str uuid to uuid.UUID. @@ -237,7 +246,7 @@

    Source code for utils

     
     
     # misc
    -
    [docs]def convert_keys_to_camel(chunk: Any) -> Any: +
    [docs]def convert_keys_to_camel(chunk: Any) -> Any: """ Convert keys given in snake_case to camelCase. @@ -269,7 +278,7 @@

    Source code for utils

     REGEX = {'email': re.compile(r'.*@.*\..*')}
     
     
    -
    [docs]def is_email(indata: str): +
    [docs]def is_email(indata: str): """ Check whether a string seems to be an email address or not. @@ -284,7 +293,7 @@

    Source code for utils

         return bool(REGEX['email'].search(indata))
    -
    [docs]def response_json(json_structure: dict): +
    [docs]def response_json(json_structure: dict): """ Convert keys to camelCase and run ``flask.jsonify()``. @@ -298,7 +307,7 @@

    Source code for utils

         return flask.jsonify(data)
    -
    [docs]def make_timestamp(): +
    [docs]def make_timestamp(): """ Generate a timestamp of the current time. @@ -309,7 +318,7 @@

    Source code for utils

     
     
     # pylint: disable=too-many-arguments
    -
    [docs]def make_log(data_type: str, +
    [docs]def make_log(data_type: str, action: str, comment: str, data: dict = None, @@ -349,12 +358,12 @@

    Source code for utils

                     'user': active_user})
         result = flask.g.db['logs'].insert_one(log, session=dbsession)
         if not result.acknowledged:
    -        logging.error(f'Log failed: A:{action} C:{comment} D:{data} ' +
    -                      f'DT: {data_type} U: {flask.g.current_user["_id"]}')
    +        flask.current_app.logger.error(f'Log failed: A:{action} C:{comment} D:{data} ' +
    +                      f'DT: {data_type} U: {flask.g.current_user["_id"]}')
         return result.acknowledged
    -
    [docs]def incremental_logs(logs: list): +
    [docs]def incremental_logs(logs: list): """ Make an incremental log. @@ -373,7 +382,7 @@

    Source code for utils

                 del logs[i]['data'][key]
    -
    [docs]def check_email_uuid(user_identifier: str) -> Union[str, uuid.UUID]: +
    [docs]def check_email_uuid(user_identifier: str) -> Union[str, uuid.UUID]: """ Check if the provided user is found in the db as email or _id. @@ -405,7 +414,7 @@

    Source code for utils

         return ''
    -
    [docs]def user_uuid_data(user_ids: Union[str, list, uuid.UUID], +
    [docs]def user_uuid_data(user_ids: Union[str, list, uuid.UUID], mongodb: pymongo.database.Database) -> list: """ Retrieve some extra information about a user using a uuid as input. @@ -473,7 +482,7 @@

    Navigation

    diff --git a/docs/build/html/modules/validate.html b/docs/build/html/modules/validate.html index 81bee4f4..45f5b189 100644 --- a/docs/build/html/modules/validate.html +++ b/docs/build/html/modules/validate.html @@ -43,13 +43,14 @@

    Source code for validate

     Indata can be sent to ``validate_field``, which will use the corresponding
     functions to check each field.
     """
    -import logging
     from typing import Any, Union
     import uuid
     
     import flask
     
    -from user import PERMISSIONS
    +import exceptions
    +
    +import user
     import utils
     
     
    @@ -75,10 +76,13 @@ 

    Source code for validate

         try:
             VALIDATION_MAPPER[field_key](field_value)
         except KeyError:
    -        logging.debug('Unknown key: %s', field_key)
    +        flask.current_app.logger.debug('Unknown key: %s', field_key)
             return False
         except ValueError as err:
    -        logging.debug('Indata validation failed: %s - %s', field_key, err)
    +        flask.current_app.logger.debug('Indata validation failed: %s - %s', field_key, err)
    +        return False
    +    except exceptions.AuthError as err:
    +        flask.current_app.logger.debug('Permission failed: %s - %s', field_key, err)
             return False
         return True
    @@ -99,16 +103,16 @@

    Source code for validate

             ValueError: Validation failed.
         """
         if not isinstance(data, list):
    -        raise ValueError(f'Must be list ({data})')
    +        raise ValueError(f'Must be list ({data})')
         for ds_entry in data:
             if not isinstance(ds_entry, str):
    -            raise ValueError(f'Must be str ({ds_entry})')
    +            raise ValueError(f'Must be str ({ds_entry})')
             try:
                 ds_uuid = uuid.UUID(ds_entry)
             except ValueError as err:
    -            raise ValueError(f'Not a valid uuid ({data})') from err
    +            raise ValueError(f'Not a valid uuid ({data})') from err
             if not flask.g.db['datasets'].find_one({'_id': ds_uuid}):
    -            raise ValueError(f'Uuid not in db ({data})')
    +            raise ValueError(f'Uuid not in db ({data})')
             return True
    @@ -128,9 +132,9 @@

    Source code for validate

             ValueError: Validation failed.
         """
         if not isinstance(data, str):
    -        raise ValueError(f'Not a string ({data})')
    +        raise ValueError(f'Not a string ({data})')
         if not utils.is_email(data):
    -        raise ValueError(f'Not a valid email address ({data})')
    +        raise ValueError(f'Not a valid email address ({data})')
         return True
    @@ -148,10 +152,10 @@

    Source code for validate

             ValueError: Validation failed.
         """
         if not isinstance(data, list):
    -        raise ValueError(f'Not a list ({data})')
    +        raise ValueError(f'Not a list ({data})')
         for entry in data:
             if not isinstance(entry, str):
    -            raise ValueError(f'Not a string ({entry})')
    +            raise ValueError(f'Not a string ({entry})')
         return True
    @@ -173,8 +177,8 @@

    Source code for validate

         if not isinstance(data, list):
             raise ValueError('Must be a list')
         for entry in data:
    -        if entry not in PERMISSIONS:
    -            raise ValueError(f'Bad entry ({entry})')
    +        if entry not in user.PERMISSIONS:
    +            raise ValueError(f'Bad entry ({entry})')
         return True
    @@ -192,15 +196,15 @@

    Source code for validate

             ValueError: Validation failed.
         """
         if not isinstance(data, str):
    -        raise ValueError(f'Not a string ({data})')
    +        raise ValueError(f'Not a string ({data})')
         return True
    -
    [docs]def validate_tags_std(data: dict) -> bool: +
    [docs]def validate_cross_references(data: list) -> bool: """ - Validate input for the ``tags_standard`` field. + Validate input for the ``cross_references`` field. - It must be a dict. + It must be a list. Args: data (dict): The data to be validated. @@ -211,19 +215,24 @@

    Source code for validate

         Raises:
             ValueError: Validation failed.
         """
    -    if not isinstance(data, dict):
    -        raise ValueError(f'Not a  dict ({data})')
    -    for key in data:
    -        if not isinstance(key, str) or not isinstance(data[key], str):
    -            raise ValueError(f'Keys and values must be strings ({key}, {data[key]})')
    +    if not isinstance(data, list):
    +        raise ValueError(f'Not a  list ({data})')
    +    for entry in data:
    +        if not isinstance(entry, dict):
    +            raise ValueError(f'List entries must be dicts ({entry})')
    +        if list(entry.keys()) != ['title', 'value']:
    +            raise KeyError(f'Incorrect keys ({entry.keys})')
    +        if not isinstance(entry['title'], str) or \
    +           not isinstance(entry['value'], str):
    +            raise ValueError(f'Values must be strings ({entry.values()})')
         return True
    -
    [docs]def validate_tags_user(data: dict) -> bool: +
    [docs]def validate_properties(data: dict) -> bool: """ - Validate input for the ``tags_user`` field. + Validate input for the ``properties`` field. - It must be a dict. + It must be a dict. The user must have DATA_MANAGEMENT permissions. Args: data (dict): The data to be validated. @@ -234,11 +243,36 @@

    Source code for validate

         Raises:
             ValueError: Validation failed.
         """
    +    if not user.has_permission('DATA_MANAGEMENT'):
    +        raise exceptions.AuthError('Permission DATA_MANAGEMENT required')
         if not isinstance(data, dict):
    -        raise ValueError(f'Not a  dict ({data})')
    +        raise ValueError(f'Not a  dict ({data})')
         for key in data:
             if not isinstance(key, str) or not isinstance(data[key], str):
    -            raise ValueError(f'Keys and values must be strings ({key}, {data[key]})')
    +            raise ValueError(f'Keys and values must be strings ({key}, {data[key]})')
    +    return True
    + + +
    [docs]def validate_tags(data: Union[tuple, list]) -> bool: + """ + Validate input for the ``tags`` field. + + It must be a list or tuple. + + Args: + data (dict): The data to be validated. + + Returns: + bool: Validation passed. + + Raises: + ValueError: Validation failed. + """ + if not isinstance(data, list) and not isinstance(data, tuple): + raise ValueError(f'Not a list ({data})') + for value in data: + if not isinstance(value, str): + raise ValueError(f'All list entries must be str ({value})') return True
    @@ -279,7 +313,7 @@

    Source code for validate

         """
         if not isinstance(data, str):
             raise ValueError('Must be a string')
    -    if not data.startswith('http://') and not data.startswith('https://'):
    +    if data and not data.startswith('http://') and not data.startswith('https://'):
             raise ValueError('URLs must start with http(s)://')
         return True
    @@ -300,18 +334,21 @@

    Source code for validate

             ValueError: Validation failed.
         """
         if not isinstance(data, str):
    -        raise ValueError(f'Bad data type (must be str): {data}')
    +        raise ValueError(f'Bad data type (must be str): {data}')
    +
    +    if not data:
    +        return True
     
         try:
             user_uuid = uuid.UUID(data)
         except ValueError as err:
    -        raise ValueError(f'Not a valid uuid ({data})') from err
    +        raise ValueError(f'Not a valid uuid ({data})') from err
         if not flask.g.db['users'].find_one({'_id': user_uuid}):
    -        raise ValueError(f'Uuid not in db ({data})')
    +        raise ValueError(f'Uuid not in db ({data})')
         return True
    -
    [docs]def validate_user_list(data: Union[str, list]) -> bool: +
    [docs]def validate_user_list(data: Union[tuple, list]) -> bool: """ Validate input for a field containing a list of user uuid(s). @@ -330,15 +367,15 @@

    Source code for validate

             ValueError: Validation failed.
         """
         if not isinstance(data, list):
    -        raise ValueError(f'Bad data type (must be list): {data}')
    +        raise ValueError(f'Bad data type (must be list): {data}')
     
         for u_uuid in data:
             try:
                 user_uuid = uuid.UUID(u_uuid)
             except ValueError as err:
    -            raise ValueError(f'Not a valid uuid ({data})') from err
    +            raise ValueError(f'Not a valid uuid ({data})') from err
             if not flask.g.db['users'].find_one({'_id': user_uuid}):
    -            raise ValueError(f'Uuid not in db ({data})')
    +            raise ValueError(f'Uuid not in db ({data})')
         return True
    @@ -346,6 +383,7 @@

    Source code for validate

                          'auth_ids': validate_list_of_strings,
                          'authors': validate_user_list,
                          'contact': validate_string,
    +                     'cross_references': validate_cross_references,
                          'description': validate_string,
                          'datasets': validate_datasets,
                          'editors': validate_user_list,
    @@ -355,8 +393,8 @@ 

    Source code for validate

                          'orcid': validate_string,
                          'organisation': validate_user,
                          'permissions': validate_permissions,
    -                     'tags_standard': validate_tags_std,
    -                     'tags_user': validate_tags_user,
    +                     'properties': validate_properties,
    +                     'tags': validate_tags,
                          'title': validate_title,
                          'url': validate_url}
     
    @@ -399,7 +437,7 @@

    Navigation

    diff --git a/docs/build/html/objects.inv b/docs/build/html/objects.inv index 27d499ca..3abbface 100644 Binary files a/docs/build/html/objects.inv and b/docs/build/html/objects.inv differ diff --git a/docs/build/html/order.html b/docs/build/html/order.html new file mode 100644 index 00000000..46cf43ab --- /dev/null +++ b/docs/build/html/order.html @@ -0,0 +1,260 @@ + + + + + + + + order module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    order module

    +

    Functions and request handlers related to orders.

    +

    Special permissions are required to access orders:

    +
      +
    • If you have permission ORDERS you have CRUD access to your own orders.

    • +
    • If you have permission DATA_MANAGER you have CRUD access to any orders.

    • +
    +
    +
    +order.add_order()[source]
    +

    Add an order.

    +
    +
    Returns
    +

    Json structure with _id of the added order.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.delete_order(identifier: str)[source]
    +

    Delete the order with the given identifier.

    +
    +
    Returns
    +

    Status code

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.get_empty_order()[source]
    +

    Provide the basic data structure for an empty order.

    +
    +
    Returns
    +

    Json structure of an empty order.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.get_order(identifier)[source]
    +

    Retrieve the order with the provided uuid.

    +

    order['datasets'] is returned as [{_id, title}, ...].

    +
    +
    Parameters
    +

    identifier (str) – Uuid for the wanted order.

    +
    +
    Returns
    +

    Json structure for the order.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.get_order_data_structure()[source]
    +

    Get an empty order entry.

    +
    +
    Returns
    +

    JSON structure with a list of orders.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.get_order_logs(identifier)[source]
    +

    List changes to the dataset.

    +

    Logs will be sorted chronologically.

    +

    The data in each log will be trimmed to only show the changed fields.

    +
    +
    Parameters
    +

    identifier (str) – Uuid for the wanted order.

    +
    +
    Returns
    +

    Json structure for the logs.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.list_orders()[source]
    +

    List all orders visible to the current user.

    +
    +
    Returns
    +

    JSON structure with a list of orders.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.list_orders_user(user_id: str)[source]
    +

    List all orders belonging to the provided user.

    +
    +
    Parameters
    +

    userid (str) – Uuid of user to find orders for.

    +
    +
    Returns
    +

    Json structure with a list of orders.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +order.prepare()[source]
    +

    All order request require ORDERS.

    +

    Make sure that the user is logged in and has the required permission.

    +
    + +
    +
    +order.prepare_order_response(order_data: dict, mongodb)[source]
    +

    Prepare an order by e.g. converting user uuids to names etc.

    +

    Changes are done in-place.

    +
    +
    Parameters
    +
      +
    • order_data (dict) – The order entry from the db.

    • +
    • mongodb – The mongo database to use.

    • +
    +
    +
    +
    + +
    +
    +order.update_order(identifier: str)[source]
    +

    Update an existing order.

    +
    +
    Parameters
    +

    identifier (str) – Order uuid.

    +
    +
    Returns
    +

    Status code of the request.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/project.html b/docs/build/html/project.html new file mode 100644 index 00000000..eb9aa217 --- /dev/null +++ b/docs/build/html/project.html @@ -0,0 +1,207 @@ + + + + + + + + project module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    project module

    +

    Project requests.

    +
    +
    +project.add_project()[source]
    +

    Add a project.

    +
    +
    Returns
    +

    Json structure with the _id of the project.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +project.delete_project(identifier: str)[source]
    +

    Delete a project.

    +

    Can be deleted only by an owner or user with DATA_MANAGEMENT permissions.

    +
    +
    Parameters
    +

    identifier (str) – The project uuid.

    +
    +
    +
    + +
    +
    +project.get_project(identifier)[source]
    +

    Retrieve the project with uuid <identifier>.

    +
    +
    Parameters
    +

    identifier (str) – uuid for the wanted project

    +
    +
    Returns
    +

    json structure for the project

    +
    +
    Return type
    +

    flask.Request

    +
    +
    +
    + +
    +
    +project.get_project_log(identifier: str = None)[source]
    +

    Get change logs for the user entry with uuid identifier.

    +

    Can be accessed by owners and admin (DATA_MANAGEMENT).

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the project.

    +
    +
    Returns
    +

    Logs as json.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +project.get_random(amount: int = 1)[source]
    +

    Retrieve random project(s).

    +
    +
    Parameters
    +

    amount (int) – number of requested projects

    +
    +
    Returns
    +

    json structure for the project(s)

    +
    +
    Return type
    +

    flask.Request

    +
    +
    +
    + +
    +
    +project.list_project()[source]
    +

    Provide a simplified list of all available projects.

    +
    + +
    +
    +project.list_user_projects()[source]
    +

    List project owned by the user.

    +
    +
    Returns
    +

    JSON structure.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +project.update_project(identifier)[source]
    +

    Update a project.

    +
    +
    Parameters
    +

    identifier (str) – The project uuid.

    +
    +
    Returns
    +

    Status code.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/py-modindex.html b/docs/build/html/py-modindex.html index 251ae72a..e8f2fd1a 100644 --- a/docs/build/html/py-modindex.html +++ b/docs/build/html/py-modindex.html @@ -50,6 +50,7 @@

    Python Module Index

    c | d | o | + p | s | u | v @@ -75,7 +76,7 @@

    Python Module Index

    - config + config
     
    @@ -83,12 +84,12 @@

    Python Module Index

    - dataset + dataset
    - developer + developer
     
    @@ -96,7 +97,15 @@

    Python Module Index

    - order + order +
     
    + p
    + project
     
    @@ -104,7 +113,7 @@

    Python Module Index

    - structure + structure
     
    @@ -112,12 +121,12 @@

    Python Module Index

    - user + user
    - utils + utils
     
    @@ -167,7 +176,7 @@

    Navigation

    diff --git a/docs/build/html/search.html b/docs/build/html/search.html index e16ca5bf..9a9dde2b 100644 --- a/docs/build/html/search.html +++ b/docs/build/html/search.html @@ -89,7 +89,7 @@

    Navigation

    diff --git a/docs/build/html/searchindex.js b/docs/build/html/searchindex.js index 88c5f8c3..e321c287 100644 --- a/docs/build/html/searchindex.js +++ b/docs/build/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["api","code.app","code.collection","code.config","code.dataset","code.developer","code.order","code.structure","code.user","code.utils","code.validate","data_structure","development","development.quick_environment","implementation","index","modules"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,"sphinx.ext.viewcode":1,sphinx:56},filenames:["api.rst","code.app.rst","code.collection.rst","code.config.rst","code.dataset.rst","code.developer.rst","code.order.rst","code.structure.rst","code.user.rst","code.utils.rst","code.validate.rst","data_structure.rst","development.rst","development.quick_environment.md","implementation.rst","index.rst","modules.rst"],objects:{"":{app:[1,0,0,"-"],collection:[2,0,0,"-"],config:[3,0,0,"-"],dataset:[4,0,0,"-"],developer:[5,0,0,"-"],order:[6,0,0,"-"],structure:[7,0,0,"-"],user:[8,0,0,"-"],utils:[9,0,0,"-"],validate:[10,0,0,"-"]},"utils.ValidationResult":{result:[9,3,1,""],status:[9,3,1,""]},app:{api_base:[1,1,1,""],error_bad_request:[1,1,1,""],error_forbidden:[1,1,1,""],error_not_found:[1,1,1,""],error_unauthorized:[1,1,1,""],finalize:[1,1,1,""],key_login:[1,1,1,""],login_types:[1,1,1,""],logout:[1,1,1,""],oidc_authorize:[1,1,1,""],oidc_login:[1,1,1,""],oidc_types:[1,1,1,""],prepare:[1,1,1,""]},collection:{add_collection:[2,1,1,""],delete_collection:[2,1,1,""],get_collection:[2,1,1,""],get_collection_log:[2,1,1,""],get_random:[2,1,1,""],list_collection:[2,1,1,""],list_user_collections:[2,1,1,""],update_collection:[2,1,1,""]},config:{init:[3,1,1,""],read_config:[3,1,1,""]},dataset:{build_dataset_info:[4,1,1,""],delete_dataset:[4,1,1,""],get_dataset:[4,1,1,""],get_dataset_log:[4,1,1,""],get_random_ds:[4,1,1,""],list_datasets:[4,1,1,""],list_user_data:[4,1,1,""],update_dataset:[4,1,1,""]},developer:{api_hello:[5,1,1,""],csrf_test:[5,1,1,""],get_added_ds:[5,1,1,""],list_config:[5,1,1,""],list_current_user:[5,1,1,""],list_session:[5,1,1,""],login:[5,1,1,""],login_hello:[5,1,1,""],permission_hello:[5,1,1,""],stop_server:[5,1,1,""]},order:{add_dataset:[6,1,1,""],add_order:[6,1,1,""],delete_order:[6,1,1,""],get_empty_order:[6,1,1,""],get_order:[6,1,1,""],get_order_data_structure:[6,1,1,""],get_order_logs:[6,1,1,""],list_orders:[6,1,1,""],list_orders_user:[6,1,1,""],prepare:[6,1,1,""],prepare_order_response:[6,1,1,""],update_order:[6,1,1,""]},structure:{collection:[7,1,1,""],dataset:[7,1,1,""],log:[7,1,1,""],order:[7,1,1,""],user:[7,1,1,""]},user:{add_new_user:[8,1,1,""],add_user:[8,1,1,""],delete_user:[8,1,1,""],do_login:[8,1,1,""],gen_new_api_key:[8,1,1,""],get_current_user:[8,1,1,""],get_current_user_info:[8,1,1,""],get_permission_info:[8,1,1,""],get_user:[8,1,1,""],get_user_actions:[8,1,1,""],get_user_data:[8,1,1,""],get_user_log:[8,1,1,""],has_permission:[8,1,1,""],list_users:[8,1,1,""],login_required:[8,1,1,""],update_current_user_info:[8,1,1,""],update_user_info:[8,1,1,""]},utils:{ValidationResult:[9,2,1,""],basic_check_indata:[9,1,1,""],check_email_uuid:[9,1,1,""],convert_keys_to_camel:[9,1,1,""],gen_api_key:[9,1,1,""],gen_api_key_hash:[9,1,1,""],gen_csrf_token:[9,1,1,""],get_db:[9,1,1,""],get_dbclient:[9,1,1,""],incremental_logs:[9,1,1,""],is_email:[9,1,1,""],make_log:[9,1,1,""],make_timestamp:[9,1,1,""],new_uuid:[9,1,1,""],response_json:[9,1,1,""],str_to_uuid:[9,1,1,""],user_uuid_data:[9,1,1,""],verify_api_key:[9,1,1,""],verify_csrf_token:[9,1,1,""]},validate:{validate_datasets:[10,1,1,""],validate_email:[10,1,1,""],validate_field:[10,1,1,""],validate_list_of_strings:[10,1,1,""],validate_permissions:[10,1,1,""],validate_string:[10,1,1,""],validate_tags_std:[10,1,1,""],validate_tags_user:[10,1,1,""],validate_title:[10,1,1,""],validate_url:[10,1,1,""],validate_user:[10,1,1,""],validate_user_list:[10,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:attribute"},terms:{"200":4,"400":[1,4,9],"401":[1,8,9],"403":1,"404":1,"5000":13,"byte":14,"case":[0,11],"class":9,"default":[9,11],"final":1,"function":[5,6,8,9,10],"int":[2,4],"new":[0,4,8,9,11],"public":11,"return":[0,1,2,3,4,6,7,8,9,10],"short":11,"true":13,For:10,IDs:11,The:[0,2,3,4,5,6,7,8,9,10,11,13,14],Use:14,Will:[10,11],_csrf_token:14,_id:[2,6,9,11],aai:[0,1,11],abort:[8,9],about:[0,8,9],access:[2,4,6,8,11,13,14],accredit:9,action:[0,8,9,11],activ:12,actual:8,add:[0,2,6,8,9,11,12,14],add_collect:2,add_dataset:6,add_new_us:8,add_ord:6,add_us:8,adddataset:9,added:[0,5,6,9,11],address:[9,11],admin:[2,4,8,11],affili:11,after:11,against:9,aid:[5,11],alia:9,all:[0,2,4,5,6,8,9,10,11,14],allow:[9,14],also:11,amount:[2,4],ani:[6,9,10,11,14],api:[8,9,11,15],api_bas:1,api_hello:5,api_kei:[0,9,11],api_salt:11,apikei:[0,1,9],app:[12,16],assert:8,associ:11,assum:9,auth_id:[0,5,8,11],auth_nam:[0,1],authent:8,author:[0,1,11],autom:11,avail:[2,4,9,14],backend:[13,14],base:[0,9,10,11],basic:[6,7,8,9],basic_check_indata:9,befor:[0,11],begin:14,belong:[4,6],between:11,bin:13,binari:9,bool:[8,9,10],browser:[10,13],build:12,build_dataset_info:4,calcul:[11,14],came:11,camelback:9,camelcas:9,can:[2,4,8,10,11,13],cannot:11,capit:9,chang:[0,2,4,6,8,9,11],check:[8,9,10,14],check_email_uuid:9,chronolog:6,chunk:9,clean:1,cleartext:9,client:9,code:[2,6,8,9,12,15],coexist:14,collect:[7,9,12,15,16],comment:[9,11],compar:9,compat:10,complet:[9,11],compon:11,compos:13,conf:9,config:[12,16],confirm:8,connect:[0,1,9],consid:14,contain:[9,10,11,12],control:11,conveni:9,convert:[6,9],convert_keys_to_camel:9,cooki:[9,14],cope:11,copi:[9,11],correct:[9,10],correspond:[10,11],could:11,cover:14,creat:[11,14],creation:11,creator:[4,11],cross_refer:11,crud:6,csrf:[5,9,15],csrf_test:5,csrftoken:14,current:[1,3,4,6,8,9,11],data:[1,3,6,7,9,10,12],data_manag:[0,2,4,6,11,14],data_typ:[9,11],databas:[1,4,6,8,9,10,11],dataset:[5,6,7,9,10,12,14,15,16],datatyp:11,datetim:9,dbserver:9,dbsession:9,decor:[8,14],defin:[11,14],delet:[0,2,4,6,8,9,11,14],delete_collect:2,delete_dataset:4,delete_ord:6,delete_us:8,descript:[0,11],dev:13,develop:[15,16],dict:[3,4,6,7,8,9,10,14],differ:[7,11],directli:11,do_login:8,docker:13,document:[7,9,11],doi:11,done:[0,6,9,10],dure:5,each:[6,10,14],edit:[9,11,14],editor:[0,4,11,14],either:[9,10,11],elixir:[0,1,11],email:[8,9,10,11],email_publ:11,empti:[6,9,10,11],endpoint:[8,14],enough:13,entiti:[1,9,11,14],entri:[2,4,6,8,9,10,11,14],entry2:14,environ:13,equal:9,error_bad_request:1,error_forbidden:1,error_not_found:1,error_unauthor:1,etc:[6,11],everyon:11,exactli:9,except:11,exist:[6,8,10],expect:[3,10],extern:11,extra:[9,11],facil:11,fail:[9,10],failur:4,fals:9,few:11,field:[0,6,7,9,10],field_kei:10,field_valu:10,figshar:11,file:[3,12],filenotfounderror:3,find:6,first:[0,8,9,11,14],flask:[2,4,5,6,8,9],folder:3,form:11,format:[9,11],found:[3,9,10],from:[3,4,6,8,9,11],fulfil:8,full:11,func:8,futur:9,gen_api_kei:9,gen_api_key_hash:9,gen_csrf_token:9,gen_new_api_kei:8,gen_test_db:13,gener:[0,8,9,13,14,15],get:[0,1,2,4,5,6,8,9,14],get_added_d:5,get_collect:2,get_collection_log:2,get_current_us:8,get_current_user_info:8,get_dataset:4,get_dataset_log:4,get_db:9,get_dbclient:9,get_empty_ord:6,get_ord:6,get_order_data_structur:6,get_order_log:6,get_permission_info:8,get_random:2,get_random_d:4,get_us:8,get_user_act:8,get_user_data:8,get_user_log:8,given:[5,6,9],group:11,handler:6,has:[6,8,11,14],has_permiss:8,hash:[9,11,14],hashlib:14,have:[0,6,10,11,14],header:[9,14],help:8,helper:[8,9],hex:9,hexdigest:14,hidden:11,homepag:11,html:1,http:[9,10,11],identfi:11,identif:5,identifi:[2,4,5,6,8,9,11],implement:15,impli:0,in_uuid:9,includ:[8,9,11,14],incom:9,increment:9,incremental_log:9,indata:[9,10],index:15,info:14,inform:[0,7,8,9,14],init:3,input:[9,10],insert:9,instal:13,instead:1,intend:[5,10],is_email:9,its:11,json:[2,4,6,8,9],json_structur:9,jsonifi:9,just:13,keep:9,kei:[0,8,9,10,11,15],key_login:1,lead:11,letter:9,level:10,like:9,limit:[0,11],link:11,list:[0,1,2,4,5,6,8,9,10,11,14],list_collect:2,list_config:5,list_current_us:5,list_dataset:4,list_ord:6,list_orders_us:6,list_sess:5,list_us:8,list_user_collect:2,list_user_data:4,load:3,localhost:13,locat:11,log:[1,2,4,5,6,7,8,9,14,15],logged_in:14,login:[0,1,5,8,11],login_hello:5,login_requir:[8,14],login_typ:1,logout:[0,1,8,14],look:[3,9],made:[11,14],mai:[9,10,11,14],main:[1,11],make:[1,6,9],make_log:9,make_timestamp:9,manag:[3,14],manament:8,map:9,markdown:11,match:[9,10],mean:11,modifi:[8,9,11,14],modul:[13,15],mongo:[6,9],mongo_cli:9,mongocli:9,mongodb:[6,9],more:7,multipl:[10,11,14],must:[9,10,11,14],name:[6,9,11,14],need:[11,14],never:11,new_uuid:9,no_us:9,non:[10,11,14],none:[2,4,8,9],note:9,number:[2,4,9,11],object:9,oidc:[0,8,11],oidc_author:1,oidc_login:1,oidc_typ:1,old:9,onc:11,one:[9,11],onli:[0,2,4,6,9,10,11],open:1,openid:[0,1],openli:11,orcid:11,order:[4,7,12,14,15,16],order_data:6,order_us:11,orders_speci:11,organis:11,other:[11,14],otherwis:[8,9],out:1,own:[2,6,11],owner:[2,11,14],owners_read:14,ownership:14,page:[1,15],paramet:[2,3,4,5,6,8,9,10],parent:3,pars:[3,11],partial:11,pass:[9,10],password:5,patch:0,path:3,peopl:11,perform:[1,9,10,11],permiss:[2,4,5,6,8,10,11,15],permission_hello:5,permission_requir:14,permit:9,pip:13,place:[6,9],post:0,prepar:[1,4,6,9,12],prepare_order_respons:6,preserv:9,profil:8,prohibit:9,project:[7,11,14],provid:[2,3,4,6,7,8,9,10,11],publicli:11,pymongo:9,python3:13,python:13,pythonpath:13,queri:[4,9],rais:[3,10],random:[2,4,13,14],read:3,read_config:3,receiv:[4,9,11],refer:[9,11,12,15],reference_data:9,relat:[6,8,11],relev:[8,9],renam:13,request:[2,4,5,6,8,9,11,14],requir:[5,6,7,8,11,13,14],research:11,respons:[1,2,4,6,8,9,11],response_json:9,result:9,retriev:[2,4,6,9,11],rout:5,run:[9,13,14],salt:[9,11,14],same:[8,11,14],sampl:[11,13],save:[9,11],search:[14,15],secret:14,see:[7,11],seem:9,sent:10,separ:10,server:[5,9],servic:0,session:[5,9],set:[0,3,8,11,13,14],sha512:[9,14],should:[8,9,11,13],show:[6,11,14],shown:11,shutdown:5,simpl:1,simplifi:[2,4],singl:[10,11,14],snake_cas:9,some:[0,9,11],sometim:14,sort:6,sourc:[1,2,3,4,5,6,7,8,9,10,11],special:[6,11],specialis:11,specif:11,stai:11,standard:11,start:[9,10,11],statu:[2,6,8,9],stop_serv:5,store:[9,14],str:[2,3,4,5,6,8,9,10],str_to_uuid:9,string:[9,10,11],structur:[2,4,6,8,9,12,15,16],style:11,succeed:8,success:[4,9],suffix:11,suggest:9,sure:[1,6],system:[9,11,12,14,15],tag:11,tags_standard:[10,11],tags_us:[10,11],task:[8,14],technic:10,terminolog:15,test:[5,12,15],thei:9,through:11,time:[9,11,14],timestamp:[9,11],titl:[6,9,10,11],token:[5,9,14],token_hex:14,topic:14,tracker:[1,3,11],trim:6,tupl:9,two:13,txt:13,type:[1,2,3,4,6,7,8,9,10,11],unauthor:8,unchang:9,union:[9,10],univers:11,updat:[0,2,4,6,8,9,11],update_collect:2,update_current_user_info:8,update_dataset:4,update_ord:6,update_user_info:8,upon:[11,14],url:[0,10,11],use:[3,6,9,10,11,14],used:[8,9,11,14],user:[1,2,4,5,6,7,9,10,12,14,15,16],user_add:14,user_id:[6,9],user_identifi:9,user_info:8,user_manag:[0,8,11,14],user_search:[0,14],user_uuid:8,user_uuid_data:9,userid:6,usernam:9,using:[0,1,9,11,13,14],util:[12,16],uuid:[0,2,4,6,8,9,10,11],valid:[9,12,16],validate_dataset:10,validate_email:10,validate_field:10,validate_list_of_str:10,validate_permiss:10,validate_str:10,validate_tags_std:10,validate_tags_us:10,validate_titl:10,validate_url:10,validate_us:10,validate_user_list:10,validationresult:9,valu:[4,9,10,11,14],valueerror:10,vari:11,variabl:[5,8,13],venv:13,verif:9,verifi:9,verify_api_kei:9,verify_csrf_token:9,version:[11,15],via:[0,9,11],virtual:13,visibl:[6,11],wai:11,want:[2,4,6],web:13,when:[11,14],whenev:11,where:0,whether:[8,9,10,14],which:10,who:11,why:[9,11],without:5,written:11,yaml:[3,13],you:6,your:6},titles:["API","app.py","collection.py","config.py","dataset.py","developer.py","order.py","structure.py","user.py","utils.py","validate.py","Data Structure","Development","System for development","Implementation","Data Tracker","Code reference"],titleterms:{activ:13,add:13,api:[0,14],app:1,build:13,code:16,collect:[0,2,11],comput:11,config:[3,13],contain:13,csrf:14,current:[0,14],data:[11,13,15],dataset:[0,4,11],develop:[5,12,13],field:11,file:13,gener:11,implement:14,indic:15,kei:14,log:[0,11],look:0,order:[0,6,11],out:0,permiss:14,prepar:13,refer:16,structur:[7,11],summari:11,system:13,tabl:15,terminolog:11,test:[13,14],tracker:15,unit:14,user:[0,8,11],util:9,valid:10,version:0}}) \ No newline at end of file +Search.setIndex({docnames:["api","app","code.app","code.collection","code.config","code.dataset","code.developer","code.order","code.project","code.structure","code.user","code.utils","code.validate","config","data_structure","dataset","developer","development","development.quick_environment","implementation","index","modules","order","project","structure","test","user","user_guide","utils"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":3,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":2,"sphinx.domains.rst":2,"sphinx.domains.std":1,"sphinx.ext.viewcode":1,sphinx:56},filenames:["api.rst","app.rst","code.app.rst","code.collection.rst","code.config.rst","code.dataset.rst","code.developer.rst","code.order.rst","code.project.rst","code.structure.rst","code.user.rst","code.utils.rst","code.validate.rst","config.rst","data_structure.rst","dataset.rst","developer.rst","development.rst","development.quick_environment.md","implementation.rst","index.rst","modules.rst","order.rst","project.rst","structure.rst","test.md","user.rst","user_guide.md","utils.rst"],objects:{"":{app:[2,0,0,"-"],collection:[3,0,0,"-"],config:[13,0,0,"-"],dataset:[15,0,0,"-"],developer:[16,0,0,"-"],order:[22,0,0,"-"],project:[23,0,0,"-"],structure:[24,0,0,"-"],user:[26,0,0,"-"],utils:[28,0,0,"-"],validate:[12,0,0,"-"]},"utils.ValidationResult":{result:[28,3,1,""],status:[28,3,1,""]},app:{api_base:[2,1,1,""],error_bad_request:[2,1,1,""],error_forbidden:[2,1,1,""],error_not_found:[2,1,1,""],error_unauthorized:[2,1,1,""],finalize:[2,1,1,""],key_login:[2,1,1,""],login_types:[2,1,1,""],logout:[2,1,1,""],oidc_authorize:[2,1,1,""],oidc_login:[2,1,1,""],oidc_types:[2,1,1,""],prepare:[2,1,1,""]},collection:{add_collection:[3,1,1,""],delete_collection:[3,1,1,""],get_collection:[3,1,1,""],get_collection_data_structure:[3,1,1,""],get_collection_log:[3,1,1,""],get_random:[3,1,1,""],list_collection:[3,1,1,""],list_user_collections:[3,1,1,""],update_collection:[3,1,1,""]},config:{init:[13,1,1,""],read_config:[13,1,1,""]},dataset:{add_dataset:[15,1,1,""],build_dataset_info:[15,1,1,""],delete_dataset:[15,1,1,""],get_dataset:[15,1,1,""],get_dataset_data_structure:[15,1,1,""],get_dataset_log:[15,1,1,""],get_random_ds:[15,1,1,""],list_datasets:[15,1,1,""],list_user_data:[15,1,1,""],update_dataset:[15,1,1,""]},developer:{api_hello:[16,1,1,""],csrf_test:[16,1,1,""],get_added_ds:[16,1,1,""],list_config:[16,1,1,""],list_current_user:[16,1,1,""],list_session:[16,1,1,""],login:[16,1,1,""],login_hello:[16,1,1,""],permission_hello:[16,1,1,""],stop_server:[16,1,1,""]},order:{add_order:[22,1,1,""],delete_order:[22,1,1,""],get_empty_order:[22,1,1,""],get_order:[22,1,1,""],get_order_data_structure:[22,1,1,""],get_order_logs:[22,1,1,""],list_orders:[22,1,1,""],list_orders_user:[22,1,1,""],prepare:[22,1,1,""],prepare_order_response:[22,1,1,""],update_order:[22,1,1,""]},project:{add_project:[23,1,1,""],delete_project:[23,1,1,""],get_project:[23,1,1,""],get_project_log:[23,1,1,""],get_random:[23,1,1,""],list_project:[23,1,1,""],list_user_projects:[23,1,1,""],update_project:[23,1,1,""]},structure:{collection:[24,1,1,""],dataset:[24,1,1,""],log:[24,1,1,""],order:[24,1,1,""],user:[24,1,1,""]},user:{add_new_user:[26,1,1,""],add_user:[26,1,1,""],delete_user:[26,1,1,""],do_login:[26,1,1,""],gen_new_api_key:[26,1,1,""],get_current_user:[26,1,1,""],get_current_user_info:[26,1,1,""],get_permission_info:[26,1,1,""],get_user:[26,1,1,""],get_user_actions:[26,1,1,""],get_user_data:[26,1,1,""],get_user_data_structure:[26,1,1,""],get_user_log:[26,1,1,""],has_permission:[26,1,1,""],list_users:[26,1,1,""],login_required:[26,1,1,""],update_current_user_info:[26,1,1,""],update_user_info:[26,1,1,""]},utils:{ValidationResult:[28,2,1,""],basic_check_indata:[28,1,1,""],check_email_uuid:[28,1,1,""],convert_keys_to_camel:[28,1,1,""],escape_html:[28,1,1,""],gen_api_key:[28,1,1,""],gen_api_key_hash:[28,1,1,""],gen_csrf_token:[28,1,1,""],get_db:[28,1,1,""],get_dbclient:[28,1,1,""],incremental_logs:[28,1,1,""],is_email:[28,1,1,""],make_log:[28,1,1,""],make_timestamp:[28,1,1,""],new_uuid:[28,1,1,""],response_json:[28,1,1,""],str_to_uuid:[28,1,1,""],user_uuid_data:[28,1,1,""],verify_api_key:[28,1,1,""],verify_csrf_token:[28,1,1,""]},validate:{validate_cross_references:[12,1,1,""],validate_datasets:[12,1,1,""],validate_email:[12,1,1,""],validate_field:[12,1,1,""],validate_list_of_strings:[12,1,1,""],validate_permissions:[12,1,1,""],validate_properties:[12,1,1,""],validate_string:[12,1,1,""],validate_tags:[12,1,1,""],validate_title:[12,1,1,""],validate_url:[12,1,1,""],validate_user:[12,1,1,""],validate_user_list:[12,1,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","function","Python function"],"2":["py","class","Python class"],"3":["py","attribute","Python attribute"]},objtypes:{"0":"py:module","1":"py:function","2":"py:class","3":"py:attribute"},terms:{"200":[5,15],"400":[1,2,5,11,15,28],"401":[1,2,10,11,26,28],"403":[1,2],"404":[1,2],"5000":18,"byte":19,"case":[0,14],"class":[11,28],"default":[11,14,27,28],"final":[1,2],"function":[6,7,10,11,12,16,22,26,28],"int":[3,5,8,15,23],"new":[0,5,10,11,14,15,26,28],"public":14,"return":[0,1,2,3,4,5,7,8,9,10,11,12,13,15,22,23,24,26,28],"short":14,"true":18,For:12,IDs:14,One:27,The:[0,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,22,23,24,26,27,28],Use:19,Will:[12,14],_csrf_token:19,_id:[3,7,8,11,14,22,23,28],aai:[0,1,2,14],abort:[10,11,26,28],about:[0,10,11,26,27,28],access:[3,5,7,8,10,14,15,18,19,22,23,26,27],account:27,accredit:[11,28],action:[0,10,11,14,26,28],activ:17,actual:[10,26],add:[0,3,5,7,8,10,11,14,15,17,19,22,23,26,28],add_collect:3,add_dataset:[5,15],add_new_us:[10,26],add_ord:[7,22],add_project:[8,23],add_us:[10,26],adddataset:[11,28],added:[0,6,7,11,14,16,22,28],address:[11,14,28],admin:[3,5,8,10,14,15,23,26],affili:[14,27],after:14,against:[11,28],aid:[6,14,16],alia:[11,28],all:[0,3,5,6,7,8,10,11,12,14,15,16,19,22,23,26,27,28],allow:[11,19,28],alreadi:27,also:[14,27],amount:[3,5,8,15,23],ani:[7,11,12,14,19,22,27,28],api:[10,11,14,20,26,28],api_bas:[1,2],api_hello:[6,16],api_kei:[0,11,14,28],api_salt:14,apikei:[0,1,2,11,28],app:[17,21],appli:27,assert:[10,26],associ:[14,27],assum:[11,28],auth_id:[0,6,10,14,16,26],auth_nam:[0,1,2],authent:[10,26],authid:27,author:[0,1,2,14,27],autom:14,avail:[3,5,8,11,15,19,23,27,28],backend:[18,19],base:[0,11,12,14,28],basic:[7,9,10,11,22,24,26,28],basic_check_indata:[11,28],befor:[0,14],begin:19,belong:[5,7,15,22],between:14,bin:18,binari:[11,28],bool:[10,11,12,26,28],browser:[12,18,27],build:17,build_dataset_info:[5,15],calcul:[14,19],came:14,camelback:[11,28],camelcas:[11,28],can:[3,5,8,10,12,14,15,18,23,26,27],cannot:14,capit:[11,28],chang:[0,3,5,7,8,10,11,14,15,22,23,26,27,28],check:[10,11,12,19,26,28],check_email_uuid:[11,28],choos:27,chronolog:[7,22],chunk:[11,28],clean:[1,2],cleartext:[11,28],click:27,client:[11,28],code:[3,7,8,10,11,17,20,22,23,25,26,28],coexist:19,collect:[9,11,17,20,21,24,28],comment:[11,14,28],compar:[11,28],compat:12,complet:[11,14,28],compon:14,compos:18,conf:[11,28],config:[17,21],confirm:[10,26],connect:[0,1,2,11,27,28],consid:[19,27],contact:27,contain:[11,12,14,17,28],control:14,conveni:[11,28],convert:[7,11,22,28],convert_keys_to_camel:[11,28],cooki:[11,19,28],cope:14,copi:[11,14,28],correct:[11,12,28],correspond:[12,14,27],could:14,cover:19,creat:[14,19],creation:14,creator:[5,14,15],cross_refer:[12,14],crud:[7,22],csrf:[6,11,16,20,28],csrf_test:[6,16],csrftoken:19,current:[1,2,4,5,7,10,11,13,14,15,22,26,27,28],data:[1,2,4,7,9,11,12,13,17,22,24,28],data_manag:[0,3,5,7,8,12,14,15,19,22,23],data_typ:[11,14,28],databas:[1,2,5,7,10,11,12,14,15,22,26,28],dataset:[6,7,9,11,12,16,17,19,20,21,22,24,28],datatyp:14,datetim:[11,28],dbserver:[11,28],dbsession:[11,28],decor:[10,19,26],defin:[14,19],delet:[0,3,5,7,8,10,11,14,15,19,22,23,26,28],delete_collect:3,delete_dataset:[5,15],delete_ord:[7,22],delete_project:[8,23],delete_us:[10,26],descript:[0,14],dev:18,develop:[20,21],dict:[4,5,7,9,10,11,12,13,15,19,22,24,26,28],differ:[9,14,24,27],directli:14,do_login:[10,26],docker:18,document:[9,11,14,24,27,28],doi:14,done:[0,7,11,12,22,28],dure:[6,16],each:[7,12,19,22],edit:[11,14,19,28],editor:[0,5,14,15,19,27],either:[11,12,14,28],elixir:[0,1,2,14],email:[10,11,12,14,26,28],email_publ:14,empti:[3,5,7,10,11,12,14,15,22,26,28],endpoint:[10,19,26],enough:18,entiti:[1,2,11,14,19,28],entri:[3,5,7,8,10,11,12,14,15,19,22,23,26,28],entry2:19,environ:18,equal:[11,28],error_bad_request:[1,2],error_forbidden:[1,2],error_not_found:[1,2],error_unauthor:[1,2],escap:[11,28],escape_html:[11,28],etc:[7,14,22],everyon:14,exactli:[11,28],exampl:27,except:14,exist:[7,10,12,22,26],expect:[4,12,13],extern:14,extra:[11,14,28],facil:[14,27],fail:[11,12,28],failur:[5,15],fals:[11,28],few:14,field:[0,7,9,11,12,22,24,28],field_kei:12,field_valu:12,figshar:14,file:[4,13,17],filenotfounderror:[4,13],find:[7,22],first:[0,10,11,14,19,26,28],flask:[3,5,6,7,8,10,11,15,16,22,23,26,28],folder:[4,13],form:14,format:[11,14,28],found:[4,11,12,13,28],from:[4,5,7,10,11,13,14,15,22,26,27,28],fulfil:[10,26],full:14,func:[10,26],futur:[11,28],gen_api_kei:[11,28],gen_api_key_hash:[11,28],gen_csrf_token:[11,28],gen_new_api_kei:[10,26],gen_test_db:18,gener:[0,10,11,18,19,20,26,28],get:[0,1,2,3,5,6,7,8,10,11,15,16,19,22,23,26,27,28],get_added_d:[6,16],get_collect:3,get_collection_data_structur:3,get_collection_log:3,get_current_us:[10,26],get_current_user_info:[10,26],get_dataset:[5,15],get_dataset_data_structur:[5,15],get_dataset_log:[5,15],get_db:[11,28],get_dbclient:[11,28],get_empty_ord:[7,22],get_ord:[7,22],get_order_data_structur:[7,22],get_order_log:[7,22],get_permission_info:[10,26],get_project:[8,23],get_project_log:[8,23],get_random:[3,8,23],get_random_d:[5,15],get_us:[10,26],get_user_act:[10,26],get_user_data:[10,26],get_user_data_structur:[10,26],get_user_log:[10,26],github:27,given:[5,6,7,11,15,16,22,28],going:27,group:14,handler:[7,22],has:[7,10,14,19,22,26,27],has_permiss:[10,26],hash:[11,14,19,28],hashlib:19,have:[0,7,12,14,19,22,27],header:[11,19,28],help:[10,26],helper:[10,11,26,28],hex:[11,28],hexdigest:19,hidden:14,homepag:14,html:[1,2,11,28],http:[11,12,14,28],identfi:14,identif:[6,16],identifi:[3,5,6,7,8,10,11,14,15,16,22,23,26,28],implement:20,impli:0,in_uuid:[11,28],includ:[10,11,14,19,26,28],incom:[11,28],increment:[11,28],incremental_log:[11,28],indata:[11,12,28],index:20,info:19,inform:[0,9,10,11,19,24,26,27,28],init:[4,13],input:[11,12,28],insert:[11,28],instal:18,instead:[1,2],intend:[6,12,16,27],intern:27,is_email:[11,28],ital:25,its:14,json:[3,5,7,8,10,11,15,22,23,26,28],json_structur:[11,28],jsonifi:[11,28],just:18,keep:[11,28],kei:[0,10,11,12,14,20,26,28],key_login:[1,2],kinda:25,lead:14,letter:[11,28],level:12,like:[11,28],limit:[0,14],link:[14,27],list:[0,1,2,3,5,6,7,8,10,11,12,14,15,16,19,22,23,26,27,28],list_collect:3,list_config:[6,16],list_current_us:[6,16],list_dataset:[5,15],list_ord:[7,22],list_orders_us:[7,22],list_project:[8,23],list_sess:[6,16],list_us:[10,26],list_user_collect:3,list_user_data:[5,15],list_user_project:[8,23],load:[4,13],localhost:18,locat:14,log:[1,2,3,5,6,7,8,9,10,11,15,16,19,20,22,23,24,26,27,28],logged_in:19,login:[0,1,2,6,10,14,16,26],login_hello:[6,16],login_requir:[10,19,26],login_typ:[1,2],logout:[0,1,2,10,19,26],look:[4,11,13,28],made:[14,19],mai:[11,12,14,19,27,28],main:[1,2,14],make:[1,2,7,11,22,28],make_log:[11,28],make_timestamp:[11,28],manag:[4,13,19,27],manament:[10,26],map:[11,28],markdown:14,match:[11,12,28],mean:14,menu:27,modifi:[10,11,14,19,26,28],modul:[18,20],mongo:[7,11,22,28],mongo_cli:[11,28],mongocli:[11,28],mongodb:[7,11,22,28],more:[9,24,27],multipl:[12,14,19,27],must:[11,12,14,19,28],name:[7,11,14,19,22,28],namedtupl:[11,28],need:[14,19],never:14,new_uuid:[11,28],newli:27,no_us:[11,28],non:[12,14,19],none:[3,5,8,10,11,15,23,26,28],note:[11,28],number:[3,5,8,11,14,15,23,28],object:[11,28],oidc:[0,10,14,26],oidc_author:[1,2],oidc_login:[1,2],oidc_typ:[1,2],old:[11,28],onc:14,one:[11,14,27,28],onli:[0,3,5,7,8,11,12,14,15,22,23,27,28],open:[1,2],openid:[0,1,2,27],openli:14,orcid:14,order:[5,9,15,17,19,20,21,24],order_data:[7,22],order_us:14,orders_speci:14,organis:14,other:[14,19],otherwis:[10,11,26,28],out:[1,2],own:[3,7,8,14,22,23],owner:[3,8,14,19,23],owners_read:19,ownership:19,page:[1,2,20,27],paramet:[3,4,5,6,7,8,10,11,12,13,15,16,22,23,26,28],parent:[4,13],pars:[4,13,14],partial:14,pass:[11,12,28],password:[6,16],patch:0,path:[4,13],peopl:14,perform:[1,2,11,12,14,28],permiss:[3,5,6,7,8,10,12,14,15,16,20,22,23,26,27],permission_hello:[6,16],permission_requir:19,permit:[11,28],pip:18,place:[7,11,22,28],post:0,prepar:[1,2,5,7,11,15,17,22,28],prepare_order_respons:[7,22],preserv:[11,28],profil:[10,26],prohibit:[11,28],project:[9,14,19,24,27],properti:12,provid:[3,4,5,7,8,9,10,11,12,13,14,15,22,23,24,26,28],publicli:14,pymongo:[11,28],python3:18,python:18,pythonpath:18,queri:[5,11,15,28],rais:[4,12,13],random:[3,5,8,15,18,19,23],read:[4,13],read_config:[4,13],receiv:[5,11,15,27,28],refer:[11,14,17,20,28],reference_data:[11,28],relat:[7,10,14,22,26],releas:27,relev:[10,11,26,28],renam:18,request:[3,5,6,7,8,10,11,14,15,16,19,22,23,26,28],requir:[6,7,9,10,14,16,18,19,22,24,26,27],research:14,respons:[1,2,3,5,7,8,10,11,14,15,22,23,26,27,28],response_json:[11,28],result:[11,28],retriev:[3,5,7,8,11,14,15,22,23,28],rout:[6,16],run:[11,18,19,28],salt:[11,14,19,28],same:[10,14,19,26,27],sampl:[14,18],save:[11,14,28],search:[19,20],secret:19,see:[9,14,24],seem:[11,28],sent:12,separ:12,server:[6,11,16,28],servic:0,session:[6,11,16,28],set:[0,4,10,13,14,18,19,26,27],sha512:[11,19,28],should:[10,11,14,18,26,27,28],show:[7,14,19,22],shown:14,shutdown:[6,16],sign:27,simpl:[1,2],simplifi:[3,5,8,15,23],singl:[12,14,19],snake_cas:[11,28],some:[0,11,14,27,28],sometim:19,sort:[7,22],sourc:[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,22,23,24,26,28],special:[7,14,22],specialis:14,specif:[14,27],stai:14,standard:14,start:[11,12,14,28],statu:[3,7,8,10,11,22,23,26,28],stop_serv:[6,16],store:[11,19,28],str:[3,4,5,6,7,8,10,11,12,13,15,16,22,23,26,28],str_to_uuid:[11,28],string:[11,12,14,28],structur:[3,5,7,8,10,11,15,17,20,21,22,23,26,28],style:14,succeed:[10,26],success:[5,11,15,28],suffix:14,suggest:[11,28],sure:[1,2,7,22],system:[11,14,17,19,20,28],tag:[11,12,14,28],tags_standard:14,tags_us:14,task:[10,19,26],technic:12,terminolog:20,test:[6,16,17,20],text:[11,28],thei:[11,27,28],thi:27,through:14,thu:27,time:[11,14,19,28],timestamp:[11,14,28],titl:[7,11,12,14,22,28],togeth:27,token:[6,11,16,19,28],token_hex:19,topic:19,tracker:[1,2,4,13,14],trim:[7,22],tupl:[11,12,28],two:18,txt:18,type:[1,2,3,4,5,7,8,9,10,11,12,13,14,15,22,23,24,26,28],unauthor:[10,26],unchang:[11,28],union:[11,12,28],univers:14,updat:[0,3,5,7,8,10,11,14,15,22,23,26,28],update_collect:3,update_current_user_info:[10,26],update_dataset:[5,15],update_ord:[7,22],update_project:[8,23],update_user_info:[10,26],upon:[14,19],url:[0,12,14],use:[4,7,11,12,13,14,19,22,28],used:[10,11,14,19,26,27,28],user:[1,2,3,5,6,7,8,9,11,12,15,16,17,19,20,21,22,23,24,28],user_add:19,user_id:[7,11,22,28],user_identifi:[11,28],user_info:[10,26],user_manag:[0,10,14,19,26],user_search:[0,19],user_uuid:[10,26],user_uuid_data:[11,28],userid:[7,22],usernam:[11,28],using:[0,1,2,11,14,18,19,27,28],util:[17,21],uuid:[0,3,5,7,8,10,11,12,14,15,22,23,26,28],valid:[11,17,21,28],validate_cross_refer:12,validate_dataset:12,validate_email:12,validate_field:12,validate_list_of_str:12,validate_permiss:12,validate_properti:12,validate_str:12,validate_tag:12,validate_titl:12,validate_url:12,validate_us:12,validate_user_list:12,validationresult:[11,28],valu:[5,11,12,14,15,19,28],valueerror:12,vari:14,variabl:[6,10,16,18,26],venv:18,verif:[11,28],verifi:[11,28],verify_api_kei:[11,28],verify_csrf_token:[11,28],version:[14,20],via:[0,11,14,28],view:27,virtual:18,visibl:[7,14,22,27],wai:14,want:[3,5,7,8,15,22,23],web:18,when:[14,19],whenev:14,where:0,whether:[10,11,12,19,26,28],which:12,who:14,why:[11,14,28],without:[6,16],written:14,yaml:[4,13,18],you:[7,22,27],your:[7,22]},titles:["API","app module","app.py","collection.py","config.py","dataset.py","developer.py","order.py","project.py","structure.py","user.py","utils.py","validate.py","config module","Data Structure","dataset module","developer module","Development","System for development","Implementation","Data Tracker","Code reference","order module","project module","structure module","Headline","user module","User Guide","utils module"],titleterms:{"new":27,Using:27,activ:18,add:18,api:[0,19,27],app:[1,2],build:18,code:21,collect:[0,3,14,27],comput:14,config:[4,13,18],contain:18,creat:27,csrf:19,current:[0,19],data:[14,18,20,27],dataset:[0,5,14,15,27],develop:[6,16,17,18],entri:27,field:14,file:18,gener:[14,27],guid:27,headlin:25,implement:19,indic:20,kei:[19,27],log:[0,14],look:0,modul:[1,13,15,16,22,23,24,26,28],order:[0,7,14,22,27],out:0,permiss:19,prepar:18,project:[8,23],refer:21,second:25,structur:[9,14,24],summari:14,system:18,tabl:20,terminolog:14,test:[18,19],tracker:20,type:27,unit:19,user:[0,10,14,26,27],util:[11,28],valid:12,version:0,websit:27}}) \ No newline at end of file diff --git a/docs/build/html/sources/app.rst.txt b/docs/build/html/sources/app.rst.txt new file mode 100644 index 00000000..ceb7f40d --- /dev/null +++ b/docs/build/html/sources/app.rst.txt @@ -0,0 +1,7 @@ +app module +========== + +.. automodule:: app + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/code.project.rst.txt b/docs/build/html/sources/code.project.rst.txt new file mode 100644 index 00000000..2a62babe --- /dev/null +++ b/docs/build/html/sources/code.project.rst.txt @@ -0,0 +1,7 @@ +project.py +========== + +.. automodule:: project + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/config.rst.txt b/docs/build/html/sources/config.rst.txt new file mode 100644 index 00000000..b559b61b --- /dev/null +++ b/docs/build/html/sources/config.rst.txt @@ -0,0 +1,7 @@ +config module +============= + +.. automodule:: config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/data_structure.rst.txt b/docs/build/html/sources/data_structure.rst.txt index 5be4525e..1537faed 100644 --- a/docs/build/html/sources/data_structure.rst.txt +++ b/docs/build/html/sources/data_structure.rst.txt @@ -59,8 +59,6 @@ Summary +---------------+-----------------------------------------------------+-------------------+-----------------------+ | editors | List of users who can edit the order and datasets | Entry creator | Hidden | +---------------+-----------------------------------------------------+-------------------+-----------------------+ -| receivers | List of users who received data from facility | Empty | Hidden | -+---------------+-----------------------------------------------------+-------------------+-----------------------+ | datasets | List of associated datasets | Empty | Visible (via dataset) | +---------------+-----------------------------------------------------+-------------------+-----------------------+ | tags_standard | Tags defined in the system | Empty | Hidden | @@ -109,10 +107,6 @@ Fields * List of ``users``. * Users that may edit the order and dataset entries. May add datasets to an order. * **Default:** The user that created the entry. -:receivers: - * List of ``users``. - * Corresponds to the users who received the data from the facility - * **Default:** Empty :datasets: * List of datasets associated to the order. * Cannot be modified directly but must be modified through specialised means. @@ -143,7 +137,6 @@ Dataset - ``authors`` - ``organisation`` - ``editors`` - - ``receivers`` Summary ------- diff --git a/docs/build/html/sources/dataset.rst.txt b/docs/build/html/sources/dataset.rst.txt new file mode 100644 index 00000000..3046110e --- /dev/null +++ b/docs/build/html/sources/dataset.rst.txt @@ -0,0 +1,7 @@ +dataset module +============== + +.. automodule:: dataset + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/developer.rst.txt b/docs/build/html/sources/developer.rst.txt new file mode 100644 index 00000000..7d2bc99a --- /dev/null +++ b/docs/build/html/sources/developer.rst.txt @@ -0,0 +1,7 @@ +developer module +================ + +.. automodule:: developer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/order.rst.txt b/docs/build/html/sources/order.rst.txt new file mode 100644 index 00000000..db9f0fec --- /dev/null +++ b/docs/build/html/sources/order.rst.txt @@ -0,0 +1,7 @@ +order module +============ + +.. automodule:: order + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/project.rst.txt b/docs/build/html/sources/project.rst.txt new file mode 100644 index 00000000..66503693 --- /dev/null +++ b/docs/build/html/sources/project.rst.txt @@ -0,0 +1,7 @@ +project module +============== + +.. automodule:: project + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/structure.rst.txt b/docs/build/html/sources/structure.rst.txt new file mode 100644 index 00000000..62b59632 --- /dev/null +++ b/docs/build/html/sources/structure.rst.txt @@ -0,0 +1,7 @@ +structure module +================ + +.. automodule:: structure + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/test.md.txt b/docs/build/html/sources/test.md.txt new file mode 100644 index 00000000..76603a2c --- /dev/null +++ b/docs/build/html/sources/test.md.txt @@ -0,0 +1,7 @@ +# Headline + +## Second headline + +`code, kinda` + +*italic* \ No newline at end of file diff --git a/docs/build/html/sources/user.rst.txt b/docs/build/html/sources/user.rst.txt new file mode 100644 index 00000000..3fcd9872 --- /dev/null +++ b/docs/build/html/sources/user.rst.txt @@ -0,0 +1,7 @@ +user module +=========== + +.. automodule:: user + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/sources/user_guide.md.txt b/docs/build/html/sources/user_guide.md.txt new file mode 100644 index 00000000..9987f937 --- /dev/null +++ b/docs/build/html/sources/user_guide.md.txt @@ -0,0 +1,58 @@ +User Guide +========== + +## Data types + +### Orders + +An order is intended to correspond to one order to a facility with a responsible PI. If the PI changes, it should be considered as a new order. + +Orders are considered to be "internal" data, and will thus only be visible to the users listed as `editors`. + +One order can generate one or more datasets. + + +### Datasets + +A dataset is a data release from an order. Thus, all datasets are associated with *one* specific order. + +The data author, generator, and data manager is considered to be the same as for the associated order. + + +### Collections + +Collections are intended to link together data from different orders, for example if the same project has received data from multiple projects. + +Any user can associate any dataset with any collection. + + +## Using the Website + +The website by default lists all available datasets and collections. + +### User + +Users can log in using OpenID Connect or, if they already have an account, AuthID and API key. + +Some information about the current user, e.g. affiliation and contact information, can be set by choosing "Current User" in the menu. + +By default, newly logged in users can create collections, but they may also apply for permission to e.g. create orders. + +### Creating a new entry + +A new entry can be created by going to the browser page of the entry type (e.g. [collections](/collections/) or [datasets](/datasets/)) and clicking the "+" sign. + +#### + +Creating an order requires that you have the `ORDERS` permission. If you can view the order browser, you should have this permission. + +### Generating an API key + + + + +## Using the API + +The documentation is available on [Github](https://scilifelabdatacentre.github.io/Data-Tracker/). + +You can get an API key that can be used for accessing the API. \ No newline at end of file diff --git a/docs/build/html/sources/utils.rst.txt b/docs/build/html/sources/utils.rst.txt new file mode 100644 index 00000000..44cef9ed --- /dev/null +++ b/docs/build/html/sources/utils.rst.txt @@ -0,0 +1,7 @@ +utils module +============ + +.. automodule:: utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/build/html/static/data-centre-logo.png b/docs/build/html/static/data-centre-logo.png new file mode 100644 index 00000000..790c01b4 Binary files /dev/null and b/docs/build/html/static/data-centre-logo.png differ diff --git a/docs/build/html/static/pygments.css b/docs/build/html/static/pygments.css index b0ec841f..e76de2a6 100644 --- a/docs/build/html/static/pygments.css +++ b/docs/build/html/static/pygments.css @@ -1,5 +1,10 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } .highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } +.highlight { background: #f8f8f8; } .highlight .c { color: #8f5902; font-style: italic } /* Comment */ .highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ .highlight .g { color: #000000 } /* Generic */ diff --git a/docs/build/html/structure.html b/docs/build/html/structure.html new file mode 100644 index 00000000..6a3544c8 --- /dev/null +++ b/docs/build/html/structure.html @@ -0,0 +1,163 @@ + + + + + + + + structure module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    structure module

    +

    Required fields for the different data types.

    +

    See documentation (Data Structure) for more information.

    +
    +
    +structure.collection()[source]
    +

    Provide a basic data structure for a project document.

    +
    +
    Returns
    +

    The data structure for a project.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +structure.dataset()[source]
    +

    Provide a basic data structure for a dataset document.

    +
    +
    Returns
    +

    The data structure for a dataset.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +structure.log()[source]
    +

    Provide a basic data structure for a log document.

    +
    +
    Returns
    +

    The data structure for a log.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +structure.order()[source]
    +

    Provide a basic data structure for an order document.

    +
    +
    Returns
    +

    The data structure for an order.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +structure.user()[source]
    +

    Provide a basic data structure for a user document.

    +
    +
    Returns
    +

    The data structure for a user.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/test.html b/docs/build/html/test.html new file mode 100644 index 00000000..b70980c2 --- /dev/null +++ b/docs/build/html/test.html @@ -0,0 +1,104 @@ + + + + + + + + Headline — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    Headline

    +
    +

    Second headline

    +

    code, kinda

    +

    italic

    +
    +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/user.html b/docs/build/html/user.html new file mode 100644 index 00000000..8502983b --- /dev/null +++ b/docs/build/html/user.html @@ -0,0 +1,353 @@ + + + + + + + + user module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    user module

    +

    User profile, permissions, and login/logout functions and endpoints.

    +
    +
    Decorators

    Decorators used to e.g. assert that a user is logged in.

    +
    +
    Helper functions

    Functions to help with user-related tasks, e.g. setting all variables at login.

    +
    +
    Requests

    User-related API endpoints, including login/logout and user manament.

    +
    +
    +
    +
    +user.add_new_user(user_info: dict)[source]
    +

    Add a new user to the database from first oidc login.

    +

    First check if user with the same email exists. +If so, add the auth_id to the user.

    +
    +
    Parameters
    +

    user_info (dict) – Information about the user

    +
    +
    +
    + +
    +
    +user.add_user()[source]
    +

    Add a user.

    +
    +
    Returns
    +

    Information about the user as json.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.delete_user(identifier: str)[source]
    +

    Delete a user.

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the user to modify.

    +
    +
    Returns
    +

    Response code.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.do_login(auth_id: str)[source]
    +

    Set all relevant variables for a logged in user.

    +
    +
    Parameters
    +

    auth_id (str) – Authentication id for the user.

    +
    +
    +

    Returns bool: Whether the login succeeded.

    +
    + +
    +
    +user.gen_new_api_key(identifier: str = None)[source]
    +

    Generate a new API key for the provided or current user.

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the user.

    +
    +
    Returns
    +

    The new API key

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.get_current_user()[source]
    +

    Get the current user.

    +
    +
    Returns
    +

    The current user.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +user.get_current_user_info()[source]
    +

    List basic information about the current user.

    +
    +
    Returns
    +

    json structure for the user

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.get_permission_info()[source]
    +

    Get a list of all permission types.

    +
    + +
    +
    +user.get_user(user_uuid=None)[source]
    +

    Get information about the user.

    +
    +
    Parameters
    +

    user_uuid (str) – The identifier (uuid) of the user.

    +
    +
    Returns
    +

    The current user.

    +
    +
    Return type
    +

    dict

    +
    +
    +
    + +
    +
    +user.get_user_actions(identifier: str = None)[source]
    +

    Get a list of actions (changes) by the user entry with uuid identifier.

    +

    Can be accessed by actual user and admin (USER_MANAGEMENT).

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the user.

    +
    +
    Returns
    +

    Information about the user as json.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.get_user_data(identifier: str)[source]
    +

    Get information about a user.

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the user.

    +
    +
    Returns
    +

    Information about the user as json.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.get_user_data_structure()[source]
    +

    Get an empty user entry.

    +
    +
    Returns
    +

    JSON structure with a list of users.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.get_user_log(identifier: str = None)[source]
    +

    Get change logs for the user entry with uuid identifier.

    +

    Can be accessed by actual user and admin (USER_MANAGEMENT).

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the user.

    +
    +
    Returns
    +

    Information about the user as json.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.has_permission(permission: str)[source]
    +

    Check if the current user permissions fulfills the requirement.

    +
    +
    Parameters
    +

    permission (str) – The required permission

    +
    +
    Returns
    +

    whether the user has the required permissions or not

    +
    +
    Return type
    +

    bool

    +
    +
    +
    + +
    +
    +user.list_users()[source]
    +

    List all users.

    +

    Admin access should be required.

    +
    + +
    +
    +user.login_required(func)[source]
    +

    Confirm that the user is logged in.

    +

    Otherwise abort with status 401 Unauthorized.

    +
    + +
    +
    +user.update_current_user_info()[source]
    +

    Update the information about the current user.

    +
    +
    Returns
    +

    Response code.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +user.update_user_info(identifier: str)[source]
    +

    Update the information about a user.

    +
    +
    Parameters
    +

    identifier (str) – The uuid of the user to modify.

    +
    +
    Returns
    +

    Response code.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/user_guide.html b/docs/build/html/user_guide.html new file mode 100644 index 00000000..96fdbae6 --- /dev/null +++ b/docs/build/html/user_guide.html @@ -0,0 +1,159 @@ + + + + + + + + User Guide — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    User Guide

    +
    +

    Data types

    +
    +

    Orders

    +

    An order is intended to correspond to one order to a facility with a responsible PI. If the PI changes, it should be considered as a new order.

    +

    Orders are considered to be “internal” data, and will thus only be visible to the users listed as editors.

    +

    One order can generate one or more datasets.

    +
    +
    +

    Datasets

    +

    A dataset is a data release from an order. Thus, all datasets are associated with one specific order.

    +

    The data author, generator, and data manager is considered to be the same as for the associated order.

    +
    +
    +

    Collections

    +

    Collections are intended to link together data from different orders, for example if the same project has received data from multiple projects.

    +

    Any user can associate any dataset with any collection.

    +
    +
    +
    +

    Using the Website

    +

    The website by default lists all available datasets and collections.

    +
    +

    User

    +

    Users can log in using OpenID Connect or, if they already have an account, AuthID and API key.

    +

    Some information about the current user, e.g. affiliation and contact information, can be set by choosing “Current User” in the menu.

    +

    By default, newly logged in users can create collections, but they may also apply for permission to e.g. create orders.

    +
    +
    +

    Creating a new entry

    +

    A new entry can be created by going to the browser page of the entry type (e.g. collections or datasets) and clicking the “+” sign.

    +
    +

    +

    Creating an order requires that you have the ORDERS permission. If you can view the order browser, you should have this permission.

    +
    +
    +
    +

    Generating an API key

    +
    +
    +
    +

    Using the API

    +

    The documentation is available on Github.

    +

    You can get an API key that can be used for accessing the API.

    +
    +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/build/html/utils.html b/docs/build/html/utils.html new file mode 100644 index 00000000..925929fe --- /dev/null +++ b/docs/build/html/utils.html @@ -0,0 +1,444 @@ + + + + + + + + utils module — Data Tracker documentation + + + + + + + + + + + + +
    +
    +
    +
    + +
    +

    utils module

    +

    General helper functions.

    +
    +
    +class utils.ValidationResult(result, status)
    +

    Bases: tuple

    +
    +
    +result
    +

    Alias for field number 0

    +
    + +
    +
    +status
    +

    Alias for field number 1

    +
    + +
    + +
    +
    +utils.basic_check_indata(indata: dict, reference_data: dict, prohibited: Union[tuple, list]) → tuple[source]
    +

    Perform basic checks of indata.

    +
      +
    • All fields are allowed in the entity type

    • +
    • If title is a field for the entity, it may not be empty

    • +
    • All fields are of the correct type

    • +
    • All prohibited fields are unchanged (if update)

    • +
    +
    +
    Parameters
    +
      +
    • indata (dict) – The incoming data.

    • +
    • reference_data (dict) – Either the old data or a reference dict.

    • +
    • prohibited (Union[tuple, list]) – Fields that may not be modified. +If they are included in indata, their values must be equal to the +values in reference_data. Defaults to None.

    • +
    +
    +
    Returns
    +

    (bool: whether the check passed, code: Suggested http code)

    +
    +
    Return type
    +

    namedtuple

    +
    +
    +
    + +
    +
    +utils.check_email_uuid(user_identifier: str) → Union[str, uuid.UUID][source]
    +

    Check if the provided user is found in the db as email or _id.

    +

    If the user is found, return the user.UUID of the user. +If the identifier is a uuid, return a user.UUID. +If the identifier is an email, return the email.

    +

    Notes

    +

    user_identifier is assumed to be either a valid email or a valid uuid.

    +
    +
    Parameters
    +

    user_identifier (str) – The identifier to look up.

    +
    +
    Returns
    +

    The new value for the field.

    +
    +
    Return type
    +

    Union[str, uuid.UUID]

    +
    +
    +
    + +
    +
    +utils.convert_keys_to_camel(chunk: Any) → Any[source]
    +

    Convert keys given in snake_case to camelCase.

    +

    The capitalization of the first letter is preserved.

    +
    +
    Parameters
    +

    chunk (Any) – Object to convert.

    +
    +
    Returns
    +

    Chunk converted to camelCase dict, otherwise chunk.

    +
    +
    Return type
    +

    Any

    +
    +
    +
    + +
    +
    +utils.escape_html(data: str) → str[source]
    +

    Escape e.g. html tags for the provided text.

    +
    +
    Parameters
    +

    data (str) – The text to escape.

    +
    +
    Returns
    +

    The escaped text.

    +
    +
    Return type
    +

    str

    +
    +
    +
    + +
    +
    +utils.gen_api_key()[source]
    +

    Generate an API key with salt.

    +
    +
    Returns
    +

    The API key with salt.

    +
    +
    Return type
    +

    APIkey

    +
    +
    +
    + +
    +
    +utils.gen_api_key_hash(api_key: str, salt: str)[source]
    +

    Generate a hash of the api_key for storing/comparing to db.

    +
    +
    Parameters
    +
      +
    • api_key (str) – The cleartext API key (hex).

    • +
    • salt (str) – The salt to use (hex).

    • +
    +
    +
    Returns
    +

    SHA512 hash as hex.

    +
    +
    Return type
    +

    str

    +
    +
    +
    + +
    +
    +utils.gen_csrf_token() → str[source]
    +

    Generate a csrf token.

    +
    +
    Returns
    +

    The csrf token.

    +
    +
    Return type
    +

    str

    +
    +
    +
    + +
    +
    +utils.get_db(dbserver: pymongo.mongo_client.MongoClient, conf) → pymongo.database.Database[source]
    +

    Get the connection to the MongoDB database.

    +
    +
    Parameters
    +
      +
    • dbserver (pymongo.mongo_client.MongoClient) – Connection to the database.

    • +
    • conf – A mapping with the relevant mongo keys available.

    • +
    +
    +
    Returns
    +

    The database connection.

    +
    +
    Return type
    +

    pymongo.database.Database

    +
    +
    +
    + +
    +
    +utils.get_dbclient(conf) → pymongo.mongo_client.MongoClient[source]
    +

    Get the connection to the MongoDB database server.

    +
    +
    Parameters
    +

    conf – A mapping with the relevant mongo keys available.

    +
    +
    Returns
    +

    The client connection.

    +
    +
    Return type
    +

    pymongo.mongo_client.MongoClient

    +
    +
    +
    + +
    +
    +utils.incremental_logs(logs: list)[source]
    +

    Make an incremental log.

    +

    The log starts from the first log and keeps only +the changed fields in data.

    +

    logs is changed in-place.

    +
    + +
    +
    +utils.is_email(indata: str)[source]
    +

    Check whether a string seems to be an email address or not.

    +
    +
    Parameters
    +

    indata (str) – Data to check.

    +
    +
    Returns
    +

    Is the indata an email address or not.

    +
    +
    Return type
    +

    bool

    +
    +
    +
    + +
    +
    +utils.make_log(data_type: str, action: str, comment: str, data: dict = None, no_user: bool = False, dbsession=None)[source]
    +

    Log a change in the system.

    +

    Saves a complete copy of the new object.

    +
    +

    Warning

    +

    It is assumed that all values are exactly like in the db, +e.g. data should only contain permitted fields.

    +
    +
    +
    Parameters
    +
      +
    • action (str) – Type of action (add, edit, delete).

    • +
    • comment (str) – Note about why the change was done +(e.g. “Dataset added via addDataset”).

    • +
    • data_type (str) – The collection name.

    • +
    • data (dict) – The new data for the entry.

    • +
    • no_user (bool) – Whether the entry should be accredited to “system”.

    • +
    • dbsession – The MongoDB session used.

    • +
    +
    +
    Returns
    +

    Whether the log insertion successed.

    +
    +
    Return type
    +

    bool

    +
    +
    +
    + +
    +
    +utils.make_timestamp()[source]
    +

    Generate a timestamp of the current time.

    +
    +
    Returns
    +

    The current time.

    +
    +
    Return type
    +

    datetime.datetime

    +
    +
    +
    + +
    +
    +utils.new_uuid() → uuid.UUID[source]
    +

    Generate a uuid for a field in a MongoDB document.

    +
    +
    Returns
    +

    The new uuid in binary format.

    +
    +
    Return type
    +

    uuid.UUID

    +
    +
    +
    + +
    +
    +utils.response_json(json_structure: dict)[source]
    +

    Convert keys to camelCase and run flask.jsonify().

    +
    +
    Parameters
    +

    json_structure (dict) – Structure to prepare.

    +
    +
    Returns
    +

    Prepared response containing json structure with camelBack keys.

    +
    +
    Return type
    +

    flask.Response

    +
    +
    +
    + +
    +
    +utils.str_to_uuid(in_uuid: Union[str, uuid.UUID]) → uuid.UUID[source]
    +

    Convert str uuid to uuid.UUID.

    +

    Provided as a convenience function if the identifier must be changed in the future.

    +
    +
    Parameters
    +

    in_uuid (str or uuid.UUID) – The uuid to be converted.

    +
    +
    Returns
    +

    The uuid as a UUID object.

    +
    +
    Return type
    +

    uuid.UUID

    +
    +
    +
    + +
    +
    +utils.user_uuid_data(user_ids: Union[str, list, uuid.UUID], mongodb: pymongo.database.Database) → list[source]
    +

    Retrieve some extra information about a user using a uuid as input.

    +

    Note that _id` will be returned as str, not uuid.UUID.

    +
    +
    Parameters
    +
      +
    • user_ids (str, list, or uuid.UUID) – UUID of the user(s).

    • +
    • mongodb (pymongo.database.Database) – The Mongo database to use for the query.

    • +
    +
    +
    Returns
    +

    The matching entries.

    +
    +
    Return type
    +

    list

    +
    +
    +
    + +
    +
    +utils.verify_api_key(username: str, api_key: str)[source]
    +

    Verify an API key against the value in the database.

    +

    Aborts with status 401 if the verification fails.

    +
    +
    Parameters
    +
      +
    • username (str) – The username to check.

    • +
    • api_key (str) – The received API key (hex).

    • +
    +
    +
    +
    + +
    +
    +utils.verify_csrf_token()[source]
    +

    Compare the csrf token from the request (header) with the one in the cookie.session.

    +

    Aborts with status 400 if the verification fails.

    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + + + \ No newline at end of file diff --git a/docs/source/data_structure.rst b/docs/source/data_structure.rst index 5be4525e..1537faed 100644 --- a/docs/source/data_structure.rst +++ b/docs/source/data_structure.rst @@ -59,8 +59,6 @@ Summary +---------------+-----------------------------------------------------+-------------------+-----------------------+ | editors | List of users who can edit the order and datasets | Entry creator | Hidden | +---------------+-----------------------------------------------------+-------------------+-----------------------+ -| receivers | List of users who received data from facility | Empty | Hidden | -+---------------+-----------------------------------------------------+-------------------+-----------------------+ | datasets | List of associated datasets | Empty | Visible (via dataset) | +---------------+-----------------------------------------------------+-------------------+-----------------------+ | tags_standard | Tags defined in the system | Empty | Hidden | @@ -109,10 +107,6 @@ Fields * List of ``users``. * Users that may edit the order and dataset entries. May add datasets to an order. * **Default:** The user that created the entry. -:receivers: - * List of ``users``. - * Corresponds to the users who received the data from the facility - * **Default:** Empty :datasets: * List of datasets associated to the order. * Cannot be modified directly but must be modified through specialised means. @@ -143,7 +137,6 @@ Dataset - ``authors`` - ``organisation`` - ``editors`` - - ``receivers`` Summary ------- diff --git a/frontend/package.json b/frontend/package.json index e43597d3..ddef640f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,14 +10,14 @@ "test": "echo \"No test specified\" && exit 0" }, "dependencies": { - "@quasar/extras": "^1.9.10", - "axios": "^0.21.0", - "core-js": "^3.8.0", - "quasar": "^1.14.5" + "@quasar/extras": "^1.9.17", + "axios": "^0.21.1", + "core-js": "^3.8.3", + "quasar": "^1.15.3" }, "devDependencies": { - "@quasar/app": "^2.1.8", - "@quasar/quasar-app-extension-qmarkdown": "^1.2.1", + "@quasar/app": "^2.1.13", + "@quasar/quasar-app-extension-qmarkdown": "^1.4.0", "babel-eslint": "^10.1.0", "eslint": "^7.14.0", "eslint-config-prettier": "^6.15.0", diff --git a/frontend/src/components/CollectionAbout.vue b/frontend/src/components/CollectionAbout.vue index 68bb5511..0462f696 100644 --- a/frontend/src/components/CollectionAbout.vue +++ b/frontend/src/components/CollectionAbout.vue @@ -7,73 +7,66 @@ - - - - - +
    + + {{ field }} {{ collection.properties[field] }} + +
    + +
    + + + {{ entry }} + +
    - - - - {{ field }} {{ collection.properties[field] }} - - - - {{ entry }} - - - +
    + +
    - - - - - - - - - - - {{ dataset.title }} - - - {{ dataset._id }} - - - - - - +
    + + + + + + + + + {{ dataset.title }} + + + {{ dataset._id }} + + + + +
    - - - -
    - - -
    -
    -
    -
    +
    + +
    + + +
    +
    +
    diff --git a/frontend/src/components/CollectionEdit.vue b/frontend/src/components/CollectionEdit.vue index e07546e2..e6cf66ca 100644 --- a/frontend/src/components/CollectionEdit.vue +++ b/frontend/src/components/CollectionEdit.vue @@ -1,53 +1,80 @@ @@ -117,14 +107,6 @@ export default { return this.$store.state.entries.entry; }, }, - - related: { - get () { - if ('related' in this.dataset) - return this.dataset.related.filter((entry) => entry._id !== this.dataset._id); - return [] - }, - } }, data () { diff --git a/frontend/src/components/DatasetEdit.vue b/frontend/src/components/DatasetEdit.vue index 3ec3b017..388d57f9 100644 --- a/frontend/src/components/DatasetEdit.vue +++ b/frontend/src/components/DatasetEdit.vue @@ -1,47 +1,45 @@ diff --git a/frontend/src/components/OrderAbout.vue b/frontend/src/components/OrderAbout.vue index 0848ed8b..9166b06c 100644 --- a/frontend/src/components/OrderAbout.vue +++ b/frontend/src/components/OrderAbout.vue @@ -7,95 +7,85 @@ - - - - - - - - - - {{ field }} {{ order.properties[field] }} - - - - {{ entry }} - - - - - - - - - - - - - - - {{ dataset.title }} - - - {{ dataset._id }} - - - - - - +
    + + {{ field }} {{ order.properties[field] }} + +
    - - - -
    - - -
    +
    + + + {{ entry }} + +
    -
    - - -
    +
    + +
    -
    - - -
    +
    + + + + + + + + + {{ dataset.title }} + + + {{ dataset._id }} + + + + +
    -
    - - -
    -
    -
    -
    +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    diff --git a/frontend/src/components/OrderEdit.vue b/frontend/src/components/OrderEdit.vue index b0f7176c..524c6720 100644 --- a/frontend/src/components/OrderEdit.vue +++ b/frontend/src/components/OrderEdit.vue @@ -1,92 +1,125 @@ @@ -173,7 +206,7 @@ export default { mounted () { this.isLoadingUsers = true; - this.$store.dispatch('adminUsers/getUsers') + this.$store.dispatch('entries/getEntries', 'user') .then(() => this.isLoadingUsers = false) .catch(() => this.isLoadingUsers = false); } diff --git a/frontend/src/components/PropertyEditor.vue b/frontend/src/components/PropertyEditor.vue index 160f401e..73666107 100644 --- a/frontend/src/components/PropertyEditor.vue +++ b/frontend/src/components/PropertyEditor.vue @@ -1,16 +1,5 @@ diff --git a/frontend/src/index.template.html b/frontend/src/index.template.html index 8157cbff..54b44e9d 100644 --- a/frontend/src/index.template.html +++ b/frontend/src/index.template.html @@ -1,5 +1,5 @@ - + <%= productName %> diff --git a/frontend/src/pages/CollectionInfo.vue b/frontend/src/pages/CollectionInfo.vue index 9c01e4b3..13bbda95 100644 --- a/frontend/src/pages/CollectionInfo.vue +++ b/frontend/src/pages/CollectionInfo.vue @@ -1,77 +1,92 @@