diff --git a/docs/user-guide/9-Sharing-objects.rst b/docs/user-guide/9-Sharing-objects.rst index 97860b3a2..c6bf0404e 100644 --- a/docs/user-guide/9-Sharing-objects.rst +++ b/docs/user-guide/9-Sharing-objects.rst @@ -148,11 +148,6 @@ Each capability has its own name and scope: Allows to access all users and groups in MWDB. Rules described in *Who is who?* don't apply to users with that permission. Enables user to create new user accounts, new groups and change their capabilities and membership. Allows to manage attribute keys, define new ones, delete and set the group permissions for them. -* - **share_queried_objects - Query for all objects in system** - - That one is a bit tricky and will be possibly deprecated. MWDB will automatically share object and all descendants with group if member directly accessed it via identifier (knows the hash e.g. have direct link to the object). It can be used for bot accounts, so they have access only to these objects that are intended to be processed by them. Internally, we abandoned that idea, so that capability may not be stable. - * **access_all_objects - Has access to all uploaded objects into system** diff --git a/mwdb/app.py b/mwdb/app.py index 21b26136e..cfb065b36 100755 --- a/mwdb/app.py +++ b/mwdb/app.py @@ -8,7 +8,6 @@ from mwdb.core.app import api, app from mwdb.core.config import app_config -from mwdb.core.deprecated import DeprecatedFeature, uses_deprecated_api from mwdb.core.log import getLogger, setup_logger from mwdb.core.metrics import metric_api_requests, metrics_enabled from mwdb.core.plugins import PluginAppContext, load_plugins @@ -42,7 +41,6 @@ ConfigResource, ConfigStatsResource, ) -from mwdb.resources.download import DownloadResource, RequestSampleDownloadResource from mwdb.resources.file import ( FileDownloadResource, FileDownloadZipResource, @@ -51,13 +49,6 @@ ) from mwdb.resources.group import GroupListResource, GroupMemberResource, GroupResource from mwdb.resources.karton import KartonAnalysisResource, KartonObjectResource -from mwdb.resources.metakey import ( - MetakeyDefinitionManageResource, - MetakeyListDefinitionManageResource, - MetakeyListDefinitionResource, - MetakeyPermissionResource, - MetakeyResource, -) from mwdb.resources.metrics import MetricsResource from mwdb.resources.oauth import ( OpenIDAccountIdentitiesResource, @@ -88,7 +79,6 @@ RemoteTextBlobPullResource, RemoteTextBlobPushResource, ) -from mwdb.resources.search import SearchResource from mwdb.resources.server import ( PingResource, ServerAdminInfoResource, @@ -204,11 +194,6 @@ def require_auth(): # Not a session token? Maybe APIKey token if g.auth_user is None: g.auth_user = APIKey.verify_token(token) - # Still nothing? Maybe legacy API key - if g.auth_user is None: - g.auth_user = User.verify_legacy_token(token) - if g.auth_user is not None: - uses_deprecated_api(DeprecatedFeature.legacy_api_key_v1) if g.auth_user: if ( @@ -307,13 +292,6 @@ def apply_rate_limit(): api.add_resource(TextBlobResource, "/blob") api.add_resource(TextBlobItemResource, "/blob/") -# Download endpoints -api.add_resource(RequestSampleDownloadResource, "/request/sample/") -api.add_resource(DownloadResource, "/download/") - -# Search endpoints -api.add_resource(SearchResource, "/search") - # Quick query endpoints api.add_resource( QuickQueryResource, "//quick_query" @@ -334,17 +312,6 @@ def apply_rate_limit(): api.add_resource(AttributeDefinitionResource, "/attribute/") api.add_resource(AttributePermissionResource, "/attribute//permissions") -# Attribute (metakey) deprecated endpoints -api.add_resource(MetakeyListDefinitionResource, "/meta/list/") -api.add_resource( - MetakeyResource, "///meta" -) -api.add_resource(MetakeyListDefinitionManageResource, "/meta/manage") -api.add_resource(MetakeyDefinitionManageResource, "/meta/manage/") -api.add_resource( - MetakeyPermissionResource, "/meta/manage//permissions/" -) - # Karton endpoints api.add_resource( KartonObjectResource, diff --git a/mwdb/core/auth.py b/mwdb/core/auth.py index ab3f89ac2..1588e1647 100644 --- a/mwdb/core/auth.py +++ b/mwdb/core/auth.py @@ -1,6 +1,6 @@ import datetime from enum import Enum -from typing import Any, Set +from typing import Any import jwt @@ -52,14 +52,14 @@ def verify_token(token: str, scope: AuthScope) -> Any: return data -def verify_legacy_token(token: str, required_fields: Set[str]) -> Any: +def verify_legacy_api_key(token: str) -> Any: try: data = jwt.decode( token, key=app_config.mwdb.secret_key, algorithms=["HS512"], ) - if set(data.keys()) != required_fields: + if set(data.keys()) != {"login", "api_key_id"}: return None except jwt.InvalidTokenError: diff --git a/mwdb/core/capabilities.py b/mwdb/core/capabilities.py index e656aaf44..40debc92c 100644 --- a/mwdb/core/capabilities.py +++ b/mwdb/core/capabilities.py @@ -1,8 +1,6 @@ class Capabilities(object): # Can create/update users and groups manage_users = "manage_users" - # Queried objects by members are automatically shared with this group - share_queried_objects = "share_queried_objects" # All new uploaded objects are automatically shared with this group access_all_objects = "access_all_objects" # Can share objects with all groups, have access to complete list of groups diff --git a/mwdb/core/deprecated.py b/mwdb/core/deprecated.py index d6e8542b7..c17c57c95 100644 --- a/mwdb/core/deprecated.py +++ b/mwdb/core/deprecated.py @@ -16,35 +16,9 @@ class DeprecatedFeature(Enum): - # Unmanageable API keys, deprecated in v2.0.0 - legacy_api_key_v1 = "legacy_api_key_v1" # API keys non-complaint with RFC7519 # Deprecated in v2.7.0 legacy_api_key_v2 = "legacy_api_key_v2" - # Legacy PUT/POST /api// - # Use POST /api/ instead - # Deprecated in v2.0.0 - legacy_object_upload = "legacy_file_upload" - # Legacy /request/sample/ - # Use /file//download instead - # Deprecated in v2.2.0 - legacy_file_download = "legacy_file_download" - # Legacy /search - # Use GET / instead - # Deprecated in v2.0.0 - legacy_search = "legacy_search" - # Legacy ?page parameter in object listing endpoints - # Use "?older_than" instead - # Deprecated in v2.0.0 - legacy_page_parameter = "legacy_page_parameter" - # Legacy Metakey API - # Use Attribute API instead - # Deprecated in v2.6.0 - legacy_metakey_api = "legacy_metakey_api" - # Legacy Metakey API - # Use Attribute API instead - # Deprecated in v2.6.0 - legacy_metakeys_upload_option = "legacy_metakeys_upload_option" def uses_deprecated_api( diff --git a/mwdb/model/api_key.py b/mwdb/model/api_key.py index 3b08bc11f..c1004e9cc 100644 --- a/mwdb/model/api_key.py +++ b/mwdb/model/api_key.py @@ -4,7 +4,12 @@ from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm.exc import NoResultFound -from mwdb.core.auth import AuthScope, generate_token, verify_legacy_token, verify_token +from mwdb.core.auth import ( + AuthScope, + generate_token, + verify_legacy_api_key, + verify_token, +) from mwdb.core.deprecated import DeprecatedFeature, uses_deprecated_api from . import db @@ -35,7 +40,7 @@ def verify_token(token): if data is None: # check for legacy API Token - data = verify_legacy_token(token, required_fields={"login", "api_key_id"}) + data = verify_legacy_api_key(token) if data is None: return None else: diff --git a/mwdb/model/attribute.py b/mwdb/model/attribute.py index 09813f766..66e3ad52d 100644 --- a/mwdb/model/attribute.py +++ b/mwdb/model/attribute.py @@ -1,5 +1,3 @@ -from string import Template - from flask import g from sqlalchemy import cast, func from sqlalchemy.dialects.postgresql import JSONB @@ -35,14 +33,6 @@ class Attribute(db.Model): value = db.Column(JSONB, nullable=False) template = db.relationship("AttributeDefinition", lazy="joined") - @property - def url(self): - # deprecated, left for metakey compatibility - if self.template.url_template: - s = Template(self.template.url_template) - return s.safe_substitute(value=self.value) - return None - @property def label(self): return self.template.label diff --git a/mwdb/model/migrations/versions/465b589d0362_remove_share_queried_objects_from_groups.py b/mwdb/model/migrations/versions/465b589d0362_remove_share_queried_objects_from_groups.py new file mode 100644 index 000000000..a35b99e92 --- /dev/null +++ b/mwdb/model/migrations/versions/465b589d0362_remove_share_queried_objects_from_groups.py @@ -0,0 +1,25 @@ +"""Remove share_queried_objects from groups + +Revision ID: 465b589d0362 +Revises: 56adf974831e +Create Date: 2024-12-12 14:39:21.117379 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = "465b589d0362" +down_revision = "56adf974831e" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute( + "UPDATE \"group\" SET capabilities = array_remove(capabilities, 'share_queried_objects');" + ) + pass + + +def downgrade(): + pass diff --git a/mwdb/model/migrations/versions/56adf974831e_removed_user_version_uid_column.py b/mwdb/model/migrations/versions/56adf974831e_removed_user_version_uid_column.py new file mode 100644 index 000000000..d6ad29010 --- /dev/null +++ b/mwdb/model/migrations/versions/56adf974831e_removed_user_version_uid_column.py @@ -0,0 +1,32 @@ +"""Removed User.version_uid column + +Revision ID: 56adf974831e +Revises: 52bb55f76ef3 +Create Date: 2024-12-11 18:21:42.442984 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "56adf974831e" +down_revision = "52bb55f76ef3" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("user", "version_uid") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "user", + sa.Column( + "version_uid", sa.VARCHAR(length=16), autoincrement=False, nullable=True + ), + ) + # ### end Alembic commands ### diff --git a/mwdb/model/object.py b/mwdb/model/object.py index c5b4e4c37..d9ed7d7dd 100644 --- a/mwdb/model/object.py +++ b/mwdb/model/object.py @@ -1,5 +1,4 @@ import datetime -from collections import namedtuple from typing import Any, Dict, Optional from uuid import UUID @@ -475,8 +474,6 @@ def access(cls, identifier, requestor=None): (default: currently authenticated user) :return: Object instance or None """ - from .group import Group - if requestor is None: requestor = g.auth_user @@ -489,23 +486,6 @@ def access(cls, identifier, requestor=None): if obj.has_explicit_access(requestor): return obj - # If not, but has "share_queried_objects" rights: give_access - if requestor.has_rights(Capabilities.share_queried_objects): - share_queried_groups = ( - db.session.query(Group) - .filter( - and_( - Group.capabilities.contains( - [Capabilities.share_queried_objects] - ), - requestor.is_member(Group.id), - ) - ) - .all() - ) - for group in share_queried_groups: - obj.give_access(group.id, AccessType.QUERIED, obj, requestor) - return obj # Well.. I've tried return None @@ -596,7 +576,6 @@ def get_attributes( as_dict=False, check_permissions=True, show_hidden=False, - show_karton=False, ): """ Gets all object attributes @@ -606,7 +585,6 @@ def get_attributes( :param check_permissions: | Filter results including current user permissions (default: True) :param show_hidden: Show hidden attributes - :param show_karton: Show Karton attributes (for compatibility) """ attributes = ( db.session.query(Attribute) @@ -630,19 +608,6 @@ def get_attributes( attributes = attributes.order_by(Attribute.id).all() - if show_karton: - KartonAttribute = namedtuple("KartonAttribute", ["key", "value"]) - - attributes += [ - KartonAttribute(key="karton", value=str(analysis.id)) - for analysis in ( - db.session.query(KartonAnalysis) - .filter(KartonAnalysis.objects.any(id=self.id)) - .order_by(KartonAnalysis.creation_time) - .all() - ) - ] - if not as_dict: return attributes @@ -654,23 +619,12 @@ def get_attributes( return dict_attributes def add_attribute( - self, key, value, commit=True, check_permissions=True, include_karton=True + self, + key, + value, + commit=True, + check_permissions=True, ): - if include_karton and key == "karton": - karton_id = UUID(value) - - if check_permissions and not g.auth_user.has_rights( - Capabilities.karton_assign - ): - # User doesn't have permissions to assign analysis - return None - - _, is_new = self.assign_analysis(karton_id, commit=False) - - if commit: - db.session.commit() - return is_new - if check_permissions: attribute_definition = AttributeDefinition.query_for_set(key).first() else: diff --git a/mwdb/model/user.py b/mwdb/model/user.py index 9c697d062..d0653c428 100644 --- a/mwdb/model/user.py +++ b/mwdb/model/user.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm.exc import NoResultFound -from mwdb.core.auth import AuthScope, generate_token, verify_legacy_token, verify_token +from mwdb.core.auth import AuthScope, generate_token, verify_token from mwdb.core.capabilities import Capabilities from . import db @@ -25,8 +25,6 @@ class User(db.Model): email = db.Column(db.String(128), nullable=False) password_hash = db.Column(db.String(128)) - # Legacy "version_uid", todo: remove it when users are ready - version_uid = db.Column(db.String(16)) # Password version (set password link and session token validation) # Invalidates set password link or session when password has been changes password_ver = db.Column(db.String(16)) @@ -224,22 +222,6 @@ def verify_set_password_token(token) -> Optional["User"]: ) return None if result is None else result[0] - @staticmethod - def verify_legacy_token(token): - data = verify_legacy_token(token, required_fields={"login", "version_uid"}) - if data is None: - return None - - try: - user_obj = User.query.filter(User.login == data["login"]).one() - except NoResultFound: - return None - - if user_obj.version_uid != data["version_uid"]: - return None - - return user_obj - def is_member(self, group_id): groups = db.session.query(Member.group_id).filter(Member.user_id == self.id) return group_id.in_(groups) diff --git a/mwdb/resources/attribute.py b/mwdb/resources/attribute.py index 4bc8ddc2a..6c87ae7c0 100644 --- a/mwdb/resources/attribute.py +++ b/mwdb/resources/attribute.py @@ -154,7 +154,7 @@ def post(self, type, identifier): key = obj["key"] value = obj["value"] - is_new = db_object.add_attribute(key, value, include_karton=False) + is_new = db_object.add_attribute(key, value) if is_new is None: raise NotFound( f"Attribute '{key}' is not defined or you have " diff --git a/mwdb/resources/blob.py b/mwdb/resources/blob.py index d000a8b66..5c2e35f0f 100644 --- a/mwdb/resources/blob.py +++ b/mwdb/resources/blob.py @@ -2,14 +2,12 @@ from werkzeug.exceptions import Conflict from mwdb.core.capabilities import Capabilities -from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint from mwdb.core.plugins import hooks from mwdb.model import TextBlob from mwdb.model.object import ObjectTypeConflictError from mwdb.schema.blob import ( BlobCreateRequestSchema, BlobItemResponseSchema, - BlobLegacyCreateRequestSchema, BlobListResponseSchema, ) @@ -146,9 +144,6 @@ def post(self): content: "blob contents" parent: null upload_as: "*" - metakeys: - - key: string - value: string attributes: - key: string value: string @@ -166,7 +161,7 @@ def post(self): 403: description: | No permissions to perform additional operations - (e.g. adding metakeys) + (e.g. adding attributes) 404: description: Specified group doesn't exist 409: @@ -181,10 +176,9 @@ def post(self): return self.create_object(obj) -class TextBlobItemResource(ObjectItemResource, TextBlobUploader): +class TextBlobItemResource(ObjectItemResource): ObjectType = TextBlob ItemResponseSchema = BlobItemResponseSchema - CreateRequestSchema = BlobLegacyCreateRequestSchema def call_specialised_remove_hook(self, text_blob): hooks.on_removed_text_blob(text_blob) @@ -222,101 +216,6 @@ def get(self, identifier): """ return super().get(identifier) - @deprecated_endpoint(DeprecatedFeature.legacy_object_upload) - @requires_authorization - @requires_capabilities(Capabilities.adding_blobs) - def put(self, identifier): - """ - --- - summary: Upload text blob - description: | - Uploads a new text blob. - - Requires `adding_blobs` capability. - - Deprecated: use POST /blob method instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - blob - parameters: - - in: path - name: identifier - schema: - type: string - default: root - description: | - Parent object identifier or `root` if there is no parent. - - User must have `adding_parents` capability to specify a parent object. - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - description: | - Blob to be uploaded with additional parameters - (verbose mode) - properties: - json: - type: object - properties: - blob_name: - type: string - blob_type: - type: string - content: - type: string - description: JSON-encoded blob object specification - metakeys: - type: object - properties: - metakeys: - type: array - items: - $ref: '#/components/schemas/MetakeyItemRequest' - description: | - Attributes to be added after file upload - - User must be allowed to set specified attribute keys. - upload_as: - type: string - default: '*' - description: | - Group that object will be shared with. - - If user doesn't have `sharing_with_all` capability, - user must be a member of specified group - (unless `Group doesn't exist` error will occur). - - If default value `*` is specified - object will be - exclusively shared with all user's groups excluding `public`. - required: - - json - application/json: - schema: BlobCreateSpecSchema - responses: - 200: - description: Text blob uploaded succesfully - content: - application/json: - schema: BlobItemResponseSchema - 403: - description: | - No permissions to perform additional operations - (e.g. adding metakeys) - 404: - description: Specified group doesn't exist - 409: - description: Object exists yet but has different type - 503: - description: | - Request canceled due to database statement timeout. - """ - return super().put(identifier) - @requires_authorization @requires_capabilities(Capabilities.removing_objects) def delete(self, identifier): diff --git a/mwdb/resources/config.py b/mwdb/resources/config.py index 30f53d830..644c484a9 100644 --- a/mwdb/resources/config.py +++ b/mwdb/resources/config.py @@ -5,7 +5,6 @@ from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound from mwdb.core.capabilities import Capabilities -from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint from mwdb.core.plugins import hooks from mwdb.core.service import Resource from mwdb.model import Config, TextBlob, db @@ -14,7 +13,6 @@ from mwdb.schema.config import ( ConfigCreateRequestSchema, ConfigItemResponseSchema, - ConfigLegacyCreateRequestSchema, ConfigListResponseSchema, ConfigStatsRequestSchema, ConfigStatsResponseSchema, @@ -262,9 +260,6 @@ def post(self): config_type: static parent: null upload_as: "*" - metakeys: - - key: string - value: string attributes: - key: string value: string @@ -282,7 +277,7 @@ def post(self): 403: description: | No permissions to perform additional operations - (e.g. adding parent, metakeys) + (e.g. adding parent, attributes) 404: description: | One of attribute keys doesn't exist @@ -302,11 +297,10 @@ def post(self): return self.create_object(obj) -class ConfigItemResource(ObjectItemResource, ConfigUploader): +class ConfigItemResource(ObjectItemResource): ObjectType = Config ItemResponseSchema = ConfigItemResponseSchema - CreateRequestSchema = ConfigLegacyCreateRequestSchema def call_specialised_remove_hook(self, config): hooks.on_removed_config(config) @@ -344,108 +338,6 @@ def get(self, identifier): """ return super().get(identifier) - @deprecated_endpoint(DeprecatedFeature.legacy_object_upload) - @requires_authorization - @requires_capabilities(Capabilities.adding_configs) - def put(self, identifier): - """ - --- - summary: Upload config - description: | - Uploads a new config. - - Requires `adding_configs` capability. - - Deprecated: use POST /config method instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - config - parameters: - - in: path - name: identifier - schema: - type: string - default: root - description: | - Parent object identifier or `root` if there is no parent. - - User must have `adding_parents` capability to specify a parent object. - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - description: | - Configuration to be uploaded with additional parameters - (verbose mode) - properties: - json: - type: object - properties: - family: - type: string - config_type: - type: string - default: static - cfg: - type: object - description: JSON-encoded config object specification - metakeys: - type: object - properties: - metakeys: - type: array - items: - $ref: '#/components/schemas/MetakeyItemRequest' - description: | - Attributes to be added after file upload - - User must be allowed to set specified attribute keys. - upload_as: - type: string - default: '*' - description: | - Group that object will be shared with. - - If user doesn't have `sharing_with_all` capability, - user must be a member of specified group - (unless `Group doesn't exist` error will occur). - - If default value `*` is specified - object will be - exclusively shared with all user's groups excluding `public`. - required: - - json - application/json: - schema: ConfigCreateSpecSchema - responses: - 200: - description: Information about uploaded config - content: - application/json: - schema: ConfigItemResponseSchema - 403: - description: | - No permissions to perform additional operations - (e.g. adding parent, metakeys) - 404: - description: | - One of attribute keys doesn't exist or - user doesn't have permission to set it. - - Specified `upload_as` group doesn't exist or - user doesn't have permission to share objects - with that group - 409: - description: Object exists yet but has different type - 503: - description: | - Request canceled due to database statement timeout. - """ - return super().put(identifier) - @requires_authorization @requires_capabilities(Capabilities.removing_objects) def delete(self, identifier): diff --git a/mwdb/resources/download.py b/mwdb/resources/download.py deleted file mode 100644 index eef553a21..000000000 --- a/mwdb/resources/download.py +++ /dev/null @@ -1,101 +0,0 @@ -from flask import Response -from werkzeug.exceptions import Forbidden, NotFound - -from mwdb.core.app import api -from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint -from mwdb.core.service import Resource -from mwdb.model import File -from mwdb.schema.download import DownloadURLResponseSchema - -from . import requires_authorization - - -class DownloadResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_file_download) - def get(self, access_token): - """ - --- - summary: Download file - description: | - Returns file contents based on provided file download token. - - Deprecated: use GET /file/{identifier}/download instead. - deprecated: true - tags: - - file - parameters: - - in: path - name: access_token - schema: - type: string - required: true - description: File download token - responses: - 200: - description: File contents - content: - application/octet-stream: - schema: - type: string - format: binary - 403: - description: When file download token is no longer valid - 503: - description: | - Request canceled due to database statement timeout. - """ - file_obj = File.get_by_download_token(access_token) - if not file_obj: - raise Forbidden("Download token expired, please re-request download.") - - return Response( - file_obj.iterate(), - content_type="application/octet-stream", - headers={"Content-disposition": f"attachment; filename={file_obj.sha256}"}, - ) - - -class RequestSampleDownloadResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_file_download) - @requires_authorization - def post(self, identifier): - """ - --- - summary: Get file download URL - description: | - Returns download URL for given file. - - Deprecated: use POST /file/{identifier}>/download instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - file - parameters: - - in: path - name: identifier - description: Requested file identifier (SHA256/MD5/SHA1/SHA512) - schema: - type: string - responses: - 200: - description: Absolute download URL for the sample, valid for 60 seconds - content: - application/json: - schema: DownloadURLResponseSchema - 404: - description: | - When file doesn't exist, object is not a file - or user doesn't have access to this object. - 503: - description: | - Request canceled due to database statement timeout. - """ - file = File.access(identifier) - if file is None: - raise NotFound("Object not found") - - download_token = file.generate_download_token() - schema = DownloadURLResponseSchema() - url = api.relative_url_for(DownloadResource, access_token=download_token) - return schema.dump({"url": url}) diff --git a/mwdb/resources/file.py b/mwdb/resources/file.py index fd6c33c3c..213f474fd 100644 --- a/mwdb/resources/file.py +++ b/mwdb/resources/file.py @@ -2,7 +2,6 @@ from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound, Unauthorized from mwdb.core.capabilities import Capabilities -from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint from mwdb.core.plugins import hooks from mwdb.core.service import Resource from mwdb.model import File @@ -12,7 +11,6 @@ FileCreateRequestSchema, FileDownloadTokenResponseSchema, FileItemResponseSchema, - FileLegacyCreateRequestSchema, FileListResponseSchema, ) @@ -156,10 +154,6 @@ def post(self): type: array items: $ref: '#/components/schemas/AttributeItemRequest' - metakeys: - type: array - items: - $ref: '#/components/schemas/MetakeyItemRequest' tags: type: array items: @@ -198,10 +192,9 @@ def post(self): return self.create_object(obj["options"]) -class FileItemResource(ObjectItemResource, FileUploader): +class FileItemResource(ObjectItemResource): ObjectType = File ItemResponseSchema = FileItemResponseSchema - CreateRequestSchema = FileLegacyCreateRequestSchema def call_specialised_remove_hook(self, file): hooks.on_removed_file(file) @@ -239,95 +232,6 @@ def get(self, identifier): """ return super().get(identifier) - @deprecated_endpoint(DeprecatedFeature.legacy_object_upload) - @requires_authorization - @requires_capabilities(Capabilities.adding_files) - def post(self, identifier): - """ - --- - summary: Upload file - description: | - Uploads a new file. - - Requires `adding_files` capability. - - Deprecated: use POST /file instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - file - parameters: - - in: path - name: identifier - schema: - type: string - default: 'root' - description: | - Parent object identifier or `root` if there is no parent. - - User must have `adding_parents` capability to specify a parent object. - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - description: File contents to be uploaded - metakeys: - type: object - properties: - metakeys: - type: array - items: - $ref: '#/components/schemas/MetakeyItemRequest' - description: | - Attributes to be added after file upload - - User must be allowed to set specified attribute keys. - upload_as: - type: string - default: '*' - description: | - Group that object will be shared with. - - If user doesn't have `sharing_with_all` capability, - user must be a member of specified group - (unless `Group doesn't exist` error will occur). - - If default value `*` is specified - object will be - exclusively shared with all user's groups excluding `public`. - required: - - file - responses: - 200: - description: Information about uploaded file - content: - application/json: - schema: FileItemResponseSchema - 403: - description: | - No permissions to perform additional operations - (e.g. adding parent, attributes) - 404: - description: | - One of attribute keys doesn't exist or user doesn't have - permission to set it. - - Specified `upload_as` group doesn't exist or user doesn't have - permission to share objects with that group - 409: - description: Object exists yet but has different type - 503: - description: | - Request canceled due to database statement timeout. - """ - return super().post(identifier) - @requires_authorization @requires_capabilities(Capabilities.removing_objects) def delete(self, identifier): diff --git a/mwdb/resources/metakey.py b/mwdb/resources/metakey.py deleted file mode 100644 index 7f8d294dc..000000000 --- a/mwdb/resources/metakey.py +++ /dev/null @@ -1,739 +0,0 @@ -from flask import g, request -from werkzeug.exceptions import BadRequest, Forbidden, NotFound - -from mwdb.core.capabilities import Capabilities -from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint -from mwdb.core.service import Resource -from mwdb.model import AttributeDefinition, AttributePermission, Group, db -from mwdb.schema.metakey import ( - MetakeyDefinitionItemRequestArgsSchema, - MetakeyDefinitionItemRequestBodySchema, - MetakeyDefinitionItemResponseSchema, - MetakeyDefinitionListResponseSchema, - MetakeyDefinitionManageItemResponseSchema, - MetakeyDefinitionManageListResponseSchema, - MetakeyItemRemoveRequestSchema, - MetakeyItemRequestSchema, - MetakeyKeySchema, - MetakeyListRequestSchema, - MetakeyListResponseSchema, - MetakeyPermissionSetRequestArgsSchema, - MetakeyPermissionSetRequestBodySchema, - MetakeyUpdateRequestSchema, -) - -from . import ( - access_object, - is_valid_uuid, - load_schema, - loads_schema, - logger, - requires_authorization, - requires_capabilities, -) - - -class MetakeyResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - def get(self, type, identifier): - """ - --- - summary: Get object attributes - description: | - Returns all attributes of specified object that user is allowed to read. - - Deprecated: use Attributes API instead - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: type - schema: - type: string - enum: [file, config, blob, object] - description: Type of object - - in: path - name: identifier - schema: - type: string - description: Object identifier - - in: query - name: hidden - schema: - type: int - description: | - Show hidden attributes - (requires `reading_all_attributes` capability) - required: false - responses: - 200: - description: Object attributes - content: - application/json: - schema: MetakeyListResponseSchema - 403: - description: | - When user requested hidden metakeys - but doesn't have `reading_all_attributes` capability - 404: - description: | - When object doesn't exist or user doesn't have - access to this object. - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = MetakeyListRequestSchema() - obj = load_schema(request.args, schema) - - show_hidden = obj["hidden"] - if show_hidden and not g.auth_user.has_rights( - Capabilities.reading_all_attributes - ): - raise Forbidden("You are not permitted to read hidden metakeys") - - db_object = access_object(type, identifier) - if db_object is None: - raise NotFound("Object not found") - - metakeys = db_object.get_attributes(show_hidden=show_hidden, show_karton=True) - schema = MetakeyListResponseSchema() - return schema.dump({"metakeys": metakeys}) - - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - def post(self, type, identifier): - """ - --- - summary: Add object attribute - description: | - Adds attribute to specified object. - - User must have `set` access to the attribute key - or `adding_all_attributes` capability. - - Deprecated: use Attributes API instead - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: type - schema: - type: string - enum: [file, config, blob, object] - description: Type of object - - in: path - name: identifier - schema: - type: string - description: Object identifier - requestBody: - description: Attribute key and value - content: - application/json: - schema: MetakeyItemRequestSchema - responses: - 200: - description: When metakey was added successfully - content: - application/json: - schema: MetakeyListResponseSchema - 400: - description: For karton attribute when value is not UUID - 404: - description: | - When object doesn't exist or user doesn't have - access to this object. - - When attribute key is not defined or user doesn't have - privileges to set that one. - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = MetakeyItemRequestSchema() - obj = loads_schema(request.get_data(as_text=True), schema) - - db_object = access_object(type, identifier) - if db_object is None: - raise NotFound("Object not found") - - key = obj["key"] - value = obj["value"] - - if key == "karton" and not is_valid_uuid(value): - raise BadRequest("'karton' attribute accepts only UUID values") - - is_new = db_object.add_attribute(key, value) - if is_new is None: - raise NotFound( - f"Attribute '{key}' is not defined or you have " - f"insufficient permissions to set it" - ) - - db.session.commit() - db.session.refresh(db_object) - metakeys = db_object.get_attributes(show_karton=True) - schema = MetakeyListResponseSchema() - return schema.dump({"metakeys": metakeys}) - - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities("removing_attributes") - def delete(self, type, identifier): - """ - --- - summary: Delete object attribute - description: | - Deletes attribute from specified object. - - User must have `removing_attributes` capability. - - If value is not specified, all values under the specified - key are removed. - - Deprecated: use Attributes API instead - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: type - schema: - type: string - enum: [file, config, blob, object] - description: Type of object - - in: path - name: identifier - schema: - type: string - description: Object identifier - - in: query - name: key - schema: - type: string - description: Key of attribute object to be deleted - required: true - - in: query - name: value - schema: - type: string - description: Value of attribute key object to be deleted - required: false - responses: - 200: - description: When metakey was deleted successfully - 404: - description: | - When object doesn't exist or user doesn't have access - to this object. - When attribute key is not defined or user doesn't have privileges - to set that one. - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = MetakeyItemRemoveRequestSchema() - obj = load_schema(request.args, schema) - - db_object = access_object(type, identifier) - if db_object is None: - raise NotFound("Object not found") - - key = obj["key"] - value = obj.get("value") - - deleted_object = db_object.remove_attribute(key, value) - if deleted_object is False: - raise NotFound( - f"Attribute '{key}' is not defined or you have " - f"insufficient permissions to delete it" - ) - db.session.commit() - - -class MetakeyListDefinitionResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - def get(self, access): - """ - --- - summary: Get list of attribute keys - description: | - Returns list of attribute keys which currently authenticated user - can read or set. - - Deprecated: use Attributes API instead - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: access - schema: - type: string - enum: [read, set] - description: Type of desired access - responses: - 200: - description: List of attribute keys and definitions - content: - application/json: - schema: MetakeyDefinitionListResponseSchema - 400: - description: When used unknown access type (other than read or set) - 503: - description: | - Request canceled due to database statement timeout. - """ - if access == "read": - metakeys = AttributeDefinition.query_for_read() - elif access == "set": - metakeys = AttributeDefinition.query_for_set() - else: - raise BadRequest(f"Unknown desired access type '{access}'") - - metakeys = metakeys.order_by(AttributeDefinition.key).all() - schema = MetakeyDefinitionListResponseSchema() - return schema.dump({"metakeys": metakeys}) - - -class MetakeyListDefinitionManageResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities(Capabilities.manage_users) - def get(self): - """ - --- - summary: Get attribute key definitions - description: | - Returns list of attribute key definitions. - - Requires `manage_users` capability. - - Deprecated: use Attributes API instead - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - responses: - 200: - description: List of attribute keys and definitions - content: - application/json: - schema: MetakeyDefinitionManageListResponseSchema - 403: - description: When user doesn't have `manage_users` capability. - 503: - description: | - Request canceled due to database statement timeout. - """ - metakeys = ( - db.session.query(AttributeDefinition) - .order_by(AttributeDefinition.key) - .all() - ) - schema = MetakeyDefinitionManageListResponseSchema() - return schema.dump({"metakeys": metakeys}) - - -class MetakeyDefinitionManageResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities(Capabilities.manage_users) - def get(self, key): - """ - --- - summary: Get attribute key details - description: | - Returns attribute key definition details. - - Requires `manage_users` capability. - - Deprecated: use Attributes API instead - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: key - schema: - type: string - description: Attribute key - responses: - 200: - description: Attribute key definition - content: - application/json: - schema: MetakeyDefinitionManageItemResponseSchema - 403: - description: When user doesn't have `manage_users` capability. - 404: - description: When specified attribute key doesn't exist - 503: - description: | - Request canceled due to database statement timeout. - """ - metakey = ( - db.session.query(AttributeDefinition) - .filter(AttributeDefinition.key == key) - .first() - ) - if metakey is None: - raise NotFound("No such metakey") - schema = MetakeyDefinitionManageItemResponseSchema() - return schema.dump(metakey) - - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities(Capabilities.manage_users) - def post(self, key): - """ - --- - summary: Create attribute key - description: | - Creates attribute key definition. - - Requires `manage_users` capability. - - Deprecated: use Attributes API instead - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: key - schema: - type: string - description: Attribute key - requestBody: - description: Attribute key definition - content: - application/json: - schema: MetakeyDefinitionItemRequestBodySchema - responses: - 200: - description: When metakey definition is successfully added - content: - application/json: - schema: MetakeyDefinitionItemResponseSchema - 400: - description: | - When one of attribute definition fields is missing or incorrect. - 403: - description: When user doesn't have `manage_users` capability. - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = MetakeyDefinitionItemRequestArgsSchema() - args_obj = load_schema({"key": key}, schema) - - schema = MetakeyDefinitionItemRequestBodySchema() - obj = loads_schema(request.get_data(as_text=True), schema) - - metakey_definition = AttributeDefinition( - key=args_obj["key"], - url_template=obj["url_template"], - label=obj["label"], - description=obj["description"], - hidden=obj["hidden"], - ) - metakey_definition = db.session.merge(metakey_definition) - db.session.commit() - - schema = MetakeyDefinitionItemResponseSchema() - return schema.dump(metakey_definition) - - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities(Capabilities.manage_users) - def put(self, key): - """ - --- - summary: Update attribute key - description: | - Update attribute key definition. - - Requires `manage_users` capability. - - Deprecated: use Attribute API instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: key - schema: - type: string - description: Attribute key - requestBody: - description: Attribute definition to update - content: - application/json: - schema: MetakeyUpdateRequestSchema - responses: - 200: - description: When metakey definition is successfully updated - content: - application/json: - schema: MetakeyDefinitionItemResponseSchema - 400: - description: | - When one of attribute definition fields is missing or incorrect. - 403: - description: When user doesn't have `manage_users` capability. - 404: - description: When metakey doesn't exist. - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = MetakeyUpdateRequestSchema() - obj = loads_schema(request.get_data(as_text=True), schema) - - metakey_obj = load_schema({"key": key}, MetakeyKeySchema()) - metakey = ( - db.session.query(AttributeDefinition) - .filter(AttributeDefinition.key == metakey_obj["key"]) - .first() - ) - if metakey is None: - raise NotFound("No such metakey") - - label = obj["label"] - if label is not None: - metakey.label = label - - description = obj["description"] - if description is not None: - metakey.description = description - - url_template = obj["template"] - if url_template is not None: - metakey.url_template = url_template - - hidden = obj["hidden"] - if hidden is not None: - metakey.hidden = obj["hidden"] - - db.session.commit() - logger.info("Attribute updated", extra=obj) - - schema = MetakeyDefinitionItemResponseSchema() - return schema.dump(metakey) - - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities(Capabilities.manage_users) - def delete(self, key): - """ - --- - summary: Delete attribute key - description: | - Deletes attribute key including all related object attributes. - - Requires `manage_users` capability. - - Deprecated: use Attribute API instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: key - schema: - type: string - description: Attribute key - responses: - 200: - description: When attribute key was deleted - 403: - description: When user doesn't have `manage_users` capability. - 404: - description: When specified attribute key doesn't exist - 503: - description: | - Request canceled due to database statement timeout. - """ - metakey = ( - db.session.query(AttributeDefinition) - .filter(AttributeDefinition.key == key) - .first() - ) - if metakey is None: - raise NotFound("No such metakey") - db.session.delete(metakey) - db.session.commit() - - -class MetakeyPermissionResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities(Capabilities.manage_users) - def put(self, key, group_name): - """ - --- - summary: Add/modify attribute key permission - description: | - Adds or modifies attribute key group permission - for specified key and group name. - - Requires `manage_users` capability. - - Deprecated: use Attribute API instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: key - schema: - type: string - description: Attribute key - - in: path - name: group_name - schema: - type: string - description: Group name to add/modify - requestBody: - description: Attribute key permission definition - content: - application/json: - schema: MetakeyPermissionSetRequestBodySchema - responses: - 200: - description: When group permission has been successfully changed - content: - application/json: - schema: MetakeyDefinitionManageItemResponseSchema - 400: - description: | - When one of attribute permission fields is missing or incorrect. - 403: - description: When user doesn't have `manage_users` capability. - 404: - description: When attribute key or group doesn't exist - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = MetakeyPermissionSetRequestArgsSchema() - args_obj = load_schema({"key": key, "group_name": group_name}, schema) - - schema = MetakeyPermissionSetRequestBodySchema() - obj = loads_schema(request.get_data(as_text=True), schema) - - metakey_definition = ( - db.session.query(AttributeDefinition) - .filter(AttributeDefinition.key == args_obj["key"]) - .first() - ) - if metakey_definition is None: - raise NotFound("No such metakey") - - group = ( - db.session.query(Group).filter(Group.name == args_obj["group_name"]).first() - ) - if group is None: - raise NotFound("No such group") - - permission = AttributePermission( - key=args_obj["key"], - group_id=group.id, - can_read=obj["can_read"], - can_set=obj["can_set"], - ) - db.session.merge(permission) - db.session.commit() - - db.session.refresh(metakey_definition) - schema = MetakeyDefinitionManageItemResponseSchema() - return schema.dump(metakey_definition) - - @deprecated_endpoint(DeprecatedFeature.legacy_metakey_api) - @requires_authorization - @requires_capabilities(Capabilities.manage_users) - def delete(self, key, group_name): - """ - --- - summary: Delete attribute key permission - description: | - Removes attribute key permission for specified key and group name. - - Requires `manage_users` capability. - - Deprecated: use Attribute API instead. - security: - - bearerAuth: [] - deprecated: true - tags: - - metakey - parameters: - - in: path - name: key - schema: - type: string - description: Attribute key - - in: path - name: group_name - schema: - type: string - description: Group name to remove - responses: - 200: - description: When group permission has been successfully removed - 403: - description: When user doesn't have `manage_users` capability. - 404: - description: | - When attribute key or group or group permission doesn't exist - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = MetakeyPermissionSetRequestArgsSchema() - args_obj = load_schema({"key": key, "group_name": group_name}, schema) - - group = ( - db.session.query(Group).filter(Group.name == args_obj["group_name"]).first() - ) - if group is None: - raise NotFound("No such group") - - metakey_permission = ( - db.session.query(AttributePermission) - .filter( - AttributePermission.key == args_obj["key"], - AttributePermission.group_id == group.id, - ) - .first() - ) - - if metakey_permission is None: - raise NotFound("No such metakey permission") - - db.session.delete(metakey_permission) - db.session.commit() diff --git a/mwdb/resources/object.py b/mwdb/resources/object.py index 82227ef62..a077ba991 100644 --- a/mwdb/resources/object.py +++ b/mwdb/resources/object.py @@ -1,12 +1,8 @@ -import json -from uuid import UUID - from flask import g, request -from werkzeug.exceptions import BadRequest, Forbidden, MethodNotAllowed, NotFound +from werkzeug.exceptions import BadRequest, Forbidden, NotFound from mwdb.core.capabilities import Capabilities from mwdb.core.config import app_config -from mwdb.core.deprecated import DeprecatedFeature, uses_deprecated_api from mwdb.core.plugins import hooks from mwdb.core.search import QueryBaseException, build_query from mwdb.core.service import Resource @@ -33,9 +29,6 @@ class ObjectUploader: """ Mixin adding common object upload capabilities to resource - - Merge it with ObjectsResource during retirement of ObjectResource - deprecated upload methods """ ObjectType = None @@ -74,44 +67,19 @@ def create_object(self, params): else: parent_object = None - # Validate metakeys and Karton assignment - analysis_id = params.get("karton_id") - - if params["metakeys"]: - uses_deprecated_api(DeprecatedFeature.legacy_metakeys_upload_option) - # If 'metakeys' are defined: keep legacy behavior - if "attributes" in params and params["attributes"]: - raise BadRequest("'attributes' and 'metakeys' options can't be mixed") - - attributes = params["metakeys"] - for attribute in params["metakeys"]: - key = attribute["key"] - if key == "karton": - if analysis_id is not None: - raise BadRequest( - "You can't provide more than one Karton analysis identifier" - ) - try: - analysis_id = UUID(attribute["value"]) - except (ValueError, AttributeError): - raise BadRequest("'karton' attribute accepts only UUID values") - elif not AttributeDefinition.query_for_set(key).first(): - raise NotFound( - f"Attribute '{key}' not defined or insufficient " - "permissions to set that one" - ) - else: - # If not, rely on 'attributes' - attributes = params["attributes"] - for attribute in params["attributes"]: - key = attribute["key"] - if not AttributeDefinition.query_for_set(key).first(): - raise NotFound( - f"Attribute '{key}' not defined or insufficient " - "permissions to set that one" - ) - - if analysis_id is not None: + # If not, rely on 'attributes' + attributes = params["attributes"] + for attribute in params["attributes"]: + key = attribute["key"] + if not AttributeDefinition.query_for_set(key).first(): + raise NotFound( + f"Attribute '{key}' not defined or insufficient " + "permissions to set that one" + ) + + # Validate Karton assignment + karton_id = params.get("karton_id") + if karton_id is not None: if not g.auth_user.has_rights(Capabilities.karton_assign): raise Forbidden( "You are not permitted to assign Karton analysis to object" @@ -130,7 +98,7 @@ def create_object(self, params): parent_object, share_with, attributes, - analysis_id, + karton_id, tags, share_3rd_party, ) @@ -216,9 +184,6 @@ def get(self): description: | Request canceled due to database statement timeout. """ - if "page" in request.args: - uses_deprecated_api(DeprecatedFeature.legacy_page_parameter) - obj = load_schema(request.args, ObjectListRequestSchema()) pivot_obj = None @@ -243,9 +208,6 @@ def get(self): ).order_by(Object.id.desc()) if pivot_obj: db_query = db_query.filter(Object.id < pivot_obj.id) - # Legacy parameter - to be removed in the future - elif obj["page"] is not None and obj["page"] > 1: - db_query = db_query.offset((obj["page"] - 1) * 10) objects = db_query.limit(limit).all() @@ -253,10 +215,9 @@ def get(self): return schema.dump(objects, many=True) -class ObjectItemResource(Resource, ObjectUploader): +class ObjectItemResource(Resource): ObjectType = Object ItemResponseSchema = ObjectItemResponseSchema - CreateRequestSchema = None def call_specialised_remove_hook(self, obj): pass @@ -298,41 +259,6 @@ def get(self, identifier): schema = self.ItemResponseSchema() return schema.dump(obj) - def _get_upload_args(self, parent_identifier): - """ - Transforms upload arguments mixed into various request fields - """ - if request.is_json: - # If request is application/json: all args are in JSON - args = json.loads(request.get_data(parse_form_data=True, as_text=True)) - else: - if "json" in request.form: - # If request is multipart/form-data: - # some args are in JSON and some are part of form - args = json.loads(request.form["json"]) - else: - args = {} - if request.form.get("metakeys"): - args["metakeys"] = request.form["metakeys"] - if request.form.get("upload_as"): - args["upload_as"] = request.form["upload_as"] - args["parent"] = parent_identifier if parent_identifier != "root" else None - return args - - @requires_authorization - def post(self, identifier): - if self.ObjectType is Object: - raise MethodNotAllowed() - - schema = self.CreateRequestSchema() - obj = load_schema(self._get_upload_args(identifier), schema) - - return self.create_object(obj) - - @requires_authorization - def put(self, identifier): - return self.post(identifier) - @requires_authorization @requires_capabilities(Capabilities.removing_objects) def delete(self, identifier): diff --git a/mwdb/resources/search.py b/mwdb/resources/search.py deleted file mode 100644 index f2ac657e2..000000000 --- a/mwdb/resources/search.py +++ /dev/null @@ -1,68 +0,0 @@ -from flask import g, request -from werkzeug.exceptions import BadRequest - -from mwdb.core.deprecated import DeprecatedFeature, deprecated_endpoint -from mwdb.core.search import QueryBaseException, build_query -from mwdb.core.service import Resource -from mwdb.model import Object -from mwdb.schema.object import ObjectListItemResponseSchema -from mwdb.schema.search import SearchRequestSchema - -from . import loads_schema, requires_authorization - - -class SearchResource(Resource): - @deprecated_endpoint(DeprecatedFeature.legacy_search) - @requires_authorization - def post(self): - """ - --- - summary: Search for objects (deprecated) - description: | - Returns objects found by Lucene query. - - Hard-limited to 10000 records. - Use `query` argument in object get methods instead. - - Deprecated: use /object?query= instead - deprecated: true - security: - - bearerAuth: [] - tags: - - object - requestBody: - description: Search query - content: - application/json: - schema: SearchRequestSchema - responses: - 200: - description: Resulting objects - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/ObjectListItemResponse' - 400: - description: When request body or query syntax is invalid - 503: - description: | - Request canceled due to database statement timeout. - """ - schema = SearchRequestSchema() - obj = loads_schema(request.get_data(as_text=True), schema) - - query = obj["query"] - try: - result = ( - build_query(query) - .filter(g.auth_user.has_access_to_object(Object.id)) - .order_by(Object.id.desc()) - .limit(10000) - ).all() - except QueryBaseException as e: - raise BadRequest(str(e)) - - schema = ObjectListItemResponseSchema(many=True) - return schema.dump(result) diff --git a/mwdb/schema/blob.py b/mwdb/schema/blob.py index 578847641..356cb3fda 100644 --- a/mwdb/schema/blob.py +++ b/mwdb/schema/blob.py @@ -4,7 +4,6 @@ from .object import ( ObjectCreateRequestSchemaBase, ObjectItemResponseSchema, - ObjectLegacyMetakeysMixin, ObjectListItemResponseSchema, ObjectListResponseSchemaBase, ) @@ -22,10 +21,6 @@ class BlobCreateRequestSchema(ObjectCreateRequestSchemaBase, BlobCreateSpecSchem pass -class BlobLegacyCreateRequestSchema(BlobCreateRequestSchema, ObjectLegacyMetakeysMixin): - pass - - class BlobListItemResponseSchema(ObjectListItemResponseSchema): blob_name = fields.Str(required=True, allow_none=False) blob_size = fields.Int(required=True, allow_none=False) diff --git a/mwdb/schema/config.py b/mwdb/schema/config.py index 06a14414e..51faa6d0b 100644 --- a/mwdb/schema/config.py +++ b/mwdb/schema/config.py @@ -3,7 +3,6 @@ from .object import ( ObjectCreateRequestSchemaBase, ObjectItemResponseSchema, - ObjectLegacyMetakeysMixin, ObjectListItemResponseSchema, ObjectListResponseSchemaBase, ) @@ -24,12 +23,6 @@ class ConfigCreateRequestSchema(ObjectCreateRequestSchemaBase, ConfigCreateSpecS pass -class ConfigLegacyCreateRequestSchema( - ConfigCreateRequestSchema, ObjectLegacyMetakeysMixin -): - pass - - class ConfigListItemResponseSchema(ObjectListItemResponseSchema): family = fields.Str(required=True, allow_none=False) config_type = fields.Str(required=True, allow_none=False) diff --git a/mwdb/schema/download.py b/mwdb/schema/download.py deleted file mode 100644 index f0e4019d9..000000000 --- a/mwdb/schema/download.py +++ /dev/null @@ -1,5 +0,0 @@ -from marshmallow import Schema, fields - - -class DownloadURLResponseSchema(Schema): - url = fields.Str(required=True, allow_none=False) diff --git a/mwdb/schema/file.py b/mwdb/schema/file.py index c11c53415..428a4ebaa 100644 --- a/mwdb/schema/file.py +++ b/mwdb/schema/file.py @@ -6,7 +6,6 @@ from .object import ( ObjectCreateRequestSchemaBase, ObjectItemResponseSchema, - ObjectLegacyMetakeysMixin, ObjectListItemResponseSchema, ObjectListResponseSchemaBase, ) @@ -33,12 +32,6 @@ def unpack_options(self, params, **kwargs): return params -class FileLegacyCreateRequestSchema( - ObjectCreateRequestSchemaBase, ObjectLegacyMetakeysMixin -): - pass - - class FileListItemResponseSchema(ObjectListItemResponseSchema): file_name = fields.Str(required=True, allow_none=False) file_size = fields.Int(required=True, allow_none=False) diff --git a/mwdb/schema/metakey.py b/mwdb/schema/metakey.py deleted file mode 100644 index c078ec056..000000000 --- a/mwdb/schema/metakey.py +++ /dev/null @@ -1,109 +0,0 @@ -import re - -from marshmallow import Schema, ValidationError, fields, pre_load, validates - - -class MetakeyKeySchema(Schema): - key = fields.Str(required=True, allow_none=False) - - @pre_load - def sanitize_key(self, params, **kwargs): - params = dict(params) - if params.get("key"): - params["key"] = params["key"].lower().strip() - return params - - @validates("key") - def validate_key(self, value): - if not re.match("^[A-Za-z0-9_-]{1,32}$", value): - raise ValidationError( - "Key should contain max 32 chars and include only letters, " - "digits, underscores and dashes" - ) - - -class MetakeyValueSchema(Schema): - value = fields.Str(required=True, allow_none=False) - - @validates("value") - def validate_value(self, value): - if not value: - raise ValidationError("Value shouldn't be empty") - - -class MetakeyListRequestSchema(Schema): - hidden = fields.Boolean(missing=False) - - -class MetakeyItemRequestSchema(MetakeyKeySchema, MetakeyValueSchema): - pass - - -class MetakeyItemRemoveRequestSchema(MetakeyKeySchema): - value = fields.Str(missing=None) - - -class MetakeyDefinitionItemRequestArgsSchema(MetakeyKeySchema): - pass - - -class MetakeyUpdateRequestSchema(Schema): - label = fields.Str(missing=None) - description = fields.Str(missing=None) - template = fields.Str(missing=None) - hidden = fields.Boolean(missing=None) - - -class MetakeyDefinitionItemRequestBodySchema(Schema): - template = fields.Str(attribute="url_template", required=True, allow_none=False) - label = fields.Str(required=True, allow_none=False) - description = fields.Str(required=True, allow_none=False) - hidden = fields.Boolean(required=True, allow_none=False) - - -class MetakeyPermissionSetRequestArgsSchema(MetakeyKeySchema): - group_name = fields.Str(required=True, allow_none=False) - - -class MetakeyPermissionSetRequestBodySchema(Schema): - can_read = fields.Boolean(required=True, allow_none=False) - can_set = fields.Boolean(required=True, allow_none=False) - - -class MetakeyItemResponseSchema(MetakeyKeySchema, MetakeyValueSchema): - url = fields.Str(required=True) - label = fields.Str(required=True) - description = fields.Str(required=True) - - -class MetakeyPermissionItemResponseSchema(Schema): - group_name = fields.Str(required=True, allow_none=False) - can_read = fields.Boolean(required=True, allow_none=False) - can_set = fields.Boolean(required=True, allow_none=False) - - -class MetakeyDefinitionItemResponseSchema(MetakeyKeySchema): - template = fields.Str(attribute="url_template", required=True, allow_none=False) - label = fields.Str(required=True, allow_none=False) - description = fields.Str(required=True, allow_none=False) - hidden = fields.Boolean(required=True, allow_none=False) - - -class MetakeyDefinitionManageItemResponseSchema(MetakeyKeySchema): - template = fields.Str(attribute="url_template", required=True, allow_none=False) - label = fields.Str(required=True, allow_none=False) - description = fields.Str(required=True, allow_none=False) - hidden = fields.Boolean(required=True, allow_none=False) - permissions = fields.Nested(MetakeyPermissionItemResponseSchema, many=True) - - -class MetakeyListResponseSchema(Schema): - metakeys = fields.Nested(MetakeyItemResponseSchema, many=True) - - -class MetakeyDefinitionListResponseSchema(Schema): - metakeys = fields.Nested(MetakeyDefinitionItemResponseSchema, many=True) - - -class MetakeyDefinitionManageListResponseSchema(Schema): - metakeys = fields.Nested(MetakeyDefinitionManageItemResponseSchema, many=True) diff --git a/mwdb/schema/object.py b/mwdb/schema/object.py index 35356224f..199c4da74 100644 --- a/mwdb/schema/object.py +++ b/mwdb/schema/object.py @@ -1,34 +1,15 @@ -import json - -from marshmallow import ( - Schema, - ValidationError, - fields, - post_dump, - pre_load, - validates_schema, -) +from marshmallow import Schema, fields, post_dump from .attribute import AttributeItemRequestSchema, AttributeItemResponseSchema -from .metakey import MetakeyItemRequestSchema from .tag import TagItemResponseSchema, TagRequestSchema from .utils import UTCDateTime class ObjectListRequestSchema(Schema): - page = fields.Int(missing=None) # legacy, to be removed in future query = fields.Str(missing=None) older_than = fields.Str(missing=None) count = fields.Int(missing=10) - @validates_schema - def validate_key(self, data, **kwargs): - if data["page"] is not None and data["older_than"] is not None: - raise ValidationError( - "'page' and 'older_than' can't be used simultaneously. " - "Use 'older_than' for new code." - ) - class ObjectCountRequestSchema(Schema): query = fields.Str(missing=None) @@ -36,7 +17,6 @@ class ObjectCountRequestSchema(Schema): class ObjectCreateRequestSchemaBase(Schema): parent = fields.Str(missing=None) - metakeys = fields.Nested(MetakeyItemRequestSchema, many=True, missing=[]) attributes = fields.Nested(AttributeItemRequestSchema, many=True, missing=[]) upload_as = fields.Str(missing="*", allow_none=False) karton_id = fields.UUID(missing=None) @@ -45,30 +25,6 @@ class ObjectCreateRequestSchemaBase(Schema): share_3rd_party = fields.Boolean(missing=True) -class ObjectLegacyMetakeysMixin(Schema): - @pre_load - def unpack_metakeys(self, params, **kwargs): - """ - Metakeys are packed into JSON string that need to be deserialized first. - Empty string in 'metakeys' field is treated like missing key. - Request providing metakeys looks like this: - `curl ... -F="metakeys='{"metakeys": [...]}'"` - """ - params = dict(params) - if "metakeys" in params: - if params["metakeys"]: - metakeys_json = json.loads(params["metakeys"]) - if "metakeys" not in metakeys_json: - raise ValidationError( - "Object provided to 'metakeys' field " - "must contain 'metakeys' key" - ) - params["metakeys"] = metakeys_json["metakeys"] - else: - del params["metakeys"] - return params - - class ObjectListItemResponseSchema(Schema): id = fields.Str(attribute="dhash", required=True, allow_none=False) type = fields.Str(required=True, allow_none=False) diff --git a/mwdb/schema/search.py b/mwdb/schema/search.py deleted file mode 100644 index 1aa2be265..000000000 --- a/mwdb/schema/search.py +++ /dev/null @@ -1,10 +0,0 @@ -from marshmallow import Schema, ValidationError, fields, validates - - -class SearchRequestSchema(Schema): - query = fields.Str(required=True, allow_none=False) - - @validates("query") - def validate_query(self, value): - if not value: - raise ValidationError("Query can't be empty") diff --git a/mwdb/templates/mwdb.ini.tmpl b/mwdb/templates/mwdb.ini.tmpl index adc117ca5..6ea01cc07 100644 --- a/mwdb/templates/mwdb.ini.tmpl +++ b/mwdb/templates/mwdb.ini.tmpl @@ -187,18 +187,7 @@ base_url = {{ base_url }} # kartonobject_post = # kartonanalysis_get = # kartonanalysis_put = -# metakey_get = -# metakey_post = -# metakey_delete = -# metakeylistdefinition_get = -# metakeylistdefinitionmanage_get = -# metakeydefinitionmanage_get = -# metakeydefinitionmanage_post = -# metakeydefinitionmanage_put = -# metakeydefinitionmanage_delete = -# metakeypermission_put = -# metakeypermission_delete = -# openidprovider_post = +# openidprovider_post = # openidsingleprovider_get = # openidsingleprovider_put = # openidsingleprovider_delete = diff --git a/mwdb/web/src/commons/auth/capabilities.tsx b/mwdb/web/src/commons/auth/capabilities.tsx index 95d2c555a..f8d2335eb 100644 --- a/mwdb/web/src/commons/auth/capabilities.tsx +++ b/mwdb/web/src/commons/auth/capabilities.tsx @@ -4,7 +4,6 @@ import { Capability } from "@mwdb-web/types/types"; export let capabilitiesList: Record = { [Capability.manageUsers]: "Managing users and groups (system administration)", - [Capability.shareQueriedObjects]: "Query for all objects in system", [Capability.accessAllObjects]: "Has access to all new uploaded objects into system", [Capability.sharingWithAll]: "Can share objects with all groups in system", diff --git a/mwdb/web/src/types/types.ts b/mwdb/web/src/types/types.ts index 9df98c1d2..fd5ac6890 100644 --- a/mwdb/web/src/types/types.ts +++ b/mwdb/web/src/types/types.ts @@ -2,7 +2,6 @@ import { AxiosError } from "axios"; export enum Capability { manageUsers = "manage_users", - shareQueriedObjects = "share_queried_objects", accessAllObjects = "access_all_objects", sharingWithAll = "sharing_with_all", accessUploaderInfo = "access_uploader_info", diff --git a/tests/backend/test_api.py b/tests/backend/test_api.py index e4afea1c3..b4d673baa 100644 --- a/tests/backend/test_api.py +++ b/tests/backend/test_api.py @@ -25,20 +25,6 @@ def test_add_sample(admin_session): parse(res['upload_time']) -def test_add_sample_legacy(admin_session): - filename = rand_string() - file_content = rand_string() - - res = admin_session.add_sample_legacy(filename, file_content) - - assert res['file_name'] == filename - assert res['file_size'] == len(file_content) - assert res['parents'] == [] - assert res['children'] == [] - assert res['tags'] == [] - parse(res['upload_time']) - - def test_get_sample(admin_session): filename = rand_string() file_content = rand_string() diff --git a/tests/backend/test_auth.py b/tests/backend/test_auth.py index 6375fb42b..4a4a8ddc6 100644 --- a/tests/backend/test_auth.py +++ b/tests/backend/test_auth.py @@ -120,20 +120,14 @@ def test_jwt_legacy_api_keys(admin_session): secret_key = "e2e-testing-key" api_key_id = admin_session.api_key_create("admin", "testing-key").json()["id"] - pre2_0_payload = {"login": "admin", "version_uid": None} pre2_7_payload = {"login": "admin", "api_key_id": api_key_id} - pre2_0_token = jwt.encode(pre2_0_payload, secret_key, algorithm="HS512") pre2_7_token = jwt.encode(pre2_7_payload, secret_key, algorithm="HS512") session = MwdbTest() response = session.request("get", "/server") assert not response["is_authenticated"] - session.set_auth_token(pre2_0_token) - response = session.request("get", "/server") - assert response["is_authenticated"] - session.set_auth_token(pre2_7_token) response = session.request("get", "/server") assert response["is_authenticated"] diff --git a/tests/backend/test_permissions.py b/tests/backend/test_permissions.py index 5ddfbc4ae..adc94425a 100644 --- a/tests/backend/test_permissions.py +++ b/tests/backend/test_permissions.py @@ -30,23 +30,6 @@ def request(*args, **kwargs): request("PUT", "/group/{}".format(group_name), json={"capabilities": []}) -def test_share_queried_objects(admin_session): - testCase = RelationTestCase(admin_session) - - Alice = testCase.new_user("Alice") - Bob = testCase.new_user("Bob", capabilities=["share_queried_objects"]) - - Sample = testCase.new_sample("Sample") - - with ShouldRaise(status_code=404): - Alice.session.get_sample(Sample.dhash) - - Bob.session.get_sample(Sample.dhash) - - Sample.should_not_access(Alice) - Sample.should_access(Bob) - - def test_access_all_objects(admin_session): testCase = RelationTestCase(admin_session) diff --git a/tests/backend/utils.py b/tests/backend/utils.py index e142255ca..36b3c697a 100644 --- a/tests/backend/utils.py +++ b/tests/backend/utils.py @@ -181,31 +181,6 @@ def remove_user(self, login): res.raise_for_status() return res - def add_sample_legacy( - self, filename=None, content=None, parent=None, metakeys=None, upload_as=None - ): - parent = parent or "root" - - if filename is None: - filename = str(uuid.uuid4()) - - if content is None: - content = str(uuid.uuid4()) - - params = {} - if metakeys: - params["metakeys"] = json.dumps(metakeys) - if upload_as: - params["upload_as"] = upload_as - - res = self.session.post( - self.mwdb_url + "/file/" + parent, - files={"file": (filename, content)}, - data=params, - ) - res.raise_for_status() - return res.json() - def add_sample( self, filename=None, @@ -464,9 +439,9 @@ def recent_blobs(self, page): return res.json() def search(self, query): - res = self.session.post(self.mwdb_url + "/search", json={"query": query}) + res = self.session.get(self.mwdb_url + "/object", params={"query": query}) res.raise_for_status() - return res.json() + return res.json()["objects"] def check_operational(self): for attempt in range(10):