From ed3dbe8f9d711a9894b51977b3ad11f4ec89f374 Mon Sep 17 00:00:00 2001 From: Kirill Lakhov Date: Fri, 26 May 2023 22:01:38 +0300 Subject: [PATCH] Open resource links from any organization/sandbox (#5892) --- CHANGELOG.md | 1 + cvat-core/src/api-implementation.ts | 10 +- cvat-core/src/api.ts | 6 + cvat-core/src/common.ts | 4 + cvat-core/src/config.ts | 6 +- cvat-core/src/organization.ts | 5 +- cvat-core/src/server-proxy.ts | 19 +- cvat-ui/src/components/cvat-app.tsx | 3 +- .../src/components/task-page/task-page.tsx | 2 +- .../watchers/organization-watcher.tsx | 28 + cvat/apps/engine/models.py | 16 +- cvat/apps/engine/schema.py | 24 + cvat/apps/engine/serializers.py | 3 +- cvat/apps/engine/view_utils.py | 2 +- cvat/apps/engine/views.py | 9 + cvat/apps/events/handlers.py | 5 +- cvat/apps/events/serializers.py | 2 +- cvat/apps/events/views.py | 2 + cvat/apps/iam/filters.py | 66 +- cvat/apps/iam/permissions.py | 226 +-- cvat/apps/iam/schema.py | 60 +- cvat/apps/iam/views.py | 29 +- cvat/apps/lambda_manager/views.py | 8 +- cvat/apps/organizations/models.py | 3 +- cvat/apps/organizations/views.py | 13 +- cvat/apps/webhooks/views.py | 20 +- cvat/schema.yml | 1544 ++--------------- cvat/settings/base.py | 1 - .../case_113_new_organization_pipeline.js | 10 +- .../e2e/actions_tasks/task_rectangles_only.js | 2 +- .../e2e/skeletons/skeletons_pipeline.js | 2 +- tests/python/rest_api/test_cloud_storages.py | 45 +- tests/python/rest_api/test_issues.py | 4 +- tests/python/rest_api/test_jobs.py | 242 ++- tests/python/rest_api/test_labels.py | 37 +- tests/python/rest_api/test_projects.py | 10 +- .../rest_api/test_resource_import_export.py | 7 +- tests/python/rest_api/test_tasks.py | 23 +- tests/python/rest_api/utils.py | 2 +- tests/python/sdk/test_client.py | 52 +- tests/python/shared/assets/cvat_db/data.json | 26 + tests/python/shared/assets/jobs.json | 19 + 42 files changed, 738 insertions(+), 1860 deletions(-) create mode 100644 cvat-ui/src/components/watchers/organization-watcher.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 14e2deb1aee..b2496720df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ without use_cache option () - Support task creation with cloud storage data and without use_cache option () ### Changed +- Resource links are opened from any organization/sandbox if available for user () - Cloud storage manifest file is optional () - Updated Django to 4.2.x version () - Some Nuclio functions' names were changed to follow a common convention: diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 4c9ed843737..421c7ed5287 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -311,11 +311,17 @@ export default function implementAPI(cvat) { cvat.organizations.activate.implementation = (organization) => { checkObjectType('organization', organization, null, Organization); - config.organizationID = organization.slug; + config.organization = { + organizationID: organization.id, + organizationSlug: organization.slug, + }; }; cvat.organizations.deactivate.implementation = async () => { - config.organizationID = null; + config.organization = { + organizationID: null, + organizationSlug: null, + }; }; cvat.webhooks.get.implementation = async (filter) => { diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index b899b3c96e8..786598d48c5 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -225,6 +225,12 @@ function build() { set removeUnderlyingMaskPixels(value: boolean) { config.removeUnderlyingMaskPixels = value; }, + get onOrganizationChange(): (orgId: number) => void { + return config.onOrganizationChange; + }, + set onOrganizationChange(value: (orgId: number) => void) { + config.onOrganizationChange = value; + }, }, client: { version: `${pjson.version}`, diff --git a/cvat-core/src/common.ts b/cvat-core/src/common.ts index 1aa67589cfd..95077eb992b 100644 --- a/cvat-core/src/common.ts +++ b/cvat-core/src/common.ts @@ -114,3 +114,7 @@ export class FieldUpdateTrigger { export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } + +export function isResourceURL(url: string): boolean { + return /\/([0-9]+)$/.test(url); +} diff --git a/cvat-core/src/config.ts b/cvat-core/src/config.ts index 063c693a890..3de103e731c 100644 --- a/cvat-core/src/config.ts +++ b/cvat-core/src/config.ts @@ -5,10 +5,14 @@ const config = { backendAPI: '/api', - organizationID: null, + organization: { + organizationID: null, + organizationSlug: null, + }, origin: '', uploadChunkSize: 100, removeUnderlyingMaskPixels: false, + onOrganizationChange: null, }; export default config; diff --git a/cvat-core/src/organization.ts b/cvat-core/src/organization.ts index 84856bf096e..166fa790d58 100644 --- a/cvat-core/src/organization.ts +++ b/cvat-core/src/organization.ts @@ -282,7 +282,10 @@ Object.defineProperties(Organization.prototype.remove, { value: async function implementation() { if (typeof this.id === 'number') { await serverProxy.organizations.delete(this.id); - config.organizationID = null; + config.organization = { + organizationID: null, + organizationSlug: null, + }; } }, }, diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 0410f630898..1e7d4d205a2 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -15,7 +15,7 @@ import { } from 'server-response-types'; import { Storage } from './storage'; import { StorageLocation, WebhookSourceType } from './enums'; -import { isEmail } from './common'; +import { isEmail, isResourceURL } from './common'; import config from './config'; import DownloadWorker from './download.worker'; import { ServerError } from './exceptions'; @@ -32,7 +32,7 @@ type Params = { }; function enableOrganization(): { org: string } { - return { org: config.organizationID || '' }; + return { org: config.organization.organizationSlug || '' }; } function configureStorage(storage: Storage, useDefaultLocation = false): Partial { @@ -266,10 +266,25 @@ Axios.interceptors.request.use((reqConfig) => { return reqConfig; } + if (isResourceURL(reqConfig.url)) { + return reqConfig; + } + reqConfig.params = { ...organization, ...(reqConfig.params || {}) }; return reqConfig; }); +Axios.interceptors.response.use((response) => { + if (isResourceURL(response.config.url)) { + const newOrg = response.data.organization; + if (newOrg && config.organization.organizationID !== newOrg) { + config?.onOrganizationChange(newOrg); + } + } + + return response; +}); + let token = store.get('token'); if (token) { Axios.defaults.headers.common.Authorization = `Token ${token}`; diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 4ca5a6a331f..0435afd7dd6 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -73,6 +73,7 @@ import EmailConfirmationPage from './email-confirmation-pages/email-confirmed'; import EmailVerificationSentPage from './email-confirmation-pages/email-verification-sent'; import IncorrectEmailConfirmationPage from './email-confirmation-pages/incorrect-email-confirmation'; import CreateModelPage from './create-model-page/create-model-page'; +import OrganizationWatcher from './watchers/organization-watcher'; interface CVATAppProps { loadFormats: () => void; @@ -499,7 +500,6 @@ class CVATApplication extends React.PureComponent - {/* eslint-disable-next-line */} @@ -507,6 +507,7 @@ class CVATApplication extends React.PureComponent ( ))} + {/* eslint-disable-next-line */} diff --git a/cvat-ui/src/components/task-page/task-page.tsx b/cvat-ui/src/components/task-page/task-page.tsx index 752029dc0ef..cd3d9bacb51 100644 --- a/cvat-ui/src/components/task-page/task-page.tsx +++ b/cvat-ui/src/components/task-page/task-page.tsx @@ -44,7 +44,7 @@ function TaskPageComponent(): JSX.Element { }).catch((error: Error) => { if (mounted.current) { notification.error({ - message: 'Could not receive the requested project from the server', + message: 'Could not receive the requested task from the server', description: error.toString(), }); } diff --git a/cvat-ui/src/components/watchers/organization-watcher.tsx b/cvat-ui/src/components/watchers/organization-watcher.tsx new file mode 100644 index 00000000000..8f5717cc552 --- /dev/null +++ b/cvat-ui/src/components/watchers/organization-watcher.tsx @@ -0,0 +1,28 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { getCore } from 'cvat-core-wrapper'; +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { CombinedState } from 'reducers'; + +const core = getCore(); + +function OrganizationWatcher(): JSX.Element { + const organizationList = useSelector((state: CombinedState) => state.organizations.list); + + useEffect(() => { + core.config.onOrganizationChange = (newOrgId: number) => { + const newOrganization = organizationList.find((org) => org.id === newOrgId); + if (newOrganization) { + localStorage.setItem('currentOrganization', newOrganization.slug); + window.location.reload(); + } + }; + }, []); + + return <>; +} + +export default React.memo(OrganizationWatcher); diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 3ac4106624f..c98dd9d32c1 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -493,7 +493,8 @@ def get_task_id(self): task = self.segment.task return task.id if task else None - def get_organization_id(self): + @property + def organization_id(self): return self.segment.task.organization_id def get_organization_slug(self): @@ -542,7 +543,8 @@ def create(cls, **kwargs): except IntegrityError: raise InvalidLabel("All label names must be unique") - def get_organization_id(self): + @property + def organization_id(self): if self.project is not None: return self.project.organization.id if self.task is not None: @@ -720,8 +722,9 @@ class Issue(models.Model): def get_project_id(self): return self.job.get_project_id() - def get_organization_id(self): - return self.job.get_organization_id() + @property + def organization_id(self): + return self.job.organization_id def get_organization_slug(self): return self.job.get_organization_slug() @@ -743,8 +746,9 @@ class Comment(models.Model): def get_project_id(self): return self.issue.get_project_id() - def get_organization_id(self): - return self.issue.get_organization_id() + @property + def organization_id(self): + return self.issue.organization_id def get_organization_slug(self): return self.issue.get_organization_slug() diff --git a/cvat/apps/engine/schema.py b/cvat/apps/engine/schema.py index 07a3e117a0a..060aa674ffa 100644 --- a/cvat/apps/engine/schema.py +++ b/cvat/apps/engine/schema.py @@ -5,6 +5,7 @@ import textwrap from typing import Type from rest_framework import serializers +from drf_spectacular.utils import OpenApiParameter from drf_spectacular.extensions import OpenApiSerializerExtension from drf_spectacular.plumbing import force_instance, build_basic_type from drf_spectacular.types import OpenApiTypes @@ -228,4 +229,27 @@ class CloudStorageReadSerializerExtension(_CloudStorageSerializerExtension): class CloudStorageWriteSerializerExtension(_CloudStorageSerializerExtension): target_class = 'cvat.apps.engine.serializers.CloudStorageWriteSerializer' +ORGANIZATION_OPEN_API_PARAMETERS = [ + OpenApiParameter( + name='org', + type=str, + required=False, + location=OpenApiParameter.QUERY, + description="Organization unique slug", + ), + OpenApiParameter( + name='org_id', + type=int, + required=False, + location=OpenApiParameter.QUERY, + description="Organization identifier", + ), + OpenApiParameter( + name='X-Organization', + type=str, + required=False, + location=OpenApiParameter.HEADER + ), +] + __all__ = [] # No public symbols here diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 5cc1a293b31..3fefec17b43 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -549,6 +549,7 @@ class JobReadSerializer(serializers.ModelSerializer): assignee = BasicUserSerializer(allow_null=True, read_only=True) dimension = serializers.CharField(max_length=2, source='segment.task.dimension', read_only=True) data_chunk_size = serializers.ReadOnlyField(source='segment.task.data.chunk_size') + organization = serializers.ReadOnlyField(source='segment.task.organization.id', allow_null=True) data_compressed_chunk_type = serializers.ReadOnlyField(source='segment.task.data.compressed_chunk_type') mode = serializers.ReadOnlyField(source='segment.task.mode') bug_tracker = serializers.CharField(max_length=2000, source='get_bug_tracker', @@ -560,7 +561,7 @@ class Meta: model = models.Job fields = ('url', 'id', 'task_id', 'project_id', 'assignee', 'dimension', 'bug_tracker', 'status', 'stage', 'state', 'mode', - 'start_frame', 'stop_frame', 'data_chunk_size', 'data_compressed_chunk_type', + 'start_frame', 'stop_frame', 'data_chunk_size', 'organization', 'data_compressed_chunk_type', 'updated_date', 'issues', 'labels' ) read_only_fields = fields diff --git a/cvat/apps/engine/view_utils.py b/cvat/apps/engine/view_utils.py index 457f07b5465..56c040dfdd8 100644 --- a/cvat/apps/engine/view_utils.py +++ b/cvat/apps/engine/view_utils.py @@ -79,7 +79,7 @@ def list_action(serializer_class: Type[Serializer], **kwargs): def get_cloud_storage_for_import_or_export( storage_id: int, *, request, is_default: bool = False ) -> CloudStorageModel: - perm = CloudStoragePermission.create_scope_view(request=request, storage_id=storage_id) + perm = CloudStoragePermission.create_scope_view(None, storage_id=storage_id, request=request) result = perm.check_access() if not result.allow: if is_default: diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 22528572a54..950857db75c 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -64,6 +64,7 @@ CloudStorageReadSerializer, DatasetFileSerializer, ProjectFileSerializer, TaskFileSerializer, CloudStorageContentSerializer) from cvat.apps.engine.view_utils import get_cloud_storage_for_import_or_export +from cvat.apps.engine.schema import ORGANIZATION_OPEN_API_PARAMETERS from utils.dataset_manifest import ImageManifestManager from cvat.apps.engine.utils import ( @@ -204,6 +205,7 @@ def plugins(request): create=extend_schema( summary='Method creates a new project', request=ProjectWriteSerializer, + parameters=ORGANIZATION_OPEN_API_PARAMETERS, responses={ '201': ProjectReadSerializer, # check ProjectWriteSerializer.to_representation }), @@ -484,6 +486,7 @@ def export_backup(self, request, pk=None): @extend_schema(summary='Methods create a project from a backup', parameters=[ + *ORGANIZATION_OPEN_API_PARAMETERS, OpenApiParameter('location', description='Where to import the backup file from', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, enum=Location.list(), default=Location.LOCAL), @@ -637,6 +640,7 @@ def __call__(self, request, start, stop, db_data): create=extend_schema( summary='Method creates a new task in a database without any attached images and videos', request=TaskWriteSerializer, + parameters=ORGANIZATION_OPEN_API_PARAMETERS, responses={ '201': TaskReadSerializer, # check TaskWriteSerializer.to_representation }), @@ -715,6 +719,7 @@ def get_queryset(self): @extend_schema(summary='Method recreates a task from an attached task backup file', parameters=[ + *ORGANIZATION_OPEN_API_PARAMETERS, OpenApiParameter('location', description='Where to import the backup file from', location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=False, enum=Location.list(), default=Location.LOCAL), @@ -1648,6 +1653,7 @@ def preview(self, request, pk): create=extend_schema( summary='Method creates an issue', request=IssueWriteSerializer, + parameters=ORGANIZATION_OPEN_API_PARAMETERS, responses={ '201': IssueReadSerializer, # check IssueWriteSerializer.to_representation }), @@ -1718,6 +1724,7 @@ def perform_create(self, serializer, **kwargs): create=extend_schema( summary='Method creates a comment', request=CommentWriteSerializer, + parameters=ORGANIZATION_OPEN_API_PARAMETERS, responses={ '201': CommentReadSerializer, # check CommentWriteSerializer.to_representation }), @@ -1784,6 +1791,7 @@ def perform_create(self, serializer, **kwargs): description='A simple equality filter for task id'), OpenApiParameter('project_id', type=OpenApiTypes.INT, description='A simple equality filter for project id'), + *ORGANIZATION_OPEN_API_PARAMETERS ], responses={ '200': LabelSerializer(many=True), @@ -2032,6 +2040,7 @@ def self(self, request): create=extend_schema( summary='Method creates a cloud storage with a specified characteristics', request=CloudStorageWriteSerializer, + parameters=ORGANIZATION_OPEN_API_PARAMETERS, responses={ '201': CloudStorageReadSerializer, # check CloudStorageWriteSerializer.to_representation }) diff --git a/cvat/apps/events/handlers.py b/cvat/apps/events/handlers.py index 819e17942ed..bdfc460d809 100644 --- a/cvat/apps/events/handlers.py +++ b/cvat/apps/events/handlers.py @@ -58,10 +58,7 @@ def organization_id(instance): return instance.id try: - oid = getattr(instance, "organization_id", None) - if oid is None: - return instance.get_organization_id() - return oid + return getattr(instance, "organization_id", None) except Exception: return None diff --git a/cvat/apps/events/serializers.py b/cvat/apps/events/serializers.py index b6750c3a930..283d6ef7478 100644 --- a/cvat/apps/events/serializers.py +++ b/cvat/apps/events/serializers.py @@ -35,7 +35,7 @@ class ClientEventsSerializer(serializers.Serializer): def to_internal_value(self, data): request = self.context.get("request") - org = request.iam_context['organization'] + org = request.iam_context["organization"] org_id = getattr(org, "id", None) org_slug = getattr(org, "slug", None) diff --git a/cvat/apps/events/views.py b/cvat/apps/events/views.py index 728db926e3a..1941bc5c439 100644 --- a/cvat/apps/events/views.py +++ b/cvat/apps/events/views.py @@ -13,6 +13,7 @@ from cvat.apps.iam.permissions import EventsPermission from cvat.apps.events.serializers import ClientEventsSerializer from cvat.apps.engine.log import vlogger +from cvat.apps.engine.schema import ORGANIZATION_OPEN_API_PARAMETERS from .export import export class EventsViewSet(viewsets.ViewSet): @@ -21,6 +22,7 @@ class EventsViewSet(viewsets.ViewSet): @extend_schema(summary='Method saves logs from a client on the server', methods=['POST'], description='Sends logs to the Clickhouse if it is connected', + parameters=ORGANIZATION_OPEN_API_PARAMETERS, request=ClientEventsSerializer(), responses={ '201': ClientEventsSerializer(), diff --git a/cvat/apps/iam/filters.py b/cvat/apps/iam/filters.py index 660e8337052..4fd6817dccd 100644 --- a/cvat/apps/iam/filters.py +++ b/cvat/apps/iam/filters.py @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: MIT -import coreapi from rest_framework.filters import BaseFilterBackend class OrganizationFilterBackend(BaseFilterBackend): @@ -10,24 +9,55 @@ class OrganizationFilterBackend(BaseFilterBackend): organization_slug_description = 'Organization unique slug' organization_id = 'org_id' organization_id_description = 'Organization identifier' - - def get_schema_fields(self, view): - return [ - # NOTE: in coreapi.Field 'type', 'description' and 'example' are now deprecated, in favor of 'schema'. - coreapi.Field(name=self.organization_slug, location='query', required=False, - type='string', description=self.organization_slug_description), - coreapi.Field(name=self.organization_id, location='query', required=False, - type='string', description=self.organization_id_description), - ] + organization_slug_header = 'X-Organization' def filter_queryset(self, request, queryset, view): - # Rego rules should filter objects correctly (see filter rule). The - # filter isn't necessary but it is an extra check that we show only - # objects inside an organization if the request in context of the - # organization. - visibility = request.iam_context['visibility'] - if visibility and view.iam_organization_field: + # Filter works only for "list" requests and allows to return + # only non-organization objects if org isn't specified + + if view.detail or not view.iam_organization_field: + return queryset + + visibility = None + org = request.iam_context['organization'] + + if org: + visibility = {'organization': org.id} + + elif not org and ( + self.organization_slug in request.query_params + or self.organization_id in request.query_params + or self.organization_slug_header in request.headers + ): + visibility = {'organization': None} + + if visibility: visibility[view.iam_organization_field] = visibility.pop('organization') return queryset.filter(**visibility).distinct() - else: - return queryset + + return queryset + + def get_schema_operation_parameters(self, view): + if not view.iam_organization_field or view.detail: + return [] + + return [ + { + 'name': self.organization_slug, + 'in': 'query', + 'description': self.organization_slug_description, + 'schema': {'type': 'string'}, + }, + { + 'name': self.organization_id, + 'in': 'query', + 'description': self.organization_id_description, + 'schema': {'type': 'integer'}, + }, + { + 'name': self.organization_slug_header, + 'in': 'header', + 'description': self.organization_slug_description, + 'schema': {'type': 'string'}, + }, + ] diff --git a/cvat/apps/iam/permissions.py b/cvat/apps/iam/permissions.py index 5e336a344c5..da32c385e4b 100644 --- a/cvat/apps/iam/permissions.py +++ b/cvat/apps/iam/permissions.py @@ -50,6 +50,48 @@ class PermissionResult: allow: bool reasons: List[str] = field(factory=list) +def get_organization(request, obj): + # Try to get organization from an object otherwise, return the organization that is specified in query parameters + if obj is not None and isinstance(obj, Organization): + return obj + + if obj: + if organization_id := getattr(obj, "organization_id", None): + try: + return Organization.objects.get(id=organization_id) + except Organization.DoesNotExist: + return None + return None + + return request.iam_context["organization"] + +def get_membership(request, organization): + if organization is None: + return None + + return Membership.objects.filter( + organization=organization, + user=request.user, + is_active=True + ).first() + +def get_iam_context(request, obj): + organization = get_organization(request, obj) + membership = get_membership(request, organization) + + if organization and not request.user.is_superuser and membership is None: + raise PermissionDenied({"message": "You should be an active member in the organization"}) + + return { + 'user_id': request.user.id, + 'group_name': request.iam_context['privilege'], + 'org_id': getattr(organization, 'id', None), + 'org_owner_id': getattr(organization.owner, 'id', None) + if organization else None, + 'org_role': getattr(membership, 'role', None), + } + + class OpenPolicyAgentPermission(metaclass=ABCMeta): url: str user_id: int @@ -62,34 +104,21 @@ class OpenPolicyAgentPermission(metaclass=ABCMeta): @classmethod @abstractmethod - def create(cls, request, view, obj) -> Sequence[OpenPolicyAgentPermission]: + def create(cls, request, view, obj, iam_context) -> Sequence[OpenPolicyAgentPermission]: ... @classmethod - def create_base_perm(cls, request, view, scope, obj=None, **kwargs): + def create_base_perm(cls, request, view, scope, iam_context, obj=None, **kwargs): return cls( scope=scope, obj=obj, - **cls.unpack_context(request), **kwargs) + **iam_context, **kwargs) @classmethod - def create_scope_list(cls, request): - return cls(**cls.unpack_context(request), scope='list') - - @staticmethod - def unpack_context(request): - privilege = request.iam_context['privilege'] - organization = request.iam_context['organization'] - membership = request.iam_context['membership'] - - return { - 'user_id': request.user.id, - 'group_name': getattr(privilege, 'name', None), - 'org_id': getattr(organization, 'id', None), - 'org_owner_id': getattr(organization.owner, 'id', None) - if organization else None, - 'org_role': getattr(membership, 'role', None), - } + def create_scope_list(cls, request, iam_context=None): + if iam_context: + return cls(**iam_context, scope='list') + return cls(**get_iam_context(request, None), scope='list') def __init__(self, **kwargs): self.obj = None @@ -183,11 +212,11 @@ class Scopes(StrEnum): VIEW = 'view' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'organization': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) return permissions @@ -243,11 +272,11 @@ class Scopes(StrEnum): VIEW = 'view' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'invitation': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj, + self = cls.create_base_perm(request, view, scope, iam_context, obj, role=request.data.get('role')) permissions.append(self) @@ -304,7 +333,7 @@ class Scopes(StrEnum): DELETE = 'delete' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'membership': for scope in cls.get_scopes(request, view, obj): @@ -312,7 +341,7 @@ def create(cls, request, view, obj): if scope == 'change:role': params['role'] = request.data.get('role') - self = cls.create_base_perm(request, view, scope, obj, **params) + self = cls.create_base_perm(request, view, scope, iam_context, obj, **params) permissions.append(self) return permissions @@ -358,11 +387,11 @@ class Scopes(StrEnum): LIST_CONTENT = 'list:content' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'server': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) return permissions @@ -390,11 +419,11 @@ class Scopes(StrEnum): DUMP_EVENTS = 'dump:events' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'events': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) return permissions @@ -434,11 +463,11 @@ class Scopes(StrEnum): VIEW = 'view' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'analytics': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) return permissions @@ -467,11 +496,11 @@ class Scopes(StrEnum): DELETE = 'delete' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'user': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) return permissions @@ -492,9 +521,9 @@ def get_scopes(request, view, obj): }.get(view.action)] @classmethod - def create_scope_view(cls, request, user_id): + def create_scope_view(cls, iam_context, user_id): obj = namedtuple('User', ['id'])(id=int(user_id)) - return cls(**cls.unpack_context(request), scope=__class__.Scopes.VIEW, obj=obj) + return cls(**iam_context, scope=__class__.Scopes.VIEW, obj=obj) def get_resource(self): data = None @@ -527,19 +556,19 @@ class Scopes(StrEnum): LIST_OFFLINE = 'list:offline' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'function' or view.basename == 'request': scopes = cls.get_scopes(request, view, obj) for scope in scopes: - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) if job_id := request.data.get('job'): - perm = JobPermission.create_scope_view_data(request, job_id) + perm = JobPermission.create_scope_view_data(iam_context, job_id) permissions.append(perm) elif task_id := request.data.get('task'): - perm = TaskPermission.create_scope_view_data(request, task_id) + perm = TaskPermission.create_scope_view_data(iam_context, task_id) permissions.append(perm) return permissions @@ -574,23 +603,26 @@ class Scopes(StrEnum): DELETE = 'delete' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'cloudstorage': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) return permissions @classmethod - def create_scope_view(cls, request, storage_id): + def create_scope_view(cls, iam_context, storage_id, request=None): try: obj = CloudStorage.objects.get(id=storage_id) except CloudStorage.DoesNotExist as ex: raise ValidationError(str(ex)) - return cls(**cls.unpack_context(request), obj=obj, scope=__class__.Scopes.VIEW) + if not iam_context and request: + iam_context = get_iam_context(request, obj) + + return cls(**iam_context, obj=obj, scope=__class__.Scopes.VIEW) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -650,26 +682,26 @@ class Scopes(StrEnum): IMPORT_BACKUP = 'import:backup' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'project': assignee_id = request.data.get('assignee_id') or request.data.get('assignee') for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj, + self = cls.create_base_perm(request, view, scope, iam_context, obj, assignee_id=assignee_id) permissions.append(self) if view.action == 'tasks': - perm = TaskPermission.create_scope_list(request) + perm = TaskPermission.create_scope_list(request, iam_context) permissions.append(perm) owner = request.data.get('owner_id') or request.data.get('owner') if owner: - perm = UserPermission.create_scope_view(request, owner) + perm = UserPermission.create_scope_view(iam_context, owner) permissions.append(perm) if assignee_id: - perm = UserPermission.create_scope_view(request, assignee_id) + perm = UserPermission.create_scope_view(iam_context, assignee_id) permissions.append(perm) for field_source, field in [ @@ -683,7 +715,7 @@ def create(cls, request, view, obj): field_path = field.split('.') if cloud_storage_id := _get_key(field_source, field_path): permissions.append(CloudStoragePermission.create_scope_view( - request=request, storage_id=cloud_storage_id)) + iam_context, storage_id=cloud_storage_id)) return permissions @@ -735,12 +767,12 @@ def get_scopes(request, view, obj): return scopes @classmethod - def create_scope_view(cls, request, project_id): + def create_scope_view(cls, iam_context, project_id): try: obj = Project.objects.get(id=project_id) except Project.DoesNotExist as ex: raise ValidationError(str(ex)) - return cls(**cls.unpack_context(request), obj=obj, scope=__class__.Scopes.VIEW) + return cls(**iam_context, obj=obj, scope=__class__.Scopes.VIEW) @classmethod def create_scope_create(cls, request, org_id): @@ -753,11 +785,7 @@ def create_scope_create(cls, request, org_id): except Organization.DoesNotExist as ex: raise ValidationError(str(ex)) - try: - membership = Membership.objects.filter( - organization=organization, user=request.user).first() - except Membership.DoesNotExist: - membership = None + membership = get_membership(request, organization) return cls( user_id=request.user.id, @@ -820,7 +848,7 @@ class Scopes(StrEnum): EXPORT_BACKUP = 'export:backup' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'task': project_id = request.data.get('project_id') or request.data.get('project') @@ -835,27 +863,28 @@ def create(cls, request, view, obj): if obj is not None and obj.project is not None: raise ValidationError('Cannot change the organization for ' 'a task inside a project') + # FIX IT: TaskPermission doesn't have create_scope_create method permissions.append(TaskPermission.create_scope_create(request, org_id)) elif scope == __class__.Scopes.UPDATE_OWNER: params['owner_id'] = owner - self = cls.create_base_perm(request, view, scope, obj, **params) + self = cls.create_base_perm(request, view, scope, iam_context, obj, **params) permissions.append(self) if view.action == 'jobs': - perm = JobPermission.create_scope_list(request) + perm = JobPermission.create_scope_list(request, iam_context) permissions.append(perm) if owner: - perm = UserPermission.create_scope_view(request, owner) + perm = UserPermission.create_scope_view(iam_context, owner) permissions.append(perm) if assignee_id: - perm = UserPermission.create_scope_view(request, assignee_id) + perm = UserPermission.create_scope_view(iam_context, assignee_id) permissions.append(perm) if project_id: - perm = ProjectPermission.create_scope_view(request, project_id) + perm = ProjectPermission.create_scope_view(iam_context, project_id) permissions.append(perm) for field_source, field in [ @@ -872,7 +901,7 @@ def create(cls, request, view, obj): field_path = field.split('.') if cloud_storage_id := _get_key(field_source, field_path): permissions.append(CloudStoragePermission.create_scope_view( - request=request, storage_id=cloud_storage_id)) + iam_context, storage_id=cloud_storage_id)) return permissions @@ -966,12 +995,12 @@ def get_scopes(request, view, obj) -> List[Scopes]: return scopes @classmethod - def create_scope_view_data(cls, request, task_id): + def create_scope_view_data(cls, iam_context, task_id): try: obj = Task.objects.get(id=task_id) except Task.DoesNotExist as ex: raise ValidationError(str(ex)) - return cls(**cls.unpack_context(request), obj=obj, scope=__class__.Scopes.VIEW_DATA) + return cls(**iam_context, obj=obj, scope=__class__.Scopes.VIEW_DATA) def get_resource(self): data = None @@ -1035,22 +1064,22 @@ class Scopes(StrEnum): VIEW = 'view' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'webhook': project_id = request.data.get('project_id') for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj, + self = cls.create_base_perm(request, view, scope, iam_context, obj, project_id=project_id) permissions.append(self) owner = request.data.get('owner_id') or request.data.get('owner') if owner: - perm = UserPermission.create_scope_view(request, owner) + perm = UserPermission.create_scope_view(iam_context, owner) permissions.append(perm) if project_id: - perm = ProjectPermission.create_scope_view(request, project_id) + perm = ProjectPermission.create_scope_view(iam_context, project_id) permissions.append(perm) return permissions @@ -1151,20 +1180,20 @@ class Scopes(StrEnum): UPDATE_METADATA = 'update:metadata' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'job': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj) + self = cls.create_base_perm(request, view, scope, iam_context, obj) permissions.append(self) if view.action == 'issues': - perm = IssuePermission.create_scope_list(request) + perm = IssuePermission.create_scope_list(request, iam_context) permissions.append(perm) assignee_id = request.data.get('assignee') if assignee_id: - perm = UserPermission.create_scope_view(request, assignee_id) + perm = UserPermission.create_scope_view(iam_context, assignee_id) permissions.append(perm) for field_source, field in [ @@ -1174,17 +1203,17 @@ def create(cls, request, view, obj): field_path = field.split('.') if cloud_storage_id := _get_key(field_source, field_path): permissions.append(CloudStoragePermission.create_scope_view( - request=request, storage_id=cloud_storage_id)) + iam_context, storage_id=cloud_storage_id)) return permissions @classmethod - def create_scope_view_data(cls, request, job_id): + def create_scope_view_data(cls, iam_context, job_id): try: obj = Job.objects.get(id=job_id) except Job.DoesNotExist as ex: raise ValidationError(str(ex)) - return cls(**cls.unpack_context(request), obj=obj, scope='view:data') + return cls(**iam_context, obj=obj, scope='view:data') def __init__(self, **kwargs): super().__init__(**kwargs) @@ -1287,11 +1316,11 @@ class Scopes(StrEnum): VIEW = 'view' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'comment': for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj, + self = cls.create_base_perm(request, view, scope, iam_context, obj, issue_id=request.data.get('issue')) permissions.append(self) @@ -1372,18 +1401,18 @@ class Scopes(StrEnum): VIEW = 'view' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): permissions = [] if view.basename == 'issue': assignee_id = request.data.get('assignee') for scope in cls.get_scopes(request, view, obj): - self = cls.create_base_perm(request, view, scope, obj, + self = cls.create_base_perm(request, view, scope, iam_context, obj, job_id=request.data.get('job'), assignee_id=assignee_id) permissions.append(self) if assignee_id: - perm = UserPermission.create_scope_view(request, assignee_id) + perm = UserPermission.create_scope_view(iam_context, assignee_id) permissions.append(perm) return permissions @@ -1464,7 +1493,7 @@ class Scopes(StrEnum): VIEW = 'view' @classmethod - def create(cls, request, view, obj): + def create(cls, request, view, obj, iam_context): Scopes = __class__.Scopes permissions = [] @@ -1483,7 +1512,7 @@ def create(cls, request, view, obj): owning_perm_scope = ProjectPermission.Scopes.UPDATE_DESC owning_perm = ProjectPermission.create_base_perm( - request, view, scope=owning_perm_scope, obj=obj.project, + request, view, owning_perm_scope, iam_context, obj=obj.project ) else: if scope == Scopes.VIEW: @@ -1492,25 +1521,25 @@ def create(cls, request, view, obj): owning_perm_scope = TaskPermission.Scopes.UPDATE_DESC owning_perm = TaskPermission.create_base_perm( - request, view, scope=owning_perm_scope, obj=obj.task, + request, view, owning_perm_scope, iam_context, obj=obj.task, ) # This component doesn't define its own rules for these cases permissions.append(owning_perm) elif scope == Scopes.LIST and isinstance(obj, Job): permissions.append(JobPermission.create_base_perm( - request, view, scope=JobPermission.Scopes.VIEW, obj=obj, + request, view, JobPermission.Scopes.VIEW, iam_context, obj=obj, )) elif scope == Scopes.LIST and isinstance(obj, Task): permissions.append(TaskPermission.create_base_perm( - request, view, scope=TaskPermission.Scopes.VIEW, obj=obj, + request, view, TaskPermission.Scopes.VIEW, iam_context, obj=obj, )) elif scope == Scopes.LIST and isinstance(obj, Project): permissions.append(ProjectPermission.create_base_perm( - request, view, scope=ProjectPermission.Scopes.VIEW, obj=obj, + request, view, ProjectPermission.Scopes.VIEW, iam_context, obj=obj, )) else: - permissions.append(cls.create_base_perm(request, view, scope, obj)) + permissions.append(cls.create_base_perm(request, view, scope, iam_context, obj)) return permissions @@ -1560,6 +1589,8 @@ class PolicyEnforcer(BasePermission): def check_permission(self, request, view, obj): permissions: List[OpenPolicyAgentPermission] = [] + iam_context = get_iam_context(request, obj) + # DRF can send OPTIONS request. Internally it will try to get # information about serializers for PUT and POST requests (clone # request and replace the http method). To avoid handling @@ -1567,7 +1598,7 @@ def check_permission(self, request, view, obj): # the condition below is enough. if not self.is_metadata_request(request, view): for perm in OpenPolicyAgentPermission.__subclasses__(): - permissions.extend(perm.create(request, view, obj)) + permissions.extend(perm.create(request, view, obj, iam_context)) allow = True for perm in permissions: @@ -1589,18 +1620,3 @@ def has_object_permission(self, request, view, obj): def is_metadata_request(request, view): return request.method == 'OPTIONS' \ or (request.method == 'POST' and view.action == 'metadata' and len(request.data) == 0) - -class IsMemberInOrganization(BasePermission): - message = 'You should be an active member in the organization.' - - # pylint: disable=no-self-use - def has_permission(self, request, view): - user = request.user - organization = request.iam_context['organization'] - membership = request.iam_context['membership'] - - if organization and not user.is_superuser: - return membership is not None - - return True - diff --git a/cvat/apps/iam/schema.py b/cvat/apps/iam/schema.py index 0fb74b63f40..8847ae227e1 100644 --- a/cvat/apps/iam/schema.py +++ b/cvat/apps/iam/schema.py @@ -6,38 +6,9 @@ import re import textwrap from drf_spectacular.openapi import AutoSchema -from drf_spectacular.extensions import OpenApiFilterExtension, OpenApiAuthenticationExtension +from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.authentication import TokenScheme, SessionScheme -from drf_spectacular.plumbing import build_parameter_type -from drf_spectacular.utils import OpenApiParameter -# https://drf-spectacular.readthedocs.io/en/latest/customization.html?highlight=OpenApiFilterExtension#step-5-extensions -class OrganizationFilterExtension(OpenApiFilterExtension): - """ - Describe OrganizationFilterBackend filter - """ - - target_class = 'cvat.apps.iam.filters.OrganizationFilterBackend' - priority = 1 - - def get_schema_operation_parameters(self, auto_schema, *args, **kwargs): - """Describe query parameters""" - return [ - build_parameter_type( - name=self.target.organization_slug, - required=False, - location=OpenApiParameter.QUERY, - description=self.target.organization_slug_description, - schema={'type': 'string'}, - ), - build_parameter_type( - name=self.target.organization_id, - required=False, - location=OpenApiParameter.QUERY, - description=self.target.organization_id_description, - schema={'type': 'string'}, - ) - ] class SignatureAuthenticationScheme(OpenApiAuthenticationExtension): """ @@ -107,35 +78,6 @@ def get_security_definition(self, auto_schema): return [sessionid_schema, csrftoken_schema] class CustomAutoSchema(AutoSchema): - """ - https://github.com/tfranzel/drf-spectacular/issues/111 - Adds organization context parameters to all endpoints - """ - - def get_override_parameters(self): - return [ - OpenApiParameter( - name='org', - type=str, - required=False, - location=OpenApiParameter.QUERY, - description="Organization unique slug", - ), - OpenApiParameter( - name='org_id', - type=int, - required=False, - location=OpenApiParameter.QUERY, - description="Organization identifier", - ), - OpenApiParameter( - name='X-Organization', - type=str, - required=False, - location=OpenApiParameter.HEADER - ), - ] - def get_operation_id(self): tokenized_path = self._tokenize_path() # replace dashes as they can be problematic later in code generation diff --git a/cvat/apps/iam/views.py b/cvat/apps/iam/views.py index 0d17fef7379..0216888771e 100644 --- a/cvat/apps/iam/views.py +++ b/cvat/apps/iam/views.py @@ -29,15 +29,16 @@ from .authentication import Signer -def get_context(request): - from cvat.apps.organizations.models import Organization, Membership +def get_organization(request): + from cvat.apps.organizations.models import Organization - IAM_ROLES = {role:priority for priority, role in enumerate(settings.IAM_ROLES)} + IAM_ROLES = {role: priority for priority, role in enumerate(settings.IAM_ROLES)} groups = list(request.user.groups.filter(name__in=list(IAM_ROLES.keys()))) groups.sort(key=lambda group: IAM_ROLES[group.name]) + privilege = groups[0] if groups else None organization = None - membership = None + try: org_slug = request.GET.get('org') org_id = request.GET.get('org_id') @@ -46,35 +47,23 @@ def get_context(request): if org_id is not None and (org_slug is not None or org_header is not None): raise ValidationError('You cannot specify "org_id" query parameter with ' '"org" query parameter or "X-Organization" HTTP header at the same time.') + if org_slug is not None and org_header is not None and org_slug != org_header: raise ValidationError('You cannot specify "org" query parameter and ' '"X-Organization" HTTP header with different values.') + org_slug = org_slug if org_slug is not None else org_header - org_filter = None if org_slug: organization = Organization.objects.get(slug=org_slug) - membership = Membership.objects.filter(organization=organization, - user=request.user).first() - org_filter = { 'organization': organization.id } elif org_id: organization = Organization.objects.get(id=int(org_id)) - membership = Membership.objects.filter(organization=organization, - user=request.user).first() - org_filter = { 'organization': organization.id } - elif org_slug is not None: - org_filter = { 'organization': None } except Organization.DoesNotExist: raise NotFound(f'{org_slug or org_id} organization does not exist.') - if membership and not membership.is_active: - membership = None - context = { - "privilege": groups[0] if groups else None, - "membership": membership, "organization": organization, - "visibility": org_filter, + "privilege": getattr(privilege, 'name', None) } return context @@ -86,7 +75,7 @@ def __init__(self, get_response): def __call__(self, request): # https://stackoverflow.com/questions/26240832/django-and-middleware-which-uses-request-user-is-always-anonymous - request.iam_context = SimpleLazyObject(lambda: get_context(request)) + request.iam_context = SimpleLazyObject(lambda: get_organization(request)) return self.get_response(request) diff --git a/cvat/apps/lambda_manager/views.py b/cvat/apps/lambda_manager/views.py index 95c1e4b68e5..b5a0ca91824 100644 --- a/cvat/apps/lambda_manager/views.py +++ b/cvat/apps/lambda_manager/views.py @@ -32,6 +32,7 @@ from cvat.apps.engine.frame_provider import FrameProvider from cvat.apps.engine.models import Job, ShapeType, SourceType, Task from cvat.apps.engine.serializers import LabeledDataSerializer +from cvat.apps.engine.schema import ORGANIZATION_OPEN_API_PARAMETERS from cvat.utils.http import make_requests_session from cvat.apps.iam.permissions import LambdaPermission @@ -804,9 +805,10 @@ def call(self, request, func_id): summary='Method returns a list of requests'), #TODO create=extend_schema( - summary='Method calls the function'), - delete=extend_schema( - summary='Method cancels the request') + parameters=ORGANIZATION_OPEN_API_PARAMETERS, + summary='Method calls the function' + ), + delete=extend_schema(summary='Method cancels the request') ) class RequestViewSet(viewsets.ViewSet): iam_organization_field = None diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index 45bd35634b3..d4b9f3a27e4 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -56,7 +56,8 @@ class Invitation(models.Model): owner = models.ForeignKey(get_user_model(), null=True, on_delete=models.SET_NULL) membership = models.OneToOneField(Membership, on_delete=models.CASCADE) - def get_organization_id(self): + @property + def organization_id(self): return self.membership.organization_id def send(self): diff --git a/cvat/apps/organizations/views.py b/cvat/apps/organizations/views.py index 39829471275..13f757018e9 100644 --- a/cvat/apps/organizations/views.py +++ b/cvat/apps/organizations/views.py @@ -9,6 +9,7 @@ from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view from cvat.apps.engine.mixins import PartialUpdateModelMixin +from cvat.apps.engine.schema import ORGANIZATION_OPEN_API_PARAMETERS from cvat.apps.iam.permissions import ( InvitationPermission, MembershipPermission, OrganizationPermission) @@ -56,7 +57,7 @@ class OrganizationViewSet(viewsets.GenericViewSet, mixins.DestroyModelMixin, PartialUpdateModelMixin, ): - queryset = Organization.objects.all() + queryset = Organization.objects.select_related('owner').all() search_fields = ('name', 'owner') filter_fields = list(search_fields) + ['id', 'slug'] simple_filters = list(search_fields) + ['slug'] @@ -114,7 +115,7 @@ class Meta: ) class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, PartialUpdateModelMixin, viewsets.GenericViewSet): - queryset = Membership.objects.all() + queryset = Membership.objects.select_related('invitation', 'user').all() ordering = '-id' http_method_names = ['get', 'patch', 'delete', 'head', 'options'] search_fields = ('user', 'role') @@ -133,8 +134,11 @@ def get_serializer_class(self): def get_queryset(self): queryset = super().get_queryset() - permission = MembershipPermission.create_scope_list(self.request) - return permission.filter(queryset) + if self.action == 'list': + permission = MembershipPermission.create_scope_list(self.request) + queryset = permission.filter(queryset) + + return queryset @extend_schema(tags=['invitations']) @extend_schema_view( @@ -157,6 +161,7 @@ def get_queryset(self): create=extend_schema( summary='Method creates an invitation', request=InvitationWriteSerializer, + parameters=ORGANIZATION_OPEN_API_PARAMETERS, responses={ '201': InvitationReadSerializer, # check InvitationWriteSerializer.to_representation }), diff --git a/cvat/apps/webhooks/views.py b/cvat/apps/webhooks/views.py index 10972e58882..e71aa5c06b2 100644 --- a/cvat/apps/webhooks/views.py +++ b/cvat/apps/webhooks/views.py @@ -2,29 +2,22 @@ # # SPDX-License-Identifier: MIT -from drf_spectacular.utils import ( - OpenApiParameter, - OpenApiResponse, - OpenApiTypes, - extend_schema, - extend_schema_view, -) +from drf_spectacular.utils import (OpenApiParameter, OpenApiResponse, + OpenApiTypes, extend_schema, + extend_schema_view) from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import SAFE_METHODS from rest_framework.response import Response +from cvat.apps.engine.schema import ORGANIZATION_OPEN_API_PARAMETERS from cvat.apps.engine.view_utils import list_action, make_paginated_response from cvat.apps.iam.permissions import WebhookPermission from .event_type import AllEvents, OrganizationEvents, ProjectEvents from .models import Webhook, WebhookDelivery, WebhookTypeChoice -from .serializers import ( - EventsSerializer, - WebhookDeliveryReadSerializer, - WebhookReadSerializer, - WebhookWriteSerializer, -) +from .serializers import (EventsSerializer, WebhookDeliveryReadSerializer, + WebhookReadSerializer, WebhookWriteSerializer) from .signals import signal_ping, signal_redelivery @@ -55,6 +48,7 @@ create=extend_schema( request=WebhookWriteSerializer, summary="Method creates a webhook", + parameters=ORGANIZATION_OPEN_API_PARAMETERS, responses={ "201": WebhookReadSerializer }, # check WebhookWriteSerializer.to_representation diff --git a/cvat/schema.yml b/cvat/schema.yml index 6f69ea88a85..508cfc84d9b 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -25,21 +25,6 @@ paths: Accept the following POST parameters: username, email, password Return the REST Framework Token Object's key. - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth requestBody: @@ -70,21 +55,6 @@ paths: assigned to the current User object. Accepts/Returns nothing. - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth security: @@ -109,21 +79,6 @@ paths: Accepts the following POST parameters: new_password1, new_password2 Returns the success/fail message. - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth requestBody: @@ -153,21 +108,6 @@ paths: Accepts the following POST parameters: email Returns the success/fail message. - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth requestBody: @@ -200,21 +140,6 @@ paths: Accepts the following POST parameters: token, uid, new_password1, new_password2 Returns the success/fail message. - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth requestBody: @@ -240,21 +165,6 @@ paths: /api/auth/register: post: operationId: auth_create_register - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth requestBody: @@ -280,21 +190,6 @@ paths: /api/auth/rules: get: operationId: auth_retrieve_rules - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth security: @@ -308,21 +203,6 @@ paths: description: Signed URL contains a token which authenticates a user on the server.Signed URL is valid during 30 seconds since signing. summary: This method signs URL for access to the server - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - auth requestBody: @@ -349,8 +229,9 @@ paths: operationId: cloudstorages_list summary: Returns a paginated list of storages parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: credentials_type @@ -377,16 +258,16 @@ paths: description: A simple equality filter for the name field schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -540,26 +421,12 @@ paths: operationId: cloudstorages_retrieve summary: Method returns details of a specific cloud storage parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this cloud storage. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - cloudstorages security: @@ -579,26 +446,12 @@ paths: operationId: cloudstorages_partial_update summary: Methods does a partial update of chosen fields in a cloud storage instance parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this cloud storage. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - cloudstorages requestBody: @@ -670,26 +523,12 @@ paths: operationId: cloudstorages_destroy summary: Method deletes a specific cloud storage parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this cloud storage. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - cloudstorages security: @@ -708,26 +547,12 @@ paths: for reading/writing summary: Method returns allowed actions for the cloud storage parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this cloud storage. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - cloudstorages security: @@ -750,10 +575,6 @@ paths: Please use the new version of API: /cloudstorages/id/content-v2/' summary: Method returns a manifest content parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: @@ -765,16 +586,6 @@ paths: schema: type: string description: Path to the manifest file in a cloud storage - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - cloudstorages security: @@ -798,10 +609,6 @@ paths: operationId: cloudstorages_retrieve_content_v2 summary: Method returns the content of the cloud storage parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: @@ -818,16 +625,6 @@ paths: schema: type: string description: Used to continue listing files in the bucket - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: page_size schema: @@ -857,26 +654,12 @@ paths: operationId: cloudstorages_retrieve_preview summary: Method returns a preview image from a cloud storage parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this cloud storage. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - cloudstorages security: @@ -897,26 +680,12 @@ paths: operationId: cloudstorages_retrieve_status summary: Method returns a cloud storage status parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this cloud storage. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - cloudstorages security: @@ -937,8 +706,9 @@ paths: operationId: comments_list summary: Method returns a paginated list of comments parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: filter @@ -963,16 +733,16 @@ paths: description: A simple equality filter for the job_id field schema: type: integer - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -1062,26 +832,12 @@ paths: operationId: comments_retrieve summary: Method returns details of a comment parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this comment. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - comments security: @@ -1101,26 +857,12 @@ paths: operationId: comments_partial_update summary: Methods does a partial update of chosen fields in a comment parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this comment. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - comments requestBody: @@ -1145,26 +887,12 @@ paths: operationId: comments_destroy summary: Method deletes a comment parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this comment. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - comments security: @@ -1182,10 +910,6 @@ paths: description: Recieve logs from the server summary: 'Method returns csv log file ' parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -1211,11 +935,6 @@ paths: schema: type: integer description: Filter events by job ID - - in: query - name: org - schema: - type: string - description: Organization unique slug - in: query name: org_id schema: @@ -1308,8 +1027,9 @@ paths: operationId: invitations_list summary: Method returns a paginated list of invitations parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: filter @@ -1318,16 +1038,16 @@ paths: description: 'A filter term. Available filter_fields: [''owner'']' schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -1417,26 +1137,12 @@ paths: operationId: invitations_retrieve summary: Method returns details of an invitation parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: key schema: type: string description: A unique value identifying this invitation. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - invitations security: @@ -1456,26 +1162,12 @@ paths: operationId: invitations_partial_update summary: Methods does a partial update of chosen fields in an invitation parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: key schema: type: string description: A unique value identifying this invitation. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - invitations requestBody: @@ -1500,26 +1192,12 @@ paths: operationId: invitations_destroy summary: Method deletes an invitation parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: key schema: type: string description: A unique value identifying this invitation. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - invitations security: @@ -1536,8 +1214,9 @@ paths: operationId: issues_list summary: Method returns a paginated list of issues parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: assignee @@ -1562,16 +1241,16 @@ paths: description: A simple equality filter for the job_id field schema: type: integer - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -1672,26 +1351,12 @@ paths: operationId: issues_retrieve summary: Method returns details of an issue parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this issue. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - issues security: @@ -1711,26 +1376,12 @@ paths: operationId: issues_partial_update summary: Methods does a partial update of chosen fields in an issue parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this issue. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - issues requestBody: @@ -1755,26 +1406,12 @@ paths: operationId: issues_destroy summary: Method deletes an issue parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this issue. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - issues security: @@ -1791,8 +1428,9 @@ paths: operationId: jobs_list summary: Method returns a paginated list of jobs parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: assignee @@ -1816,16 +1454,16 @@ paths: ''updated_date'', ''dimension'']' schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: page required: false in: query @@ -1912,26 +1550,12 @@ paths: operationId: jobs_retrieve summary: Method returns details of a job parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - jobs security: @@ -1951,26 +1575,12 @@ paths: operationId: jobs_partial_update summary: Methods does a partial update of chosen fields in a job parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - jobs requestBody: @@ -1997,10 +1607,6 @@ paths: summary: Method returns annotations for a specific job as a JSON document. If format is specified, a zip archive is returned. parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -2035,22 +1641,12 @@ paths: required: true - in: query name: location - schema: - type: string - enum: - - cloud_storage - - local - description: Where need to save downloaded annotation - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier + schema: + type: string + enum: + - cloud_storage + - local + description: Where need to save downloaded annotation - in: query name: use_default_location schema: @@ -2082,10 +1678,6 @@ paths: operationId: jobs_create_annotations summary: Method allows to upload job annotations parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: cloud_storage_id schema: @@ -2118,16 +1710,6 @@ paths: - cloud_storage - local description: where to import the annotation from - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -2162,10 +1744,6 @@ paths: operationId: jobs_update_annotations summary: Method performs an update of all annotations in a specific job parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: format schema: @@ -2180,16 +1758,6 @@ paths: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - jobs requestBody: @@ -2217,10 +1785,6 @@ paths: operationId: jobs_partial_update_annotations summary: Method performs a partial update of annotations in a specific job parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -2236,16 +1800,6 @@ paths: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - jobs requestBody: @@ -2269,26 +1823,12 @@ paths: operationId: jobs_destroy_annotations summary: Method deletes all annotations for a specific job parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - jobs security: @@ -2305,10 +1845,6 @@ paths: operationId: jobs_retrieve_data summary: Method returns data for a specific job parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: @@ -2320,16 +1856,6 @@ paths: schema: type: integer description: A unique number value identifying chunk or frame - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: quality schema: @@ -2369,26 +1895,12 @@ paths: summary: Method provides a meta information about media files which are related with the job parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - jobs security: @@ -2409,26 +1921,12 @@ paths: summary: Method provides a meta information about media files which are related with the job parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks requestBody: @@ -2454,10 +1952,6 @@ paths: operationId: jobs_retrieve_dataset summary: Export job as a dataset in a specific format parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -2499,16 +1993,6 @@ paths: - cloud_storage - local description: Where need to save downloaded dataset - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -2542,26 +2026,12 @@ paths: operationId: jobs_retrieve_preview summary: Method returns a preview image for the job parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this job. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - jobs security: @@ -2695,26 +2165,12 @@ paths: operationId: labels_retrieve summary: Method returns details of a label parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this label. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - labels security: @@ -2735,26 +2191,12 @@ paths: summary: Methods does a partial update of chosen fields in a labelTo modify a sublabel, please use the PATCH method of the parent label parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this label. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - labels requestBody: @@ -2780,26 +2222,12 @@ paths: summary: Method deletes a label. To delete a sublabel, please use the PATCH method of the parent label parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this label. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - labels security: @@ -2815,21 +2243,6 @@ paths: get: operationId: lambda_list_functions summary: Method returns a list of functions - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - lambda security: @@ -2846,26 +2259,12 @@ paths: operationId: lambda_retrieve_functions summary: Method returns the information about the function parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: func_id schema: type: string pattern: ^[a-zA-Z0-9_.-]+$ required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - lambda security: @@ -2893,26 +2292,12 @@ paths: in the 'job' input field. The task id is not required in this case, but if it is specified, it must match the job task id. parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: func_id schema: type: string pattern: ^[a-zA-Z0-9_.-]+$ required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - lambda requestBody: @@ -2933,21 +2318,6 @@ paths: get: operationId: lambda_list_requests summary: Method returns a list of requests - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - lambda security: @@ -2993,26 +2363,12 @@ paths: operationId: lambda_retrieve_requests summary: Method returns the status of the request parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: Request id required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - lambda security: @@ -3029,8 +2385,9 @@ paths: operationId: memberships_list summary: Method returns a paginated list of memberships parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: filter @@ -3040,16 +2397,16 @@ paths: ''id'']' schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: page required: false in: query @@ -3110,26 +2467,12 @@ paths: operationId: memberships_retrieve summary: Method returns details of a membership parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this membership. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - memberships security: @@ -3149,26 +2492,12 @@ paths: operationId: memberships_partial_update summary: Methods does a partial update of chosen fields in a membership parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this membership. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - memberships requestBody: @@ -3193,26 +2522,12 @@ paths: operationId: memberships_destroy summary: Method deletes a membership parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this membership. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - memberships security: @@ -3229,10 +2544,6 @@ paths: operationId: organizations_list summary: Method returns a paginated list of organizations parameters: - - in: header - name: X-Organization - schema: - type: string - name: filter required: false in: query @@ -3245,16 +2556,6 @@ paths: description: A simple equality filter for the name field schema: type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -3308,21 +2609,6 @@ paths: post: operationId: organizations_create summary: Method creates an organization - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - organizations requestBody: @@ -3349,26 +2635,12 @@ paths: operationId: organizations_retrieve summary: Method returns details of an organization parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this organization. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - organizations security: @@ -3388,26 +2660,12 @@ paths: operationId: organizations_partial_update summary: Methods does a partial update of chosen fields in an organization parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this organization. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - organizations requestBody: @@ -3432,26 +2690,12 @@ paths: operationId: organizations_destroy summary: Method deletes an organization parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this organization. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - organizations security: @@ -3468,8 +2712,9 @@ paths: operationId: projects_list summary: Returns a paginated list of projects parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: assignee @@ -3489,16 +2734,16 @@ paths: description: A simple equality filter for the name field schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -3598,26 +2843,12 @@ paths: operationId: projects_retrieve summary: Method returns details of a specific project parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this project. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - projects security: @@ -3637,26 +2868,12 @@ paths: operationId: projects_partial_update summary: Methods does a partial update of chosen fields in a project parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this project. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - projects requestBody: @@ -3681,26 +2898,12 @@ paths: operationId: projects_destroy summary: Method deletes a specific project parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this project. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - projects security: @@ -3717,10 +2920,6 @@ paths: operationId: projects_retrieve_annotations summary: Method allows to download project annotations parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -3762,16 +2961,6 @@ paths: - cloud_storage - local description: Where need to save downloaded dataset - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -3806,10 +2995,6 @@ paths: operationId: projects_retrieve_backup summary: Methods creates a backup copy of a project parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -3842,16 +3027,6 @@ paths: - cloud_storage - local description: Where need to save downloaded backup - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -3878,10 +3053,6 @@ paths: operationId: projects_retrieve_dataset summary: Export project as a dataset in a specific format parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -3923,16 +3094,6 @@ paths: - cloud_storage - local description: Where need to save downloaded dataset - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -3965,10 +3126,6 @@ paths: operationId: projects_create_dataset summary: Import dataset in specific format as a project parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: cloud_storage_id schema: @@ -4001,16 +3158,6 @@ paths: - cloud_storage - local description: Where to import the dataset from - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -4046,26 +3193,12 @@ paths: operationId: projects_retrieve_preview summary: Method returns a preview image for the project parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this project. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - projects security: @@ -4147,10 +3280,6 @@ paths: - YAML: application/vnd.oai.openapi - JSON: application/vnd.oai.openapi+json parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: lang schema: @@ -4253,17 +3382,7 @@ paths: - uz - vi - zh-hans - - zh-hant - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier + - zh-hant - in: query name: scheme schema: @@ -4303,21 +3422,6 @@ paths: get: operationId: server_retrieve_about summary: Method provides basic CVAT information - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - server security: @@ -4337,21 +3441,6 @@ paths: get: operationId: server_retrieve_annotation_formats summary: Method provides the list of supported annotations formats - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - server security: @@ -4371,21 +3460,6 @@ paths: get: operationId: server_retrieve_plugins summary: Method provides allowed plugins - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - server security: @@ -4407,25 +3481,11 @@ paths: summary: Returns all files and folders that are on the server along specified path parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: directory schema: type: string description: Directory to browse - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - server security: @@ -4448,8 +3508,9 @@ paths: operationId: tasks_list summary: Returns a paginated list of tasks parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: assignee @@ -4483,16 +3544,16 @@ paths: description: A simple equality filter for the name field schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -4615,26 +3676,12 @@ paths: operationId: tasks_retrieve summary: Method returns details of a specific task parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks security: @@ -4654,26 +3701,12 @@ paths: operationId: tasks_partial_update summary: Methods does a partial update of chosen fields in a task parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks requestBody: @@ -4699,26 +3732,12 @@ paths: summary: Method deletes a specific task, all attached jobs, annotations, and data parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks security: @@ -4735,10 +3754,6 @@ paths: operationId: tasks_retrieve_annotations summary: Method allows to download task annotations parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -4779,16 +3794,6 @@ paths: - cloud_storage - local description: Where need to save downloaded dataset - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -4823,10 +3828,6 @@ paths: summary: Method allows to upload task annotations from a local file or a cloud storage parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: cloud_storage_id schema: @@ -4859,16 +3860,6 @@ paths: - cloud_storage - local description: where to import the annotation from - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -4902,10 +3893,6 @@ paths: operationId: tasks_update_annotations summary: Method allows to upload task annotations parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: format schema: @@ -4920,16 +3907,6 @@ paths: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks requestBody: @@ -4957,10 +3934,6 @@ paths: operationId: tasks_partial_update_annotations summary: Method performs a partial update of annotations in a specific task parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -4976,16 +3949,6 @@ paths: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks requestBody: @@ -5013,26 +3976,12 @@ paths: operationId: tasks_destroy_annotations summary: Method deletes all annotations for a specific task parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks security: @@ -5049,10 +3998,6 @@ paths: operationId: tasks_retrieve_backup summary: Method backup a specified task parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -5085,16 +4030,6 @@ paths: - cloud_storage - local description: Where need to save downloaded backup - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -5121,10 +4056,6 @@ paths: operationId: tasks_retrieve_data summary: Method returns data for a specific task parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: @@ -5136,16 +4067,6 @@ paths: schema: type: integer description: A unique number value identifying chunk or frame - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: quality schema: @@ -5196,26 +4117,12 @@ paths: schema: type: boolean description: Initializes data upload. No data should be sent with this header - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks requestBody: @@ -5242,26 +4149,12 @@ paths: summary: Method provides a meta information about media files which are related with the task parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks security: @@ -5282,26 +4175,12 @@ paths: summary: Method provides a meta information about media files which are related with the task parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. - required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier + required: true tags: - tasks requestBody: @@ -5327,10 +4206,6 @@ paths: operationId: tasks_retrieve_dataset summary: Export task as a dataset in a specific format parameters: - - in: header - name: X-Organization - schema: - type: string - in: query name: action schema: @@ -5372,16 +4247,6 @@ paths: - cloud_storage - local description: Where need to save downloaded dataset - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: use_default_location schema: @@ -5417,26 +4282,12 @@ paths: operationId: tasks_retrieve_preview summary: Method returns a preview image for the task parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks security: @@ -5456,26 +4307,12 @@ paths: summary: When task is being created the method returns information about a status of the creation process parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this task. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - tasks security: @@ -5556,8 +4393,9 @@ paths: operationId: users_list summary: Method returns a paginated list of users parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: filter @@ -5582,16 +4420,16 @@ paths: description: A simple equality filter for the last_name field schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: page required: false in: query @@ -5643,26 +4481,12 @@ paths: operationId: users_retrieve summary: Method provides information of a specific user parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this user. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - users security: @@ -5682,26 +4506,12 @@ paths: operationId: users_partial_update summary: Method updates chosen fields of a user parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this user. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - users requestBody: @@ -5726,26 +4536,12 @@ paths: operationId: users_destroy summary: Method deletes a specific user from the server parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this user. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - users security: @@ -5762,21 +4558,6 @@ paths: operationId: users_retrieve_self description: Method returns an instance of a user who is currently authorized summary: Method returns an instance of a user who is currently authorized - parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - users security: @@ -5797,8 +4578,9 @@ paths: operationId: webhooks_list summary: Method returns a paginated list of webhook according to query parameters parameters: - - in: header - name: X-Organization + - name: X-Organization + in: header + description: Organization unique slug schema: type: string - name: filter @@ -5808,16 +4590,16 @@ paths: ''type'', ''description'', ''id'', ''project_id'', ''updated_date'']' schema: type: string - - in: query - name: org + - name: org + in: query + description: Organization unique slug schema: type: string - description: Organization unique slug - - in: query - name: org_id + - name: org_id + in: query + description: Organization identifier schema: type: integer - description: Organization identifier - name: owner in: query description: A simple equality filter for the owner field @@ -5927,26 +4709,12 @@ paths: operationId: webhooks_retrieve summary: Method returns details of a webhook parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - webhooks security: @@ -5966,26 +4734,12 @@ paths: operationId: webhooks_update summary: Method updates a webhook by id parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - webhooks requestBody: @@ -6011,26 +4765,12 @@ paths: operationId: webhooks_partial_update summary: Methods does a partial update of chosen fields in a webhook parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - webhooks requestBody: @@ -6055,26 +4795,12 @@ paths: operationId: webhooks_destroy summary: Method deletes a webhook parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - webhooks security: @@ -6091,10 +4817,6 @@ paths: operationId: webhooks_list_deliveries summary: Method return a list of deliveries for a specific webhook parameters: - - in: header - name: X-Organization - schema: - type: string - name: filter required: false in: query @@ -6107,16 +4829,6 @@ paths: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - name: page required: false in: query @@ -6162,10 +4874,6 @@ paths: operationId: webhooks_retrieve_deliveries summary: Method return a specific delivery for a specific webhook parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: delivery_id schema: @@ -6178,16 +4886,6 @@ paths: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - webhooks security: @@ -6208,10 +4906,6 @@ paths: operationId: webhooks_create_deliveries_redelivery summary: Method redeliver a specific webhook delivery parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: delivery_id schema: @@ -6224,16 +4918,6 @@ paths: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - webhooks security: @@ -6250,26 +4934,12 @@ paths: operationId: webhooks_create_ping summary: Method send ping webhook parameters: - - in: header - name: X-Organization - schema: - type: string - in: path name: id schema: type: integer description: A unique integer value identifying this webhook. required: true - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier tags: - webhooks security: @@ -6290,20 +4960,6 @@ paths: operationId: webhooks_retrieve_events summary: Method return a list of available webhook events parameters: - - in: header - name: X-Organization - schema: - type: string - - in: query - name: org - schema: - type: string - description: Organization unique slug - - in: query - name: org_id - schema: - type: integer - description: Organization identifier - in: query name: type schema: @@ -7336,6 +5992,10 @@ components: minimum: 0 nullable: true readOnly: true + organization: + type: integer + readOnly: true + nullable: true data_compressed_chunk_type: allOf: - $ref: '#/components/schemas/ChunkType' diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 83547e6def3..8c24978a922 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -154,7 +154,6 @@ def add_ssh_keys(): ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', - 'cvat.apps.iam.permissions.IsMemberInOrganization', 'cvat.apps.iam.permissions.PolicyEnforcer', ], 'DEFAULT_AUTHENTICATION_CLASSES': [ diff --git a/tests/cypress/e2e/actions_organizations/case_113_new_organization_pipeline.js b/tests/cypress/e2e/actions_organizations/case_113_new_organization_pipeline.js index 6f488ea5f98..b645dc61ed6 100644 --- a/tests/cypress/e2e/actions_organizations/case_113_new_organization_pipeline.js +++ b/tests/cypress/e2e/actions_organizations/case_113_new_organization_pipeline.js @@ -233,22 +233,18 @@ context('New organization pipeline.', () => { }); }); - it("Logout, the third user login. The user does not see the project, the task. The user can't open the job using direct link.", () => { + it('Logout, the third user login. The user does not see the project, the task.', () => { cy.logout(secondUserName); cy.login(thirdUserName, thirdUser.password); cy.contains('.cvat-item-task-name', taskName).should('not.exist'); cy.goToProjectsList(); cy.contains('.cvat-projects-project-item-title', project.name).should('not.exist'); - cy.visit(`/tasks/${taskID}/jobs/${jobID}`); - cy.get('.cvat-canvas-container').should('not.exist'); - cy.get('.cvat-notification-notice-fetch-job-failed').should('be.visible'); - cy.closeNotification('.cvat-notification-notice-fetch-job-failed'); }); - it('The third user activates the organization. Now can open the job using direct link. Create an object, save annotations.', () => { - cy.activateOrganization(organizationParams.shortName); + it('User can open the job using direct link. Organization is set automatically. Create an object, save annotations.', () => { cy.visit(`/tasks/${taskID}/jobs/${jobID}`); cy.get('.cvat-canvas-container').should('exist'); + cy.get('.cvat-header-menu-user-dropdown-organization').should('have.text', organizationParams.shortName); cy.createCuboid(createCuboidShape2Points); cy.saveJob(); }); diff --git a/tests/cypress/e2e/actions_tasks/task_rectangles_only.js b/tests/cypress/e2e/actions_tasks/task_rectangles_only.js index 7e5bdcdabdc..0ce54094fe1 100644 --- a/tests/cypress/e2e/actions_tasks/task_rectangles_only.js +++ b/tests/cypress/e2e/actions_tasks/task_rectangles_only.js @@ -80,7 +80,7 @@ context('Creating a task with only bounding boxes', () => { cy.wait('@taskPost').then((interception) => { taskID = interception.response.body.id; expect(interception.response.statusCode).to.be.equal(201); - cy.intercept(`/api/tasks/${taskID}?**`).as('getTask'); + cy.intercept(`/api/tasks/${taskID}`).as('getTask'); cy.wait('@getTask', { timeout: 10000 }); cy.get('.cvat-task-jobs-table-row').should('exist').and('be.visible'); cy.openJob(); diff --git a/tests/cypress/e2e/skeletons/skeletons_pipeline.js b/tests/cypress/e2e/skeletons/skeletons_pipeline.js index b35da629884..049ad405287 100644 --- a/tests/cypress/e2e/skeletons/skeletons_pipeline.js +++ b/tests/cypress/e2e/skeletons/skeletons_pipeline.js @@ -132,7 +132,7 @@ context('Manipulations with skeletons', { scrollBehavior: false }, () => { cy.wait('@taskPost').then((interception) => { taskID = interception.response.body.id; expect(interception.response.statusCode).to.be.equal(201); - cy.intercept(`/api/tasks/${taskID}?**`).as('getTask'); + cy.intercept(`/api/tasks/${taskID}`).as('getTask'); cy.wait('@getTask', { timeout: 10000 }); cy.get('.cvat-task-jobs-table-row').should('exist').and('be.visible'); cy.openJob(); diff --git a/tests/python/rest_api/test_cloud_storages.py b/tests/python/rest_api/test_cloud_storages.py index ac1cf86bc1a..da308e4410b 100644 --- a/tests/python/rest_api/test_cloud_storages.py +++ b/tests/python/rest_api/test_cloud_storages.py @@ -26,11 +26,10 @@ @pytest.mark.usefixtures("restore_db_per_class") class TestGetCloudStorage: - def _test_can_see(self, user, storage_id, data, **kwargs): + def _test_can_see(self, user, storage_id, data): with make_api_client(user) as api_client: (_, response) = api_client.cloudstorages_api.retrieve( id=storage_id, - **kwargs, _parse_response=False, _check_status=False, ) @@ -44,11 +43,10 @@ def _test_can_see(self, user, storage_id, data, **kwargs): == {} ) - def _test_cannot_see(self, user, storage_id, **kwargs): + def _test_cannot_see(self, user, storage_id): with make_api_client(user) as api_client: (_, response) = api_client.cloudstorages_api.retrieve( id=storage_id, - **kwargs, _parse_response=False, _check_status=False, ) @@ -66,7 +64,6 @@ def _test_cannot_see(self, user, storage_id, **kwargs): def test_sandbox_user_get_cloud_storage( self, storage_id, group, is_owner, is_allow, users, cloud_storages ): - org = "" cloud_storage = cloud_storages[storage_id] username = ( cloud_storage["owner"]["username"] @@ -81,9 +78,9 @@ def test_sandbox_user_get_cloud_storage( ) if is_allow: - self._test_can_see(username, storage_id, cloud_storage, org=org) + self._test_can_see(username, storage_id, cloud_storage) else: - self._test_cannot_see(username, storage_id, org=org) + self._test_cannot_see(username, storage_id) @pytest.mark.parametrize("org_id", [2]) @pytest.mark.parametrize("storage_id", [2]) @@ -112,9 +109,9 @@ def test_org_user_get_cloud_storage( ) if is_allow: - self._test_can_see(username, storage_id, cloud_storage, org_id=org_id) + self._test_can_see(username, storage_id, cloud_storage) else: - self._test_cannot_see(username, storage_id, org_id=org_id) + self._test_cannot_see(username, storage_id) class TestCloudStoragesListFilters(CollectionSimpleFilterTestBase): @@ -258,12 +255,11 @@ class TestPatchCloudStorage: } ] - def _test_can_update(self, user, storage_id, spec, **kwargs): + def _test_can_update(self, user, storage_id, spec): with make_api_client(user) as api_client: (_, response) = api_client.cloudstorages_api.partial_update( id=storage_id, patched_cloud_storage_write_request=models.PatchedCloudStorageWriteRequest(**spec), - **kwargs, _parse_response=False, _check_status=False, ) @@ -275,12 +271,11 @@ def _test_can_update(self, user, storage_id, spec, **kwargs): == {} ) - def _test_cannot_update(self, user, storage_id, spec, **kwargs): + def _test_cannot_update(self, user, storage_id, spec): with make_api_client(user) as api_client: (_, response) = api_client.cloudstorages_api.partial_update( id=storage_id, patched_cloud_storage_write_request=models.PatchedCloudStorageWriteRequest(**spec), - **kwargs, _parse_response=False, _check_status=False, ) @@ -298,7 +293,6 @@ def _test_cannot_update(self, user, storage_id, spec, **kwargs): def test_sandbox_user_update_cloud_storage( self, storage_id, group, is_owner, is_allow, users, cloud_storages ): - org = "" cloud_storage = cloud_storages[storage_id] username = ( cloud_storage["owner"]["username"] @@ -313,9 +307,9 @@ def test_sandbox_user_update_cloud_storage( ) if is_allow: - self._test_can_update(username, storage_id, self._SPEC, org=org) + self._test_can_update(username, storage_id, self._SPEC) else: - self._test_cannot_update(username, storage_id, self._SPEC, org=org) + self._test_cannot_update(username, storage_id, self._SPEC) @pytest.mark.parametrize("org_id", [2]) @pytest.mark.parametrize("storage_id", [2]) @@ -344,18 +338,17 @@ def test_org_user_update_cloud_storage( ) if is_allow: - self._test_can_update(username, storage_id, self._PRIVATE_BUCKET_SPEC, org_id=org_id) + self._test_can_update(username, storage_id, self._PRIVATE_BUCKET_SPEC) else: - self._test_cannot_update(username, storage_id, self._PRIVATE_BUCKET_SPEC, org_id=org_id) + self._test_cannot_update(username, storage_id, self._PRIVATE_BUCKET_SPEC) @pytest.mark.usefixtures("restore_db_per_class") class TestGetCloudStoragePreview: - def _test_can_see(self, user, storage_id, **kwargs): + def _test_can_see(self, user, storage_id): with make_api_client(user) as api_client: (_, response) = api_client.cloudstorages_api.retrieve_preview( id=storage_id, - **kwargs, _parse_response=False, _check_status=False, ) @@ -364,11 +357,10 @@ def _test_can_see(self, user, storage_id, **kwargs): (width, height) = Image.open(io.BytesIO(response.data)).size assert width > 0 and height > 0 - def _test_cannot_see(self, user, storage_id, **kwargs): + def _test_cannot_see(self, user, storage_id): with make_api_client(user) as api_client: (_, response) = api_client.cloudstorages_api.retrieve_preview( id=storage_id, - **kwargs, _parse_response=False, _check_status=False, ) @@ -386,7 +378,6 @@ def _test_cannot_see(self, user, storage_id, **kwargs): def test_sandbox_user_get_cloud_storage_preview( self, storage_id, group, is_owner, is_allow, users, cloud_storages ): - org = "" cloud_storage = cloud_storages[storage_id] username = ( cloud_storage["owner"]["username"] @@ -401,9 +392,9 @@ def test_sandbox_user_get_cloud_storage_preview( ) if is_allow: - self._test_can_see(username, storage_id, org=org) + self._test_can_see(username, storage_id) else: - self._test_cannot_see(username, storage_id, org=org) + self._test_cannot_see(username, storage_id) @pytest.mark.parametrize("org_id", [2]) @pytest.mark.parametrize("storage_id", [2]) @@ -432,9 +423,9 @@ def test_org_user_get_cloud_storage_preview( ) if is_allow: - self._test_can_see(username, storage_id, org_id=org_id) + self._test_can_see(username, storage_id) else: - self._test_cannot_see(username, storage_id, org_id=org_id) + self._test_cannot_see(username, storage_id) class TestGetCloudStorageContent: diff --git a/tests/python/rest_api/test_issues.py b/tests/python/rest_api/test_issues.py index 163a636a6c8..dd80869cb59 100644 --- a/tests/python/rest_api/test_issues.py +++ b/tests/python/rest_api/test_issues.py @@ -245,7 +245,7 @@ def test_member_update_issue( username, issue_id = find_issue_staff_user(issues, users, issue_staff, issue_admin) data = request_and_response_data(issue_id, username=username) - self._test_check_response(username, issue_id, data, is_allow, org_id=org) + self._test_check_response(username, issue_id, data, is_allow) @pytest.mark.usefixtures("restore_db_per_function") @@ -331,7 +331,7 @@ def test_org_member_delete_issue( issues = issues_by_org[org] username, issue_id = find_issue_staff_user(issues, users, issue_staff, issue_admin) - self._test_check_response(username, issue_id, expect_success, org_id=org) + self._test_check_response(username, issue_id, expect_success) class TestIssuesListFilters(CollectionSimpleFilterTestBase): diff --git a/tests/python/rest_api/test_jobs.py b/tests/python/rest_api/test_jobs.py index b347b94b611..2a6c260a428 100644 --- a/tests/python/rest_api/test_jobs.py +++ b/tests/python/rest_api/test_jobs.py @@ -39,15 +39,15 @@ def get_job_staff(job, tasks, projects): def filter_jobs(jobs, tasks, org): - if org is None: - kwargs = {} - jobs = jobs.raw + if isinstance(org, int): + kwargs = {"org_id": org} + jobs = [job for job in jobs if tasks[job["task_id"]]["organization"] == org] elif org == "": kwargs = {"org": ""} jobs = [job for job in jobs if tasks[job["task_id"]]["organization"] is None] else: - kwargs = {"org_id": org} - jobs = [job for job in jobs if tasks[job["task_id"]]["organization"] == org] + kwargs = {} + jobs = jobs.raw return jobs, kwargs @@ -75,31 +75,51 @@ def _test_get_job_403(self, user, jid, **kwargs): ) assert response.status == HTTPStatus.FORBIDDEN - @pytest.mark.parametrize("org", [None, "", 1, 2]) - def test_admin_get_job(self, jobs, tasks, org): - jobs, kwargs = filter_jobs(jobs, tasks, org) - - # keep only the reasonable amount of jobs - for job in jobs[:8]: - self._test_get_job_200("admin2", job["id"], job, **kwargs) + def test_admin_can_get_sandbox_job(self, jobs, tasks): + job = next(job for job in jobs if tasks[job["task_id"]]["organization"] is None) + self._test_get_job_200("admin2", job["id"], job) - @pytest.mark.parametrize("org_id", ["", None, 1, 2]) - @pytest.mark.parametrize("groups", [["business"], ["user"], ["worker"], []]) - def test_non_admin_get_job(self, org_id, groups, users, jobs, tasks, projects, org_staff): - # keep the reasonable amount of users and jobs - users = [u for u in users if u["groups"] == groups][:4] - jobs, kwargs = filter_jobs(jobs, tasks, org_id) - org_staff = org_staff(org_id) + def test_admin_can_get_org_job(self, jobs, tasks): + job = next(job for job in jobs if tasks[job["task_id"]]["organization"] is not None) + self._test_get_job_200("admin2", job["id"], job) - for job in jobs[:8]: - job_staff = get_job_staff(job, tasks, projects) + @pytest.mark.parametrize("groups", [["business"], ["user"]]) + def test_non_admin_org_staff_can_get_job( + self, groups, users, organizations, org_staff, jobs_by_org + ): + user, org_id = next( + (user, org["id"]) + for user in users + for org in organizations + if user["groups"] == groups and user["id"] in org_staff(org["id"]) + ) + job = jobs_by_org[org_id][0] + self._test_get_job_200(user["username"], job["id"], job) + + @pytest.mark.parametrize("groups", [["business"], ["user"], ["worker"]]) + def test_non_admin_job_staff_can_get_job(self, groups, users, jobs, is_job_staff): + user, job = next( + (user, job) + for user in users + for job in jobs + if user["groups"] == groups and is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_200(user["username"], job["id"], job) - # check if the specific user in job_staff to see the job - for user in users: - if user["id"] in job_staff | org_staff: - self._test_get_job_200(user["username"], job["id"], job, **kwargs) - else: - self._test_get_job_403(user["username"], job["id"], **kwargs) + @pytest.mark.parametrize("groups", [["business"], ["user"], ["worker"]]) + def test_non_admin_non_job_staff_non_org_staff_cannot_get_job( + self, groups, users, organizations, org_staff, jobs, is_job_staff + ): + user, job_id = next( + (user, job["id"]) + for user in users + for org in organizations + for job in jobs + if user["groups"] == groups + and user["id"] not in org_staff(org["id"]) + and not is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_403(user["username"], job_id) @pytest.mark.usefixtures("restore_db_per_class") @@ -176,9 +196,9 @@ def test_can_use_simple_filter_for_object_list(self, field): @pytest.mark.usefixtures("restore_db_per_class") class TestGetAnnotations: - def _test_get_job_annotations_200(self, user, jid, data, **kwargs): + def _test_get_job_annotations_200(self, user, jid, data): with make_api_client(user) as client: - (_, response) = client.jobs_api.retrieve_annotations(jid, **kwargs) + (_, response) = client.jobs_api.retrieve_annotations(jid) assert response.status == HTTPStatus.OK response_data = json.loads(response.data) @@ -187,10 +207,10 @@ def _test_get_job_annotations_200(self, user, jid, data, **kwargs): == {} ) - def _test_get_job_annotations_403(self, user, jid, **kwargs): + def _test_get_job_annotations_403(self, user, jid): with make_api_client(user) as client: (_, response) = client.jobs_api.retrieve_annotations( - jid, **kwargs, _check_status=False, _parse_response=False + jid, _check_status=False, _parse_response=False ) assert response.status == HTTPStatus.FORBIDDEN @@ -221,15 +241,13 @@ def test_user_get_job_annotations( find_job_staff_user, ): users = [u for u in users if u["groups"] == groups] - jobs, kwargs = filter_jobs(jobs, tasks, org) + jobs, _ = filter_jobs(jobs, tasks, org) username, job_id = find_job_staff_user(jobs, users, job_staff) if expect_success: - self._test_get_job_annotations_200( - username, job_id, annotations["job"][str(job_id)], **kwargs - ) + self._test_get_job_annotations_200(username, job_id, annotations["job"][str(job_id)]) else: - self._test_get_job_annotations_403(username, job_id, **kwargs) + self._test_get_job_annotations_403(username, job_id) @pytest.mark.parametrize("org", [2]) @pytest.mark.parametrize( @@ -258,15 +276,15 @@ def test_member_get_job_annotations( find_users, ): users = find_users(org=org, role=role) - jobs, kwargs = filter_jobs(jobs, tasks, org) + jobs, _ = filter_jobs(jobs, tasks, org) username, jid = find_job_staff_user(jobs, users, job_staff) if expect_success: data = annotations["job"][str(jid)] data["shapes"] = sorted(data["shapes"], key=lambda a: a["id"]) - self._test_get_job_annotations_200(username, jid, data, **kwargs) + self._test_get_job_annotations_200(username, jid, data) else: - self._test_get_job_annotations_403(username, jid, **kwargs) + self._test_get_job_annotations_403(username, jid) @pytest.mark.parametrize("org", [1]) @pytest.mark.parametrize( @@ -285,34 +303,23 @@ def test_non_member_get_job_annotations( find_users, ): users = find_users(privilege=privilege, exclude_org=org) - jobs, kwargs = filter_jobs(jobs, tasks, org) + jobs, _ = filter_jobs(jobs, tasks, org) username, job_id = find_job_staff_user(jobs, users, False) - kwargs = {"org_id": org} if expect_success: - self._test_get_job_annotations_200( - username, job_id, annotations["job"][str(job_id)], **kwargs - ) + self._test_get_job_annotations_200(username, job_id, annotations["job"][str(job_id)]) else: - self._test_get_job_annotations_403(username, job_id, **kwargs) + self._test_get_job_annotations_403(username, job_id) @pytest.mark.usefixtures("restore_db_per_function") class TestPatchJobAnnotations: - def _check_respone(self, username, jid, expect_success, data=None, org=None): - kwargs = {} - if org is not None: - if isinstance(org, str): - kwargs["org"] = org - else: - kwargs["org_id"] = org - + def _check_response(self, username, jid, expect_success, data=None): with make_api_client(username) as client: (_, response) = client.jobs_api.partial_update_annotations( id=jid, patched_labeled_data_request=deepcopy(data), action="update", - **kwargs, _parse_response=expect_success, _check_status=expect_success, ) @@ -375,7 +382,7 @@ def test_member_update_job_annotations( username, jid = find_job_staff_user(filtered_jobs, users, job_staff) data = request_data(jid) - self._check_respone(username, jid, expect_success, data, org=org) + self._check_response(username, jid, expect_success, data) @pytest.mark.parametrize("org", [2]) @pytest.mark.parametrize( @@ -399,7 +406,7 @@ def test_non_member_update_job_annotations( username, jid = find_job_staff_user(filtered_jobs, users, False) data = request_data(jid) - self._check_respone(username, jid, expect_success, data, org=org) + self._check_response(username, jid, expect_success, data) @pytest.mark.parametrize("org", [""]) @pytest.mark.parametrize( @@ -433,7 +440,7 @@ def test_user_update_job_annotations( username, jid = find_job_staff_user(filtered_jobs, users, job_staff) data = request_data(jid) - self._check_respone(username, jid, expect_success, data, org=org) + self._check_response(username, jid, expect_success, data) @pytest.mark.usefixtures("restore_db_per_function") @@ -503,7 +510,6 @@ def test_member_update_job_assignee( (_, response) = client.jobs_api.partial_update( id=jid, patched_job_write_request={"assignee": assignee}, - org_id=org, _parse_response=expect_success, _check_status=expect_success, ) @@ -716,30 +722,106 @@ def _test_get_job_preview_403(self, username, jid, **kwargs): ) assert response.status == HTTPStatus.FORBIDDEN - @pytest.mark.parametrize("org", [None, "", 1, 2]) - def test_admin_get_job_preview(self, jobs, tasks, org): - jobs, kwargs = filter_jobs(jobs, tasks, org) + def test_admin_get_sandbox_job_preview(self, jobs, tasks): + job_id = next(job["id"] for job in jobs if not tasks[job["task_id"]]["organization"]) + self._test_get_job_preview_200("admin2", job_id) - # keep only the reasonable amount of jobs - for job in jobs[:8]: - self._test_get_job_preview_200("admin2", job["id"], **kwargs) + def test_admin_get_org_job_preview(self, jobs, tasks): + job_id = next(job["id"] for job in jobs if tasks[job["task_id"]]["organization"]) + self._test_get_job_preview_200("admin2", job_id) - @pytest.mark.parametrize("org_id", ["", None, 1, 2]) - @pytest.mark.parametrize("groups", [["business"], ["user"], ["worker"], []]) - def test_non_admin_get_job_preview( - self, org_id, groups, users, jobs, tasks, projects, org_staff + def test_business_can_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff): + username, job_id = next( + (user["username"], job["id"]) + for user in find_users(privilege="business") + for job in jobs + if is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_preview_200(username, job_id) + + def test_user_can_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff): + username, job_id = next( + (user["username"], job["id"]) + for user in find_users(privilege="user") + for job in jobs + if is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_preview_200(username, job_id) + + def test_business_cannot_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff): + username, job_id = next( + (user["username"], job["id"]) + for user in find_users(privilege="business") + for job in jobs + if not is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_preview_403(username, job_id) + + def test_user_cannot_get_job_preview_in_sandbox(self, find_users, jobs, is_job_staff): + username, job_id = next( + (user["username"], job["id"]) + for user in find_users(privilege="user") + for job in jobs + if not is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_preview_403(username, job_id) + + def test_org_staff_can_get_job_preview_in_org( + self, organizations, users, org_staff, jobs_by_org ): - # keep the reasonable amount of users and jobs - users = [u for u in users if u["groups"] == groups][:4] - jobs, kwargs = filter_jobs(jobs, tasks, org_id) - org_staff = org_staff(org_id) + username, job_id = next( + (user["username"], jobs_by_org[org["id"]][0]["id"]) + for user in users + for org in organizations + if user["id"] in org_staff(org["id"]) + ) + self._test_get_job_preview_200(username, job_id) - for job in jobs[:8]: - job_staff = get_job_staff(job, tasks, projects) + def test_job_staff_can_get_job_preview_in_org( + self, organizations, users, jobs_by_org, is_job_staff + ): + username, job_id = next( + (user["username"], job["id"]) + for user in users + for org in organizations + for job in jobs_by_org[org["id"]] + if is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_preview_200(username, job_id) + + def test_job_staff_can_get_job_preview_in_sandbox(self, users, jobs, tasks, is_job_staff): + username, job_id = next( + (user["username"], job["id"]) + for user in users + for job in jobs + if is_job_staff(user["id"], job["id"]) and tasks[job["task_id"]]["organization"] is None + ) + self._test_get_job_preview_200(username, job_id) - # check if the specific user in job_staff to see the job preview - for user in users: - if user["id"] in job_staff | org_staff: - self._test_get_job_preview_200(user["username"], job["id"], **kwargs) - else: - self._test_get_job_preview_403(user["username"], job["id"], **kwargs) + def test_non_org_staff_non_job_staff_cannot_get_job_preview_in_org( + self, users, organizations, jobs_by_org, is_job_staff, org_staff + ): + username, job_id = next( + (user["username"], job["id"]) + for user in users + for org in organizations + for job in jobs_by_org[org["id"]] + if user["id"] not in org_staff(org["id"]) and not is_job_staff(user["id"], job["id"]) + ) + self._test_get_job_preview_403(username, job_id) + + +@pytest.mark.usefixtures("restore_db_per_class") +class TestGetJobDataMeta: + def test_admin_can_get_job_meta(self, jobs): + with make_api_client("admin1") as client: + job_id = jobs.raw[0]["id"] + + client.organization_slug = None + client.jobs_api.retrieve_data_meta(job_id) + + client.organization_slug = "" + client.jobs_api.retrieve_data_meta(job_id) + + client.organization_slug = "org1" + client.jobs_api.retrieve_data_meta(job_id) diff --git a/tests/python/rest_api/test_labels.py b/tests/python/rest_api/test_labels.py index bb8bd47612f..36743acf6b2 100644 --- a/tests/python/rest_api/test_labels.py +++ b/tests/python/rest_api/test_labels.py @@ -491,9 +491,9 @@ class TestGetLabels(_TestLabelsPermissionsBase): def setup(self, restore_db_per_class, _base_setup): # pylint: disable=arguments-differ pass - def _test_get_ok(self, user, lid, data, **kwargs): + def _test_get_ok(self, user, lid, data): with make_api_client(user) as client: - (_, response) = client.labels_api.retrieve(lid, **kwargs) + (_, response) = client.labels_api.retrieve(lid) assert response.status == HTTPStatus.OK assert ( DeepDiff( @@ -505,10 +505,10 @@ def _test_get_ok(self, user, lid, data, **kwargs): == {} ) - def _test_get_denied(self, user, lid, **kwargs): + def _test_get_denied(self, user, lid): with make_api_client(user) as client: (_, response) = client.labels_api.retrieve( - lid, **kwargs, _check_status=False, _parse_response=False + lid, _check_status=False, _parse_response=False ) assert response.status == HTTPStatus.FORBIDDEN @@ -531,15 +531,12 @@ def test_regular_user_get_sandbox_label(self, user_sandbox_case): self._test_get_denied(user["username"], label["id"]) def test_regular_user_get_org_label(self, user_org_case): - label, user, org_id, is_staff = get_attrs( - user_org_case, ["label", "user", "org_id", "is_staff"] - ) + label, user, is_staff = get_attrs(user_org_case, ["label", "user", "is_staff"]) - kwargs = {"org_id": org_id} if is_staff: - self._test_get_ok(user["username"], label["id"], label, **kwargs) + self._test_get_ok(user["username"], label["id"], label) else: - self._test_get_denied(user["username"], label["id"], **kwargs) + self._test_get_denied(user["username"], label["id"]) class TestPatchLabels(_TestLabelsPermissionsBase): @@ -727,20 +724,16 @@ def test_regular_user_patch_sandbox_label(self, user_sandbox_case): self._test_update_denied(user["username"], label["id"], patch_data) def test_regular_user_patch_org_label(self, user_org_case): - label, user, org_id, is_staff = get_attrs( - user_org_case, ["label", "user", "org_id", "is_staff"] - ) - - kwargs = {"org_id": org_id} + label, user, is_staff = get_attrs(user_org_case, ["label", "user", "is_staff"]) expected_data, patch_data, *_ = self._get_patch_data(label) if is_staff: self._test_update_ok( - user["username"], label["id"], patch_data, expected_data=expected_data, **kwargs + user["username"], label["id"], patch_data, expected_data=expected_data ) else: - self._test_update_denied(user["username"], label["id"], patch_data, **kwargs) + self._test_update_denied(user["username"], label["id"], patch_data) class TestDeleteLabels(_TestLabelsPermissionsBase): @@ -820,13 +813,9 @@ def test_regular_user_delete_sandbox_label(self, user_sandbox_case): self._test_delete_denied(user["username"], label["id"]) def test_regular_user_delete_org_label(self, user_org_case): - label, user, org_id, is_staff = get_attrs( - user_org_case, ["label", "user", "org_id", "is_staff"] - ) - - kwargs = {"org_id": org_id} + label, user, is_staff = get_attrs(user_org_case, ["label", "user", "is_staff"]) if is_staff: - self._test_delete_ok(user["username"], label["id"], **kwargs) + self._test_delete_ok(user["username"], label["id"]) else: - self._test_delete_denied(user["username"], label["id"], **kwargs) + self._test_delete_denied(user["username"], label["id"]) diff --git a/tests/python/rest_api/test_projects.py b/tests/python/rest_api/test_projects.py index b43aa641638..0e296122d9d 100644 --- a/tests/python/rest_api/test_projects.py +++ b/tests/python/rest_api/test_projects.py @@ -40,9 +40,9 @@ def _find_project_by_user_org(self, user, projects, is_project_staff_flag, is_pr if is_project_staff(user["id"], p["id"]) == is_project_staff_flag: return p["id"] - def _test_response_200(self, username, project_id, **kwargs): + def _test_response_200(self, username, project_id): with make_api_client(username) as api_client: - (project, response) = api_client.projects_api.retrieve(project_id, **kwargs) + (project, response) = api_client.projects_api.retrieve(project_id) assert response.status == HTTPStatus.OK assert project_id == project.id @@ -122,7 +122,7 @@ def test_if_maintainer_or_owner_can_see_project( ) ) - self._test_response_200(user["username"], pid, org_id=user["org"]) + self._test_response_200(user["username"], pid) @pytest.mark.parametrize("role", ("supervisor", "worker")) def test_if_org_member_supervisor_or_worker_can_see_project( @@ -138,7 +138,7 @@ def test_if_org_member_supervisor_or_worker_can_see_project( ) ) - self._test_response_200(user["username"], pid, org_id=user["org"]) + self._test_response_200(user["username"], pid) class TestProjectsListFilters(CollectionSimpleFilterTestBase): @@ -994,7 +994,7 @@ def test_if_maintainer_or_owner_can_see_project_preview( ) ) - self._test_response_200(user["username"], pid, org_id=user["org"]) + self._test_response_200(user["username"], pid) @pytest.mark.usefixtures("restore_db_per_function") diff --git a/tests/python/rest_api/test_resource_import_export.py b/tests/python/rest_api/test_resource_import_export.py index e3db4d17dac..a079c397233 100644 --- a/tests/python/rest_api/test_resource_import_export.py +++ b/tests/python/rest_api/test_resource_import_export.py @@ -270,14 +270,11 @@ def test_import_resource_from_cloud_storage_with_default_location( @pytest.mark.parametrize( "obj, resource", [ - ("projects", "annotations"), ("projects", "dataset"), - ("projects", "backup"), ("tasks", "annotations"), - ("tasks", "dataset"), - ("tasks", "backup"), ("jobs", "annotations"), - ("jobs", "dataset"), + ("tasks", "backup"), + ("projects", "backup"), ], ) def test_user_cannot_import_from_cloud_storage_with_specific_location_without_access( diff --git a/tests/python/rest_api/test_tasks.py b/tests/python/rest_api/test_tasks.py index 0b9332c863a..d680e1589d1 100644 --- a/tests/python/rest_api/test_tasks.py +++ b/tests/python/rest_api/test_tasks.py @@ -400,7 +400,6 @@ def test_user_update_task_annotations( (_, response) = api_client.tasks_api.partial_update_annotations( id=tid, action="update", - org=org, patched_labeled_data_request=deepcopy(data), _parse_response=False, _check_status=False, @@ -441,7 +440,6 @@ def test_member_update_task_annotation( with make_api_client(username) as api_client: (_, response) = api_client.tasks_api.partial_update_annotations( id=tid, - org_id=org, action="update", patched_labeled_data_request=deepcopy(data), _parse_response=False, @@ -863,8 +861,9 @@ def test_create_task_with_cloud_storage_files( "server_files": cloud_storage_content, } + kwargs = {"org": org} if org else {} _test_create_task( - self._USERNAME, task_spec, data_spec, content_type="application/json", org=org + self._USERNAME, task_spec, data_spec, content_type="application/json", **kwargs ) @pytest.mark.with_external_services @@ -967,7 +966,7 @@ def test_create_task_with_cloud_storage_directories_and_excluded_files( ) with make_api_client(self._USERNAME) as api_client: - (task, response) = api_client.tasks_api.retrieve(task_id, org=org) + (task, response) = api_client.tasks_api.retrieve(task_id) assert response.status == HTTPStatus.OK assert task.size == task_size @@ -1056,7 +1055,6 @@ def test_user_cannot_create_task_with_cloud_storage_without_access( (None, "[e-z]*.jpeg", False, 0), ], ) - @pytest.mark.parametrize("org", [""]) def test_create_task_with_file_pattern( self, cloud_storage_id, @@ -1064,7 +1062,6 @@ def test_create_task_with_file_pattern( filename_pattern, sub_dir, task_size, - org, cloud_storages, request, ): @@ -1131,11 +1128,11 @@ def test_create_task_with_file_pattern( if task_size: task_id, _ = _test_create_task( - self._USERNAME, task_spec, data_spec, content_type="application/json", org=org + self._USERNAME, task_spec, data_spec, content_type="application/json" ) with make_api_client(self._USERNAME) as api_client: - (task, response) = api_client.tasks_api.retrieve(task_id, org=org) + (task, response) = api_client.tasks_api.retrieve(task_id) assert response.status == HTTPStatus.OK assert task.size == task_size else: @@ -1407,11 +1404,11 @@ class TestWorkWithTask: @pytest.mark.with_external_services @pytest.mark.parametrize( - "cloud_storage_id, manifest, org", - [(1, "manifest.jsonl", "")], # public bucket + "cloud_storage_id, manifest", + [(1, "manifest.jsonl")], # public bucket ) def test_work_with_task_containing_non_stable_cloud_storage_files( - self, cloud_storage_id, manifest, org, cloud_storages, request + self, cloud_storage_id, manifest, cloud_storages, request ): image_name = "image_case_65_1.png" cloud_storage_content = [image_name, manifest] @@ -1429,7 +1426,7 @@ def test_work_with_task_containing_non_stable_cloud_storage_files( } task_id, _ = _test_create_task( - self._USERNAME, task_spec, data_spec, content_type="application/json", org=org + self._USERNAME, task_spec, data_spec, content_type="application/json" ) # save image from the "public" bucket and remove it temporary @@ -1507,7 +1504,7 @@ def test_org_task_assigneed_to_see_task_preview( tasks = list(filter(lambda x: x["project_id"] == project_id and x["assignee"], tasks)) assert len(tasks) - self._test_assigned_users_to_see_task_preview(tasks, users, is_task_staff, org=org["slug"]) + self._test_assigned_users_to_see_task_preview(tasks, users, is_task_staff) @pytest.mark.parametrize("project_id, groups", [(1, "user")]) def test_task_unassigned_cannot_see_task_preview( diff --git a/tests/python/rest_api/utils.py b/tests/python/rest_api/utils.py index 2616bbeb113..d32f76b3c61 100644 --- a/tests/python/rest_api/utils.py +++ b/tests/python/rest_api/utils.py @@ -163,7 +163,7 @@ def _test_create_task(username, spec, data, content_type, **kwargs): del data["client_files"] (_, response) = api_client.tasks_api.create_data( - task.id, data_request=deepcopy(data), _content_type=content_type, **kwargs + task.id, data_request=deepcopy(data), _content_type=content_type ) assert response.status == HTTPStatus.ACCEPTED diff --git a/tests/python/sdk/test_client.py b/tests/python/sdk/test_client.py index 4c3a63837f8..7554e8f5f2d 100644 --- a/tests/python/sdk/test_client.py +++ b/tests/python/sdk/test_client.py @@ -10,7 +10,6 @@ import packaging.version as pv import pytest from cvat_sdk import Client, models -from cvat_sdk.api_client.exceptions import NotFoundException from cvat_sdk.core.client import Config, make_client from cvat_sdk.core.exceptions import IncompatibleVersionException, InvalidHostException from cvat_sdk.exceptions import ApiException @@ -235,17 +234,56 @@ def test_organization_contexts(admin_user: str): client.projects.retrieve(personal_project.id) client.projects.retrieve(org_project.id) - # only the personal project should be visible in the personal workspace + # retrieve personal and org projects by id client.organization_slug = "" client.projects.retrieve(personal_project.id) - with pytest.raises(NotFoundException): - client.projects.retrieve(org_project.id) + client.projects.retrieve(org_project.id) - # only the organizational project should be visible in the organization + # org context doesn't make sense for detailed request client.organization_slug = org.slug client.projects.retrieve(org_project.id) - with pytest.raises(NotFoundException): - client.projects.retrieve(personal_project.id) + client.projects.retrieve(personal_project.id) + + +@pytest.mark.usefixtures("restore_db_per_function") +def test_organization_filtering(regular_lonely_user: str, fxt_image_file): + with make_client(BASE_URL, credentials=(regular_lonely_user, USER_PASS)) as client: + org = client.organizations.create(models.OrganizationWriteRequest(slug="testorg")) + + # create a project and task in sandbox + client.organization_slug = None + client.projects.create(models.ProjectWriteRequest(name="personal_project")) + client.tasks.create_from_data( + spec={"name": "personal_task", "labels": [{"name": "a"}]}, resources=[fxt_image_file] + ) + + # create a project and task in the organization + client.organization_slug = org.slug + client.projects.create(models.ProjectWriteRequest(name="org_project")) + client.tasks.create_from_data( + spec={"name": "org_task", "labels": [{"name": "a"}]}, resources=[fxt_image_file] + ) + + # return only non-org objects if org parameter is empty + client.organization_slug = "" + projects, tasks, jobs = client.projects.list(), client.tasks.list(), client.jobs.list() + + assert len(projects) == len(tasks) == len(jobs) == 1 + assert projects[0].organization == tasks[0].organization == jobs[0].organization == None + + # return all objects if org parameter wasn't presented + client.organization_slug = None + projects, tasks, jobs = client.projects.list(), client.tasks.list(), client.jobs.list() + + assert len(projects) == len(tasks) == len(jobs) == 2 + assert {None, org.id} == set([a.organization for a in (*projects, *tasks, *jobs)]) + + # return only org objects if org parameter is presented and not empty + client.organization_slug = org.slug + projects, tasks, jobs = client.projects.list(), client.tasks.list(), client.jobs.list() + + assert len(projects) == len(tasks) == len(jobs) == 1 + assert projects[0].organization == tasks[0].organization == jobs[0].organization == org.id def test_organization_context_manager(): diff --git a/tests/python/shared/assets/cvat_db/data.json b/tests/python/shared/assets/cvat_db/data.json index 4f5566860d8..afc008c9808 100644 --- a/tests/python/shared/assets/cvat_db/data.json +++ b/tests/python/shared/assets/cvat_db/data.json @@ -525,6 +525,14 @@ "expire_date": "2022-10-31T17:09:16.915Z" } }, +{ + "model": "sessions.session", + "pk": "d5hzap008bh0kj64738vfcnlddz3rq3q", + "fields": { + "session_data": ".eJxVjMsOwiAQRf-FtSHQ4VFcuvcbyMAwUjU0Ke3K-O_apAvd3nPOfYmI21rj1ssSJxJnoZU4_Y4J86O0ndAd222WeW7rMiW5K_KgXV5nKs_L4f4dVOz1WzvjmIjAQWaTBstgNIzsXAnkgUJSQVmA4tFzZgDLxHoEp82AWEIQ7w8KLDgb:1ptSj6:O-nWOvTicB2nf5bWCA0_-_IWElDl02tyQY-O2obZc4Q", + "expire_date": "2023-05-15T12:33:56.878Z" + } +}, { "model": "sessions.session", "pk": "dpaw6pntyqwr5l6qjv6zq3yoajp301be", @@ -685,6 +693,14 @@ "expire_date": "2022-02-25T14:54:28.092Z" } }, +{ + "model": "sessions.session", + "pk": "y0gqgy87qj5rzdymhcrm5cilkbzmmmdi", + "fields": { + "session_data": ".eJxVjEEOwiAQRe_C2hCmdCi4dN8zkIEBqZqSlHZlvLtt0oVu_3vvv4WnbS1-a2nxE4urACUuv2Og-EzzQfhB873KWOd1mYI8FHnSJsfK6XU73b-DQq3sdbbUJaNz4MToIvfEoBBwQDDKoYXOaZNcwH4nGYGGAGQtdho4Rg3i8wUMbDfM:1ptP7Q:QohmTMRYLohe_xym4-jZdnDdhEcDBLoWmnf-6tnNuAY", + "expire_date": "2023-05-15T08:42:48.134Z" + } +}, { "model": "authtoken.token", "pk": "2dcf8d2ff5032c3cf7d276549af3a50edb4f11c8", @@ -715,6 +731,16 @@ "created": "2023-03-30T09:37:31.700Z" } }, +{ + "model": "authtoken.token", + "pk": "c051fe19df24a0ac4c6bec5e635034271c9549dc", + "fields": { + "user": [ + "business1" + ], + "created": "2023-05-01T08:42:48.127Z" + } +}, { "model": "sites.site", "pk": 1, diff --git a/tests/python/shared/assets/jobs.json b/tests/python/shared/assets/jobs.json index ecee087bcf5..5a580305e24 100644 --- a/tests/python/shared/assets/jobs.json +++ b/tests/python/shared/assets/jobs.json @@ -19,6 +19,7 @@ "url": "http://localhost:8080/api/labels?job_id=26" }, "mode": "annotation", + "organization": null, "project_id": null, "stage": "annotation", "start_frame": 6, @@ -45,6 +46,7 @@ "url": "http://localhost:8080/api/labels?job_id=25" }, "mode": "annotation", + "organization": null, "project_id": null, "stage": "annotation", "start_frame": 0, @@ -71,6 +73,7 @@ "url": "http://localhost:8080/api/labels?job_id=24" }, "mode": "annotation", + "organization": null, "project_id": 12, "stage": "annotation", "start_frame": 0, @@ -97,6 +100,7 @@ "url": "http://localhost:8080/api/labels?job_id=23" }, "mode": "annotation", + "organization": null, "project_id": null, "stage": "annotation", "start_frame": 0, @@ -123,6 +127,7 @@ "url": "http://localhost:8080/api/labels?job_id=22" }, "mode": "annotation", + "organization": 2, "project_id": 11, "stage": "annotation", "start_frame": 0, @@ -149,6 +154,7 @@ "url": "http://localhost:8080/api/labels?job_id=21" }, "mode": "annotation", + "organization": 2, "project_id": null, "stage": "annotation", "start_frame": 0, @@ -175,6 +181,7 @@ "url": "http://localhost:8080/api/labels?job_id=19" }, "mode": "interpolation", + "organization": null, "project_id": 8, "stage": "annotation", "start_frame": 0, @@ -201,6 +208,7 @@ "url": "http://localhost:8080/api/labels?job_id=18" }, "mode": "annotation", + "organization": 2, "project_id": 5, "stage": "annotation", "start_frame": 0, @@ -227,6 +235,7 @@ "url": "http://localhost:8080/api/labels?job_id=17" }, "mode": "annotation", + "organization": 2, "project_id": 4, "stage": "annotation", "start_frame": 0, @@ -259,6 +268,7 @@ "url": "http://localhost:8080/api/labels?job_id=16" }, "mode": "annotation", + "organization": 2, "project_id": 2, "stage": "annotation", "start_frame": 0, @@ -285,6 +295,7 @@ "url": "http://localhost:8080/api/labels?job_id=14" }, "mode": "annotation", + "organization": null, "project_id": 1, "stage": "annotation", "start_frame": 15, @@ -311,6 +322,7 @@ "url": "http://localhost:8080/api/labels?job_id=13" }, "mode": "annotation", + "organization": null, "project_id": 1, "stage": "acceptance", "start_frame": 10, @@ -337,6 +349,7 @@ "url": "http://localhost:8080/api/labels?job_id=12" }, "mode": "annotation", + "organization": null, "project_id": 1, "stage": "validation", "start_frame": 5, @@ -369,6 +382,7 @@ "url": "http://localhost:8080/api/labels?job_id=11" }, "mode": "annotation", + "organization": null, "project_id": 1, "stage": "annotation", "start_frame": 0, @@ -401,6 +415,7 @@ "url": "http://localhost:8080/api/labels?job_id=10" }, "mode": "annotation", + "organization": null, "project_id": null, "stage": "annotation", "start_frame": 0, @@ -427,6 +442,7 @@ "url": "http://localhost:8080/api/labels?job_id=9" }, "mode": "annotation", + "organization": 2, "project_id": null, "stage": "annotation", "start_frame": 0, @@ -453,6 +469,7 @@ "url": "http://localhost:8080/api/labels?job_id=8" }, "mode": "annotation", + "organization": null, "project_id": null, "stage": "annotation", "start_frame": 0, @@ -485,6 +502,7 @@ "url": "http://localhost:8080/api/labels?job_id=7" }, "mode": "interpolation", + "organization": null, "project_id": null, "stage": "annotation", "start_frame": 0, @@ -517,6 +535,7 @@ "url": "http://localhost:8080/api/labels?job_id=2" }, "mode": "annotation", + "organization": 1, "project_id": null, "stage": "annotation", "start_frame": 0,